diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index 346dcdece41..40e2426cd39 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -20,6 +20,8 @@ import java.io.Serializable; import java.net.InetSocketAddress; import java.net.URI; import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.time.Instant; @@ -28,6 +30,7 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.ArrayList; +import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; @@ -38,6 +41,8 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -1564,4 +1569,88 @@ public class HttpHeaders implements MultiValueMap, Serializable return (headers.readOnly ? headers : new HttpHeaders(headers, true)); } + /** + * Returns a {@code HttpHeaders} consumer that adds Basic Authentication. + * More specifically: a consumer that adds an {@linkplain #AUTHORIZATION + * Authorization} header based on the given username and password. Meant + * to be used in combination with + * {@link org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec#headers(java.util.function.Consumer)}. + *

Note that Basic Authentication only supports characters in the + * {@linkplain StandardCharsets#ISO_8859_1 ISO-8859-1} character set. + * + * @param username the username + * @param password the password + * @return a consumer that adds a Basic Authentication header + */ + public static Consumer basicAuthenticationConsumer(String username,String password) { + return basicAuthenticationConsumer(() -> username, () -> password); + + } + + /** + * Returns a {@code HttpHeaders} consumer that adds Basic Authentication. + * More specifically: a consumer that adds an {@linkplain #AUTHORIZATION + * Authorization} header based on the given username and password + * suppliers. Meant to be used in combination with + * {@link org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec#headers(java.util.function.Consumer)}. + *

Note that Basic Authentication only supports characters in the + * {@linkplain StandardCharsets#ISO_8859_1 ISO-8859-1} character set. + * + * @param usernameSupplier supplier for the username + * @param passwordSupplier supplier for the password + * @return a consumer that adds a Basic Authentication header + */ + public static Consumer basicAuthenticationConsumer(Supplier usernameSupplier, + Supplier passwordSupplier) { + + Assert.notNull(usernameSupplier, "Username Supplier must not be null"); + Assert.notNull(passwordSupplier, "Password Supplier must not be null"); + + return new BasicAuthenticationConsumer(usernameSupplier, passwordSupplier); + } + + + /** + * @see #basicAuthenticationConsumer + */ + private static class BasicAuthenticationConsumer implements Consumer { + + private final Supplier usernameSupplier; + + private final Supplier passwordSupplier; + + public BasicAuthenticationConsumer(Supplier usernameSupplier, + Supplier passwordSupplier) { + this.usernameSupplier = usernameSupplier; + this.passwordSupplier = passwordSupplier; + } + + @Override + public void accept(HttpHeaders httpHeaders) { + String username = this.usernameSupplier.get(); + String password = this.passwordSupplier.get(); + + Assert.state(username != null, "Supplied username is null"); + Assert.state(password != null, "Supplied password is null"); + + checkIllegalCharacters(username, password); + + String credentialsString = username + ":" + password; + byte[] credentialBytes = credentialsString.getBytes(StandardCharsets.ISO_8859_1); + byte[] encodedBytes = Base64.getEncoder().encode(credentialBytes); + String encodedCredentials = new String(encodedBytes, StandardCharsets.ISO_8859_1); + + httpHeaders.set(HttpHeaders.AUTHORIZATION, "Basic " + encodedCredentials); + } + + private static void checkIllegalCharacters(String username, String password) { + // Basic authentication only supports ISO 8859-1 + 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"); + } + } + + } } diff --git a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java index 763967f24fd..a3924ffc141 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java @@ -26,6 +26,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Calendar; import java.util.Collections; import java.util.EnumSet; @@ -33,12 +34,13 @@ import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; import java.util.TimeZone; +import java.util.function.Consumer; import org.hamcrest.Matchers; import org.junit.Test; -import static java.time.format.DateTimeFormatter.*; -import static org.hamcrest.Matchers.*; +import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.*; /** @@ -529,4 +531,23 @@ public class HttpHeadersTests { assertTrue(headers.getFirstZonedDateTime(HttpHeaders.DATE).isEqual(date)); } + @Test + public void basicAuthenticationConsumer() throws Exception { + + String username = "foo"; + String password = "bar"; + + Consumer consumer = + HttpHeaders.basicAuthenticationConsumer(username, password); + + HttpHeaders headers = new HttpHeaders(); + assertFalse(headers.containsKey(HttpHeaders.AUTHORIZATION)); + consumer.accept(headers); + String authorization = headers.getFirst(HttpHeaders.AUTHORIZATION); + assertNotNull(authorization); + assertTrue(authorization.startsWith("Basic ")); + byte[] result = Base64.getDecoder().decode(authorization.substring(6).getBytes(StandardCharsets.ISO_8859_1)); + assertEquals("foo:bar", new String(result, StandardCharsets.ISO_8859_1)); + + } }