diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java
index 9053fdb9c9..5820b8b383 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java
@@ -16,10 +16,12 @@
package org.springframework.web.reactive.function.client;
-import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
+import java.util.Map;
import java.util.Optional;
+import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -40,52 +42,45 @@ import org.springframework.util.Assert;
public abstract class ExchangeFilterFunctions {
/**
- * Name of the {@link ClientRequest} attribute that contains the username, as used by
+ * Name of the {@link ClientRequest} attribute that contains the {@link Credentials}, as used by
* {@link #basicAuthentication()}
*/
- public static final String USERNAME_ATTRIBUTE = ExchangeFilterFunctions.class.getName() + ".username";
-
- /**
- * Name of the {@link ClientRequest} attribute that contains the password, as used by
- * {@link #basicAuthentication()}
- */
- public static final String PASSWORD_ATTRIBUTE = ExchangeFilterFunctions.class.getName() + ".password";
+ public static final String BASIC_AUTHENTICATION_CREDENTIALS_ATTRIBUTE = ExchangeFilterFunctions.class.getName() + ".basicAuthenticationCredentials";
/**
* Return a filter that adds an Authorization header for HTTP Basic Authentication, based on
* the given username and password.
+ *
Note that Basic Authentication only supports characters in the
+ * {@link StandardCharsets#ISO_8859_1 ISO-8859-1} character set.
* @param username the username to use
* @param password the password to use
* @return the {@link ExchangeFilterFunction} that adds the Authorization header
+ * @throws IllegalArgumentException if either {@code username} or {@code password} contain
+ * characters that cannot be encoded to ISO-8859-1
*/
public static ExchangeFilterFunction basicAuthentication(String username, String password) {
Assert.notNull(username, "'username' must not be null");
Assert.notNull(password, "'password' must not be null");
+ checkIllegalCharacters(username, password);
return basicAuthenticationInternal(r -> Optional.of(new Credentials(username, password)));
}
/**
* Return a filter that adds an Authorization header for HTTP Basic Authentication, based on
- * the username and password provided in the
- * {@linkplain ClientRequest#attributes() request attributes}. If the attributes are not found,
- * no authorization header
+ * the {@link Credentials} provided in the
+ * {@linkplain ClientRequest#attributes() request attributes}. If the attribute is not found,
+ * no authorization header is added.
+ *
Note that Basic Authentication only supports characters in the
+ * {@link StandardCharsets#ISO_8859_1 ISO-8859-1} character set.
* @return the {@link ExchangeFilterFunction} that adds the Authorization header
- * @see #USERNAME_ATTRIBUTE
- * @see #PASSWORD_ATTRIBUTE
+ * @see #BASIC_AUTHENTICATION_CREDENTIALS_ATTRIBUTE
+ * @see Credentials#basicAuthenticationCredentials(String, String)
*/
public static ExchangeFilterFunction basicAuthentication() {
return basicAuthenticationInternal(
- request -> {
- Optional username = request.attribute(USERNAME_ATTRIBUTE).map(o -> (String)o);
- Optional password = request.attribute(PASSWORD_ATTRIBUTE).map(o -> (String)o);
- if (username.isPresent() && password.isPresent()) {
- return Optional.of(new Credentials(username.get(), password.get()));
- } else {
- return Optional.empty();
- }
- });
+ request -> request.attribute(BASIC_AUTHENTICATION_CREDENTIALS_ATTRIBUTE).map(o -> (Credentials)o));
}
private static ExchangeFilterFunction basicAuthenticationInternal(
@@ -106,12 +101,28 @@ public abstract class ExchangeFilterFunctions {
}
private static String authorization(Credentials credentials) {
- byte[] credentialBytes = credentials.toByteArray(StandardCharsets.ISO_8859_1);
+ String credentialsString = credentials.username + ":" + credentials.password;
+ byte[] credentialBytes = credentialsString.getBytes(StandardCharsets.ISO_8859_1);
byte[] encodedBytes = Base64.getEncoder().encode(credentialBytes);
String encodedCredentials = new String(encodedBytes, StandardCharsets.ISO_8859_1);
return "Basic " + encodedCredentials;
}
+ /*
+ * Basic authentication only supports ISO 8859-1, see
+ * https://stackoverflow.com/questions/702629/utf-8-characters-mangled-in-http-basic-auth-username#703341
+ */
+ private static void checkIllegalCharacters(String username, String password) {
+ CharsetEncoder encoder = StandardCharsets.ISO_8859_1.newEncoder();
+ if (!encoder.canEncode(username) || !encoder.canEncode(password)) {
+ throw new IllegalArgumentException(
+ "Username or password contains characters that cannot be encoded to ISO-8859-1");
+ }
+
+ }
+
+
+
/**
* Return a filter that returns a given {@link Throwable} as response if the given
* {@link HttpStatus} predicate matches.
@@ -140,20 +151,63 @@ public abstract class ExchangeFilterFunctions {
}
- private static final class Credentials {
+ /**
+ * Represents a combination of username and password, as used by {@link #basicAuthentication()}.
+ * @see #basicAuthenticationCredentials(String, String)
+ */
+ public static final class Credentials {
- private String username;
+ private final String username;
- private String password;
+ private final String password;
+ /**
+ * Create a new {@code Credentials} instance with the given username and password.
+ * @param username the username
+ * @param password the password
+ */
public Credentials(String username, String password) {
+ Assert.notNull(username, "'username' must not be null");
+ Assert.notNull(password, "'password' must not be null");
+
this.username = username;
this.password = password;
}
- public byte[] toByteArray(Charset charset) {
- String credentials = this.username + ":" + this.password;
- return credentials.getBytes(charset);
+ /**
+ * Return a consumer that stores the given username and password in the
+ * {@linkplain ClientRequest.Builder#attributes(java.util.function.Consumer) request
+ * attributes} as a {@code Credentials} object.
+ * @param username the username
+ * @param password the password
+ * @return a consumer that adds the given credentials to the attribute map
+ * @see ClientRequest.Builder#attributes(java.util.function.Consumer)
+ * @see #BASIC_AUTHENTICATION_CREDENTIALS_ATTRIBUTE
+ */
+ public static Consumer