diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java index e61766bbe7..0cfdf4f8d6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java +++ b/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; - +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; 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 @@ -28,4 +33,11 @@ import org.springframework.context.annotation.Configuration; @Configuration public class WebFluxSecurityConfiguration { + @Autowired(required = false) + private List securityWebFilterChains; + + @Bean + public WebFilterChainFilter springSecurityFilterChain() { + return WebFilterChainFilter.fromSecurityWebFilterChainsList(securityWebFilterChains); + } } diff --git a/config/src/main/java/org/springframework/security/config/web/server/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/HttpSecurity.java index 7e774bfe37..a03bea9fbc 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/HttpSecurity.java +++ b/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 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.WebFilterChainFilter; import org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter; 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.web.server.WebFilter; @@ -32,6 +35,8 @@ import org.springframework.web.server.WebFilter; * @since 5.0 */ public class HttpSecurity { + private ServerWebExchangeMatcher securityMatcher = ServerWebExchangeMatchers.anyExchange(); + private AuthorizeExchangeBuilder authorizeExchangeBuilder; private HeaderBuilder headers = new HeaderBuilder(); @@ -40,6 +45,26 @@ public class HttpSecurity { private Optional 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) { Assert.notNull(securityContextRepository, "securityContextRepository cannot be null"); this.securityContextRepository = Optional.of(securityContextRepository); @@ -69,7 +94,7 @@ public class HttpSecurity { return this; } - public WebFilterChainFilter build() { + public SecurityWebFilterChain build() { List filters = new ArrayList<>(); if(headers != null) { filters.add(headers.build()); @@ -84,7 +109,7 @@ public class HttpSecurity { filters.add(new ExceptionTranslationWebFilter()); filters.add(authorizeExchangeBuilder.build()); } - return new WebFilterChainFilter(filters); + return new MatcherSecurityWebFilterChain(getSecurityMatcher(), filters); } public static HttpSecurity http() { diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/builders/NamespaceHttpTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/builders/NamespaceHttpTests.groovy index 780ccb3701..4c1902b4fe 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/builders/NamespaceHttpTests.groovy +++ b/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"() { when: diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java new file mode 100644 index 0000000000..0159db1601 --- /dev/null +++ b/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() + ); + } + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/web/server/HttpSecurityTests.java b/config/src/test/java/org/springframework/security/config/web/server/HttpSecurityTests.java index 03ef0b4e00..4126c4daf0 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/HttpSecurityTests.java +++ b/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.TestingAuthenticationToken; 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.WebSessionSecurityContextRepository; import org.springframework.test.web.reactive.server.EntityExchangeResult; import org.springframework.test.web.reactive.server.FluxExchangeResult; import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.server.WebSession; import reactor.core.publisher.Mono; import static org.assertj.core.api.Assertions.assertThat; @@ -101,7 +101,22 @@ public class HttpSecurityTests { 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() { - return WebTestClientBuilder.bindToWebFilters(http.build()).build(); + WebFilterChainFilter springSecurityFilterChain = WebFilterChainFilter.fromSecurityWebFilterChains(http.build()); + return WebTestClientBuilder.bindToWebFilters(springSecurityFilterChain).build(); } } diff --git a/samples/javaconfig/hellowebflux/src/main/java/sample/SecurityConfig.java b/samples/javaconfig/hellowebflux/src/main/java/sample/SecurityConfig.java index 352e0d5ff4..1dca016f55 100644 --- a/samples/javaconfig/hellowebflux/src/main/java/sample/SecurityConfig.java +++ b/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.userdetails.User; 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 reactor.core.publisher.Mono; @@ -38,7 +38,7 @@ import reactor.core.publisher.Mono; public class SecurityConfig { @Bean - WebFilterChainFilter springSecurityFilterChain(HttpSecurity http) throws Exception { + SecurityWebFilterChain springWebFilterChain(HttpSecurity http) throws Exception { http.authorizeExchange() .pathMatchers("/admin/**").hasRole("ADMIN") .pathMatchers("/users/{user}/**").access(this::currentUserMatchesPath) diff --git a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/SecurityConfig.java b/samples/javaconfig/hellowebfluxfn/src/main/java/sample/SecurityConfig.java index 352e0d5ff4..e3bd584e11 100644 --- a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/SecurityConfig.java +++ b/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.userdetails.User; 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.authorization.AuthorizationContext; import reactor.core.publisher.Mono; @@ -38,7 +39,7 @@ import reactor.core.publisher.Mono; public class SecurityConfig { @Bean - WebFilterChainFilter springSecurityFilterChain(HttpSecurity http) throws Exception { + SecurityWebFilterChain httpSecurity(HttpSecurity http) throws Exception { http.authorizeExchange() .pathMatchers("/admin/**").hasRole("ADMIN") .pathMatchers("/users/{user}/**").access(this::currentUserMatchesPath) diff --git a/webflux/src/main/java/org/springframework/security/web/server/MatcherSecurityWebFilterChain.java b/webflux/src/main/java/org/springframework/security/web/server/MatcherSecurityWebFilterChain.java new file mode 100644 index 0000000000..97040c7b39 --- /dev/null +++ b/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 filters; + + public MatcherSecurityWebFilterChain(ServerWebExchangeMatcher matcher, List filters) { + this(matcher, Flux.fromIterable(filters)); + } + + public MatcherSecurityWebFilterChain(ServerWebExchangeMatcher matcher, Flux filters) { + this.matcher = matcher; + this.filters = filters; + } + + @Override + public Mono matches(ServerWebExchange exchange) { + return matcher.matches(exchange) + .map( m -> m.isMatch() ); + } + + @Override + public Flux getWebFilters() { + return filters; + } +} diff --git a/webflux/src/main/java/org/springframework/security/web/server/SecurityWebFilterChain.java b/webflux/src/main/java/org/springframework/security/web/server/SecurityWebFilterChain.java new file mode 100644 index 0000000000..6c22c53b8a --- /dev/null +++ b/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 matches(ServerWebExchange exchange); + + Flux getWebFilters(); +} diff --git a/webflux/src/main/java/org/springframework/security/web/server/WebFilterChainFilter.java b/webflux/src/main/java/org/springframework/security/web/server/WebFilterChainFilter.java index c90974941f..502b1f2c72 100644 --- a/webflux/src/main/java/org/springframework/security/web/server/WebFilterChainFilter.java +++ b/webflux/src/main/java/org/springframework/security/web/server/WebFilterChainFilter.java @@ -17,15 +17,21 @@ */ package org.springframework.security.web.server; +import java.util.Arrays; import java.util.Iterator; 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.WebFilter; import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.handler.DefaultWebFilterChain; import org.springframework.web.server.handler.FilteringWebHandler; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** @@ -33,16 +39,34 @@ import reactor.core.publisher.Mono; * @since 5.0 */ public class WebFilterChainFilter implements WebFilter { - private final List filters; + private final Flux filters; - public WebFilterChainFilter(List filters) { - super(); + public WebFilterChainFilter(Flux filters) { this.filters = filters; } @Override public Mono 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 filters) { + return new WebFilterChainFilter(Flux.just(new MatcherSecurityWebFilterChain(ServerWebExchangeMatchers.anyExchange(), filters))); + } + + public static WebFilterChainFilter fromSecurityWebFilterChainsList(List securityWebFilterChains) { + return new WebFilterChainFilter(Flux.fromIterable(securityWebFilterChains)); + } + + public static WebFilterChainFilter fromSecurityWebFilterChains(SecurityWebFilterChain... securityWebFilterChains) { + return fromSecurityWebFilterChainsList(Arrays.asList(securityWebFilterChains)); } }