瀏覽代碼

Add Multiple Reactive HttpSecurity

Fixes gh-4395
Rob Winch 8 年之前
父節點
當前提交
9141a8a7c0

+ 13 - 1
config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java

@@ -18,8 +18,13 @@
 
 
 package org.springframework.security.config.annotation.web.reactive;
 package org.springframework.security.config.annotation.web.reactive;
 
 
-
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.WebFilterChainFilter;
+
+import java.util.List;
 
 
 /**
 /**
  * @author Rob Winch
  * @author Rob Winch
@@ -28,4 +33,11 @@ import org.springframework.context.annotation.Configuration;
 @Configuration
 @Configuration
 public class WebFluxSecurityConfiguration {
 public class WebFluxSecurityConfiguration {
 
 
+	@Autowired(required = false)
+	private List<SecurityWebFilterChain> securityWebFilterChains;
+
+	@Bean
+	public WebFilterChainFilter springSecurityFilterChain() {
+		return WebFilterChainFilter.fromSecurityWebFilterChainsList(securityWebFilterChains);
+	}
 }
 }

+ 28 - 3
config/src/main/java/org/springframework/security/config/web/server/HttpSecurity.java

@@ -20,10 +20,13 @@ import java.util.List;
 import java.util.Optional;
 import java.util.Optional;
 
 
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
+import org.springframework.security.web.server.SecurityWebFilterChain;
 import org.springframework.security.web.server.context.SecurityContextRepositoryWebFilter;
 import org.springframework.security.web.server.context.SecurityContextRepositoryWebFilter;
-import org.springframework.security.web.server.WebFilterChainFilter;
 import org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter;
 import org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter;
 import org.springframework.security.web.server.context.SecurityContextRepository;
 import org.springframework.security.web.server.context.SecurityContextRepository;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
 import org.springframework.web.server.WebFilter;
 import org.springframework.web.server.WebFilter;
 
 
@@ -32,6 +35,8 @@ import org.springframework.web.server.WebFilter;
  * @since 5.0
  * @since 5.0
  */
  */
 public class HttpSecurity {
 public class HttpSecurity {
+	private ServerWebExchangeMatcher securityMatcher = ServerWebExchangeMatchers.anyExchange();
+
 	private AuthorizeExchangeBuilder authorizeExchangeBuilder;
 	private AuthorizeExchangeBuilder authorizeExchangeBuilder;
 
 
 	private HeaderBuilder headers = new HeaderBuilder();
 	private HeaderBuilder headers = new HeaderBuilder();
@@ -40,6 +45,26 @@ public class HttpSecurity {
 
 
 	private Optional<SecurityContextRepository> securityContextRepository = Optional.empty();
 	private Optional<SecurityContextRepository> securityContextRepository = Optional.empty();
 
 
+	/**
+	 * The ServerExchangeMatcher that determines which requests apply to this HttpSecurity instance.
+	 *
+	 * @param matcher the ServerExchangeMatcher that determines which requests apply to this HttpSecurity instance.
+	 *                Default is all requests.
+	 */
+	public HttpSecurity securityMatcher(ServerWebExchangeMatcher matcher) {
+		Assert.notNull(matcher, "matcher cannot be null");
+		this.securityMatcher = matcher;
+		return this;
+	}
+
+	/**
+	 * Gets the ServerExchangeMatcher that determines which requests apply to this HttpSecurity instance.
+	 * @return the ServerExchangeMatcher that determines which requests apply to this HttpSecurity instance.
+	 */
+	private ServerWebExchangeMatcher getSecurityMatcher() {
+		return this.securityMatcher;
+	}
+
 	public HttpSecurity securityContextRepository(SecurityContextRepository securityContextRepository) {
 	public HttpSecurity securityContextRepository(SecurityContextRepository securityContextRepository) {
 		Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
 		Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
 		this.securityContextRepository = Optional.of(securityContextRepository);
 		this.securityContextRepository = Optional.of(securityContextRepository);
@@ -69,7 +94,7 @@ public class HttpSecurity {
 		return this;
 		return this;
 	}
 	}
 
 
-	public WebFilterChainFilter build() {
+	public SecurityWebFilterChain build() {
 		List<WebFilter> filters = new ArrayList<>();
 		List<WebFilter> filters = new ArrayList<>();
 		if(headers != null) {
 		if(headers != null) {
 			filters.add(headers.build());
 			filters.add(headers.build());
@@ -84,7 +109,7 @@ public class HttpSecurity {
 			filters.add(new ExceptionTranslationWebFilter());
 			filters.add(new ExceptionTranslationWebFilter());
 			filters.add(authorizeExchangeBuilder.build());
 			filters.add(authorizeExchangeBuilder.build());
 		}
 		}
-		return new WebFilterChainFilter(filters);
+		return new MatcherSecurityWebFilterChain(getSecurityMatcher(), filters);
 	}
 	}
 
 
 	public static HttpSecurity http() {
 	public static HttpSecurity http() {

+ 1 - 1
config/src/test/groovy/org/springframework/security/config/annotation/web/builders/NamespaceHttpTests.groovy

@@ -346,7 +346,7 @@ public class NamespaceHttpTests extends BaseSpringSpec {
 		}
 		}
 	}
 	}
 
 
-	// http@request-matcher is not available (instead request matcher instances are used)
+	// http@request-matcher is not available (instead request securityMatcher instances are used)
 
 
 	def "http@request-matcher-ref ant"() {
 	def "http@request-matcher-ref ant"() {
 		when:
 		when:

+ 98 - 0
config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java

@@ -0,0 +1,98 @@
+/*
+ *
+ *  * Copyright 2002-2017 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.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *      http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+package org.springframework.security.config.annotation.web.reactive;
+
+import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.security.config.web.server.HttpSecurity;
+import org.springframework.security.core.userdetails.MapUserDetailsRepository;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetailsRepository;
+import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.WebFilterChainFilter;
+import org.springframework.security.web.server.util.matcher.PathMatcherServerWebExchangeMatcher;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+import static org.mockito.Mockito.mock;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+@RunWith(Enclosed.class)
+public class EnableWebFluxSecurityTests {
+
+
+	@RunWith(SpringRunner.class)
+	public static class MultiHttpSecurity {
+		@Autowired
+		WebFilterChainFilter springSecurityFilterChain;
+
+		@Test
+		public void multiWorks() {
+			WebTestClient client = WebTestClientBuilder.bindToWebFilters(springSecurityFilterChain).build();
+
+			client.get()
+				.uri("/api/test")
+				.exchange()
+				.expectStatus().isUnauthorized()
+				.expectBody().isEmpty();
+
+			client.get()
+				.uri("/test")
+				.exchange()
+				.expectStatus().isOk();
+		}
+
+		@EnableWebFluxSecurity
+		static class Config {
+			@Order(Ordered.HIGHEST_PRECEDENCE)
+			@Bean
+			public SecurityWebFilterChain apiHttpSecurity(HttpSecurity http) {
+				http
+					.securityMatcher(new PathMatcherServerWebExchangeMatcher("/api/**"))
+					.authorizeExchange()
+						.anyExchange().denyAll();
+				return http.build();
+			}
+
+			@Bean
+			public SecurityWebFilterChain httpSecurity(HttpSecurity http) {
+				return http.build();
+			}
+
+			@Bean
+			public UserDetailsRepository userDetailsRepository() {
+				return new MapUserDetailsRepository(User.withUsername("user")
+					.password("password")
+					.roles("USER")
+					.build()
+				);
+			}
+		}
+	}
+}

+ 17 - 2
config/src/test/java/org/springframework/security/config/web/server/HttpSecurityTests.java

@@ -27,12 +27,12 @@ import org.mockito.runners.MockitoJUnitRunner;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
 import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
+import org.springframework.security.web.server.WebFilterChainFilter;
 import org.springframework.security.web.server.context.SecurityContextRepository;
 import org.springframework.security.web.server.context.SecurityContextRepository;
 import org.springframework.security.web.server.context.WebSessionSecurityContextRepository;
 import org.springframework.security.web.server.context.WebSessionSecurityContextRepository;
 import org.springframework.test.web.reactive.server.EntityExchangeResult;
 import org.springframework.test.web.reactive.server.EntityExchangeResult;
 import org.springframework.test.web.reactive.server.FluxExchangeResult;
 import org.springframework.test.web.reactive.server.FluxExchangeResult;
 import org.springframework.test.web.reactive.server.WebTestClient;
 import org.springframework.test.web.reactive.server.WebTestClient;
-import org.springframework.web.server.WebSession;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
 
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -101,7 +101,22 @@ public class HttpSecurityTests {
 		assertThat(result.getResponseCookies().getFirst("SESSION")).isNotNull();
 		assertThat(result.getResponseCookies().getFirst("SESSION")).isNotNull();
 	}
 	}
 
 
+	@Test
+	public void basicWhenNoCredentialsThenUnauthorized() {
+		http.authorizeExchange().anyExchange().authenticated();
+
+		WebTestClient client = buildClient();
+		client
+			.get()
+			.uri("/")
+			.exchange()
+			.expectStatus().isUnauthorized()
+			.expectHeader().valueMatches(HttpHeaders.CACHE_CONTROL, ".+")
+			.expectBody().isEmpty();
+	}
+
 	private WebTestClient buildClient() {
 	private WebTestClient buildClient() {
-		return WebTestClientBuilder.bindToWebFilters(http.build()).build();
+		WebFilterChainFilter springSecurityFilterChain = WebFilterChainFilter.fromSecurityWebFilterChains(http.build());
+		return WebTestClientBuilder.bindToWebFilters(springSecurityFilterChain).build();
 	}
 	}
 }
 }

+ 2 - 2
samples/javaconfig/hellowebflux/src/main/java/sample/SecurityConfig.java

@@ -26,7 +26,7 @@ import org.springframework.security.config.web.server.HttpSecurity;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetails;
-import org.springframework.security.web.server.WebFilterChainFilter;
+import org.springframework.security.web.server.SecurityWebFilterChain;
 import org.springframework.security.web.server.authorization.AuthorizationContext;
 import org.springframework.security.web.server.authorization.AuthorizationContext;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
 
 
@@ -38,7 +38,7 @@ import reactor.core.publisher.Mono;
 public class SecurityConfig {
 public class SecurityConfig {
 
 
 	@Bean
 	@Bean
-	WebFilterChainFilter springSecurityFilterChain(HttpSecurity http) throws Exception {
+	SecurityWebFilterChain springWebFilterChain(HttpSecurity http) throws Exception {
 		http.authorizeExchange()
 		http.authorizeExchange()
 			.pathMatchers("/admin/**").hasRole("ADMIN")
 			.pathMatchers("/admin/**").hasRole("ADMIN")
 			.pathMatchers("/users/{user}/**").access(this::currentUserMatchesPath)
 			.pathMatchers("/users/{user}/**").access(this::currentUserMatchesPath)

+ 2 - 1
samples/javaconfig/hellowebfluxfn/src/main/java/sample/SecurityConfig.java

@@ -26,6 +26,7 @@ import org.springframework.security.config.web.server.HttpSecurity;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.server.SecurityWebFilterChain;
 import org.springframework.security.web.server.WebFilterChainFilter;
 import org.springframework.security.web.server.WebFilterChainFilter;
 import org.springframework.security.web.server.authorization.AuthorizationContext;
 import org.springframework.security.web.server.authorization.AuthorizationContext;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
@@ -38,7 +39,7 @@ import reactor.core.publisher.Mono;
 public class SecurityConfig {
 public class SecurityConfig {
 
 
 	@Bean
 	@Bean
-	WebFilterChainFilter springSecurityFilterChain(HttpSecurity http) throws Exception {
+	SecurityWebFilterChain httpSecurity(HttpSecurity http) throws Exception {
 		http.authorizeExchange()
 		http.authorizeExchange()
 			.pathMatchers("/admin/**").hasRole("ADMIN")
 			.pathMatchers("/admin/**").hasRole("ADMIN")
 			.pathMatchers("/users/{user}/**").access(this::currentUserMatchesPath)
 			.pathMatchers("/users/{user}/**").access(this::currentUserMatchesPath)

+ 56 - 0
webflux/src/main/java/org/springframework/security/web/server/MatcherSecurityWebFilterChain.java

@@ -0,0 +1,56 @@
+/*
+ *
+ *  * Copyright 2002-2017 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.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *      http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+package org.springframework.security.web.server;
+
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+public class MatcherSecurityWebFilterChain implements SecurityWebFilterChain {
+	private final ServerWebExchangeMatcher matcher;
+	private final Flux<WebFilter> filters;
+
+	public MatcherSecurityWebFilterChain(ServerWebExchangeMatcher matcher, List<WebFilter> filters) {
+		this(matcher, Flux.fromIterable(filters));
+	}
+
+	public MatcherSecurityWebFilterChain(ServerWebExchangeMatcher matcher, Flux<WebFilter> filters) {
+		this.matcher = matcher;
+		this.filters = filters;
+	}
+
+	@Override
+	public Mono<Boolean> matches(ServerWebExchange exchange) {
+		return matcher.matches(exchange)
+			.map( m -> m.isMatch() );
+	}
+
+	@Override
+	public Flux<WebFilter> getWebFilters() {
+		return filters;
+	}
+}

+ 35 - 0
webflux/src/main/java/org/springframework/security/web/server/SecurityWebFilterChain.java

@@ -0,0 +1,35 @@
+/*
+ *
+ *  * Copyright 2002-2017 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.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *      http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+package org.springframework.security.web.server;
+
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+public interface SecurityWebFilterChain {
+
+	Mono<Boolean> matches(ServerWebExchange exchange);
+
+	Flux<WebFilter> getWebFilters();
+}

+ 29 - 5
webflux/src/main/java/org/springframework/security/web/server/WebFilterChainFilter.java

@@ -17,15 +17,21 @@
  */
  */
 package org.springframework.security.web.server;
 package org.springframework.security.web.server;
 
 
+import java.util.Arrays;
 import java.util.Iterator;
 import java.util.Iterator;
 import java.util.List;
 import java.util.List;
+import java.util.function.Function;
 
 
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
 import org.springframework.web.server.ServerWebExchange;
 import org.springframework.web.server.ServerWebExchange;
 import org.springframework.web.server.WebFilter;
 import org.springframework.web.server.WebFilter;
 import org.springframework.web.server.WebFilterChain;
 import org.springframework.web.server.WebFilterChain;
 
 
 import org.springframework.web.server.handler.DefaultWebFilterChain;
 import org.springframework.web.server.handler.DefaultWebFilterChain;
 import org.springframework.web.server.handler.FilteringWebHandler;
 import org.springframework.web.server.handler.FilteringWebHandler;
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
 
 
 /**
 /**
@@ -33,16 +39,34 @@ import reactor.core.publisher.Mono;
  * @since 5.0
  * @since 5.0
  */
  */
 public class WebFilterChainFilter implements WebFilter {
 public class WebFilterChainFilter implements WebFilter {
-	private final List<WebFilter> filters;
+	private final Flux<SecurityWebFilterChain> filters;
 
 
-	public WebFilterChainFilter(List<WebFilter> filters) {
-		super();
+	public WebFilterChainFilter(Flux<SecurityWebFilterChain> filters) {
 		this.filters = filters;
 		this.filters = filters;
 	}
 	}
 
 
 	@Override
 	@Override
 	public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
 	public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
-		DefaultWebFilterChain delegate = new DefaultWebFilterChain(new FilteringWebHandler(e -> chain.filter(e), filters));
-		return delegate.filter(exchange);
+		return filters
+				.filterWhen( securityWebFilterChain -> securityWebFilterChain.matches(exchange))
+				.next()
+				.flatMap( securityWebFilterChain -> securityWebFilterChain.getWebFilters()
+					.collectList()
+				)
+				.map( filters -> new FilteringWebHandler(webHandler -> chain.filter(webHandler), filters))
+				.map( handler -> new DefaultWebFilterChain(handler) )
+				.flatMap( securedChain -> securedChain.filter(exchange));
+	}
+
+	public static WebFilterChainFilter fromWebFiltersList(List<WebFilter> filters) {
+		return new WebFilterChainFilter(Flux.just(new MatcherSecurityWebFilterChain(ServerWebExchangeMatchers.anyExchange(), filters)));
+	}
+
+	public static WebFilterChainFilter fromSecurityWebFilterChainsList(List<SecurityWebFilterChain> securityWebFilterChains) {
+		return new WebFilterChainFilter(Flux.fromIterable(securityWebFilterChains));
+	}
+
+	public static WebFilterChainFilter fromSecurityWebFilterChains(SecurityWebFilterChain... securityWebFilterChains) {
+		return fromSecurityWebFilterChainsList(Arrays.asList(securityWebFilterChains));
 	}
 	}
 }
 }