16
16
17
17
package org .springframework .web .reactive .function .client ;
18
18
19
- import java .nio .charset .Charset ;
19
+ import java .nio .charset .CharsetEncoder ;
20
20
import java .nio .charset .StandardCharsets ;
21
21
import java .util .Base64 ;
22
+ import java .util .Map ;
22
23
import java .util .Optional ;
24
+ import java .util .function .Consumer ;
23
25
import java .util .function .Function ;
24
26
import java .util .function .Predicate ;
25
27
40
42
public abstract class ExchangeFilterFunctions {
41
43
42
44
/**
43
- * Name of the {@link ClientRequest} attribute that contains the username , as used by
45
+ * Name of the {@link ClientRequest} attribute that contains the {@link Credentials} , as used by
44
46
* {@link #basicAuthentication()}
45
47
*/
46
- public static final String USERNAME_ATTRIBUTE = ExchangeFilterFunctions .class .getName () + ".username" ;
47
-
48
- /**
49
- * Name of the {@link ClientRequest} attribute that contains the password, as used by
50
- * {@link #basicAuthentication()}
51
- */
52
- public static final String PASSWORD_ATTRIBUTE = ExchangeFilterFunctions .class .getName () + ".password" ;
48
+ public static final String BASIC_AUTHENTICATION_CREDENTIALS_ATTRIBUTE = ExchangeFilterFunctions .class .getName () + ".basicAuthenticationCredentials" ;
53
49
54
50
55
51
/**
56
52
* Return a filter that adds an Authorization header for HTTP Basic Authentication, based on
57
53
* the given username and password.
54
+ * <p>Note that Basic Authentication only supports characters in the
55
+ * {@link StandardCharsets#ISO_8859_1 ISO-8859-1} character set.
58
56
* @param username the username to use
59
57
* @param password the password to use
60
58
* @return the {@link ExchangeFilterFunction} that adds the Authorization header
59
+ * @throws IllegalArgumentException if either {@code username} or {@code password} contain
60
+ * characters that cannot be encoded to ISO-8859-1
61
61
*/
62
62
public static ExchangeFilterFunction basicAuthentication (String username , String password ) {
63
63
Assert .notNull (username , "'username' must not be null" );
64
64
Assert .notNull (password , "'password' must not be null" );
65
65
66
+ checkIllegalCharacters (username , password );
66
67
return basicAuthenticationInternal (r -> Optional .of (new Credentials (username , password )));
67
68
}
68
69
69
70
/**
70
71
* Return a filter that adds an Authorization header for HTTP Basic Authentication, based on
71
- * the username and password provided in the
72
- * {@linkplain ClientRequest#attributes() request attributes}. If the attributes are not found,
73
- * no authorization header
72
+ * the {@link Credentials} provided in the
73
+ * {@linkplain ClientRequest#attributes() request attributes}. If the attribute is not found,
74
+ * no authorization header is added.
75
+ * <p>Note that Basic Authentication only supports characters in the
76
+ * {@link StandardCharsets#ISO_8859_1 ISO-8859-1} character set.
74
77
* @return the {@link ExchangeFilterFunction} that adds the Authorization header
75
- * @see #USERNAME_ATTRIBUTE
76
- * @see #PASSWORD_ATTRIBUTE
78
+ * @see #BASIC_AUTHENTICATION_CREDENTIALS_ATTRIBUTE
79
+ * @see Credentials#basicAuthenticationCredentials(String, String)
77
80
*/
78
81
public static ExchangeFilterFunction basicAuthentication () {
79
82
return basicAuthenticationInternal (
80
- request -> {
81
- Optional <String > username = request .attribute (USERNAME_ATTRIBUTE ).map (o -> (String )o );
82
- Optional <String > password = request .attribute (PASSWORD_ATTRIBUTE ).map (o -> (String )o );
83
- if (username .isPresent () && password .isPresent ()) {
84
- return Optional .of (new Credentials (username .get (), password .get ()));
85
- } else {
86
- return Optional .empty ();
87
- }
88
- });
83
+ request -> request .attribute (BASIC_AUTHENTICATION_CREDENTIALS_ATTRIBUTE ).map (o -> (Credentials )o ));
89
84
}
90
85
91
86
private static ExchangeFilterFunction basicAuthenticationInternal (
@@ -106,12 +101,28 @@ private static ExchangeFilterFunction basicAuthenticationInternal(
106
101
}
107
102
108
103
private static String authorization (Credentials credentials ) {
109
- byte [] credentialBytes = credentials .toByteArray (StandardCharsets .ISO_8859_1 );
104
+ String credentialsString = credentials .username + ":" + credentials .password ;
105
+ byte [] credentialBytes = credentialsString .getBytes (StandardCharsets .ISO_8859_1 );
110
106
byte [] encodedBytes = Base64 .getEncoder ().encode (credentialBytes );
111
107
String encodedCredentials = new String (encodedBytes , StandardCharsets .ISO_8859_1 );
112
108
return "Basic " + encodedCredentials ;
113
109
}
114
110
111
+ /*
112
+ * Basic authentication only supports ISO 8859-1, see
113
+ * https://stackoverflow.com/questions/702629/utf-8-characters-mangled-in-http-basic-auth-username#703341
114
+ */
115
+ private static void checkIllegalCharacters (String username , String password ) {
116
+ CharsetEncoder encoder = StandardCharsets .ISO_8859_1 .newEncoder ();
117
+ if (!encoder .canEncode (username ) || !encoder .canEncode (password )) {
118
+ throw new IllegalArgumentException (
119
+ "Username or password contains characters that cannot be encoded to ISO-8859-1" );
120
+ }
121
+
122
+ }
123
+
124
+
125
+
115
126
/**
116
127
* Return a filter that returns a given {@link Throwable} as response if the given
117
128
* {@link HttpStatus} predicate matches.
@@ -140,20 +151,63 @@ public static ExchangeFilterFunction statusError(Predicate<HttpStatus> statusPre
140
151
}
141
152
142
153
143
- private static final class Credentials {
154
+ /**
155
+ * Represents a combination of username and password, as used by {@link #basicAuthentication()}.
156
+ * @see #basicAuthenticationCredentials(String, String)
157
+ */
158
+ public static final class Credentials {
144
159
145
- private String username ;
160
+ private final String username ;
146
161
147
- private String password ;
162
+ private final String password ;
148
163
164
+ /**
165
+ * Create a new {@code Credentials} instance with the given username and password.
166
+ * @param username the username
167
+ * @param password the password
168
+ */
149
169
public Credentials (String username , String password ) {
170
+ Assert .notNull (username , "'username' must not be null" );
171
+ Assert .notNull (password , "'password' must not be null" );
172
+
150
173
this .username = username ;
151
174
this .password = password ;
152
175
}
153
176
154
- public byte [] toByteArray (Charset charset ) {
155
- String credentials = this .username + ":" + this .password ;
156
- return credentials .getBytes (charset );
177
+ /**
178
+ * Return a consumer that stores the given username and password in the
179
+ * {@linkplain ClientRequest.Builder#attributes(java.util.function.Consumer) request
180
+ * attributes} as a {@code Credentials} object.
181
+ * @param username the username
182
+ * @param password the password
183
+ * @return a consumer that adds the given credentials to the attribute map
184
+ * @see ClientRequest.Builder#attributes(java.util.function.Consumer)
185
+ * @see #BASIC_AUTHENTICATION_CREDENTIALS_ATTRIBUTE
186
+ */
187
+ public static Consumer <Map <String , Object >> basicAuthenticationCredentials (String username , String password ) {
188
+ Credentials credentials = new Credentials (username , password );
189
+ checkIllegalCharacters (username , password );
190
+
191
+ return attributes -> attributes .put (BASIC_AUTHENTICATION_CREDENTIALS_ATTRIBUTE , credentials );
192
+ }
193
+
194
+ @ Override
195
+ public boolean equals (Object o ) {
196
+ if (this == o ) {
197
+ return true ;
198
+ }
199
+ if (o instanceof Credentials ) {
200
+ Credentials other = (Credentials ) o ;
201
+ return this .username .equals (other .username ) &&
202
+ this .password .equals (other .password );
203
+
204
+ }
205
+ return false ;
206
+ }
207
+
208
+ @ Override
209
+ public int hashCode () {
210
+ return 31 * this .username .hashCode () + this .password .hashCode ();
157
211
}
158
212
159
213
}
0 commit comments