Преглед изворни кода

WebFlux httpBasic() matches on XHR requests

Closes gh-9660
Joe Grandja пре 4 година
родитељ
комит
5afeaa3ce7

+ 22 - 2
config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

@@ -41,6 +41,7 @@ import org.springframework.core.ResolvableType;
 import org.springframework.core.annotation.AnnotationAwareOrderComparator;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
@@ -111,6 +112,7 @@ import org.springframework.security.web.server.authentication.AnonymousAuthentic
 import org.springframework.security.web.server.authentication.AuthenticationConverterServerWebExchangeMatcher;
 import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
 import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
+import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
 import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager;
 import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;
 import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;
@@ -1910,13 +1912,25 @@ public class ServerHttpSecurity {
 	 */
 	public final class HttpBasicSpec {
 
+		private final ServerWebExchangeMatcher xhrMatcher = (exchange) -> Mono.just(exchange.getRequest().getHeaders())
+				.filter((h) -> h.getOrEmpty("X-Requested-With").contains("XMLHttpRequest"))
+				.flatMap((h) -> ServerWebExchangeMatcher.MatchResult.match())
+				.switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());
+
 		private ReactiveAuthenticationManager authenticationManager;
 
 		private ServerSecurityContextRepository securityContextRepository;
 
-		private ServerAuthenticationEntryPoint entryPoint = new HttpBasicServerAuthenticationEntryPoint();
+		private ServerAuthenticationEntryPoint entryPoint;
 
 		private HttpBasicSpec() {
+			List<DelegateEntry> entryPoints = new ArrayList<>();
+			entryPoints
+					.add(new DelegateEntry(this.xhrMatcher, new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)));
+			DelegatingServerAuthenticationEntryPoint defaultEntryPoint = new DelegatingServerAuthenticationEntryPoint(
+					entryPoints);
+			defaultEntryPoint.setDefaultEntryPoint(new HttpBasicServerAuthenticationEntryPoint());
+			this.entryPoint = defaultEntryPoint;
 		}
 
 		/**
@@ -1981,7 +1995,13 @@ public class ServerHttpSecurity {
 					MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML, MediaType.MULTIPART_FORM_DATA,
 					MediaType.TEXT_XML);
 			restMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
-			ServerHttpSecurity.this.defaultEntryPoints.add(new DelegateEntry(restMatcher, this.entryPoint));
+			ServerWebExchangeMatcher notHtmlMatcher = new NegatedServerWebExchangeMatcher(
+					new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML));
+			ServerWebExchangeMatcher restNotHtmlMatcher = new AndServerWebExchangeMatcher(
+					Arrays.asList(notHtmlMatcher, restMatcher));
+			ServerWebExchangeMatcher preferredMatcher = new OrServerWebExchangeMatcher(
+					Arrays.asList(this.xhrMatcher, restNotHtmlMatcher));
+			ServerHttpSecurity.this.defaultEntryPoints.add(new DelegateEntry(preferredMatcher, this.entryPoint));
 			AuthenticationWebFilter authenticationFilter = new AuthenticationWebFilter(this.authenticationManager);
 			authenticationFilter
 					.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(this.entryPoint));

+ 23 - 1
config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@ import org.mockito.junit.MockitoJUnitRunner;
 import reactor.core.publisher.Mono;
 import reactor.test.publisher.TestPublisher;
 
+import org.springframework.http.HttpStatus;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfigurationBuilder;
@@ -45,9 +46,11 @@ import org.springframework.security.oauth2.core.endpoint.TestOAuth2Authorization
 import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
 import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
 import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
 import org.springframework.security.web.server.WebFilterChainProxy;
 import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilterTests;
 import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
+import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
 import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter;
 import org.springframework.security.web.server.authentication.logout.DelegatingServerLogoutHandler;
 import org.springframework.security.web.server.authentication.logout.LogoutWebFilter;
@@ -184,6 +187,25 @@ public class ServerHttpSecurityTests {
 		// @formatter:on
 	}
 
+	@Test
+	public void basicWhenXHRRequestThenUnauthorized() {
+		ServerAuthenticationEntryPoint authenticationEntryPoint = spy(
+				new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED));
+		this.http.httpBasic().authenticationEntryPoint(authenticationEntryPoint);
+		this.http.authorizeExchange().anyExchange().authenticated();
+		WebTestClient client = buildClient();
+		// @formatter:off
+		client.get().uri("/")
+				.header("X-Requested-With", "XMLHttpRequest")
+				.exchange()
+				.expectStatus().isUnauthorized()
+				.expectHeader().doesNotExist("WWW-Authenticate")
+				.expectHeader().valueMatches(HttpHeaders.CACHE_CONTROL, ".+")
+				.expectBody().isEmpty();
+		// @formatter:on
+		verify(authenticationEntryPoint).commence(any(), any());
+	}
+
 	@Test
 	public void buildWhenServerWebExchangeFromContextThenFound() {
 		SecurityWebFilterChain filter = this.http.build();