瀏覽代碼

WebFlux httpBasic() matches on XHR requests

Closes gh-9660
Joe Grandja 4 年之前
父節點
當前提交
99db0ca2c5

+ 25 - 3
config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

@@ -33,6 +33,7 @@ import java.util.function.Supplier;
 
 import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
 import org.springframework.security.oauth2.client.web.server.WebSessionOAuth2ServerAuthorizationRequestRepository;
+import org.springframework.http.HttpStatus;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
 import reactor.core.publisher.Mono;
@@ -113,6 +114,7 @@ import org.springframework.security.web.server.WebFilterExchange;
 import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter;
 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;
@@ -2965,11 +2967,17 @@ public class ServerHttpSecurity {
 	 * @see #httpBasic()
 	 */
 	public 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;
 
 		/**
 		 * The {@link ReactiveAuthenticationManager} used to authenticate. Defaults to
@@ -3034,7 +3042,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));
@@ -3043,7 +3057,15 @@ public class ServerHttpSecurity {
 			http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.HTTP_BASIC);
 		}
 
-		private HttpBasicSpec() {}
+		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;
+		}
 	}
 
 	/**

+ 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.
@@ -41,6 +41,7 @@ import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
+import org.springframework.http.HttpStatus;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
 import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
@@ -48,6 +49,8 @@ import org.springframework.security.oauth2.client.web.server.authentication.OAut
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
 import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
+import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
 import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter;
 import org.springframework.security.web.server.savedrequest.ServerRequestCache;
 import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache;
@@ -184,6 +187,25 @@ public class ServerHttpSecurityTests {
 			.expectBody().isEmpty();
 	}
 
+	@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();