Add Multiple Reactive HttpSecurity

Fixes gh-4395
This commit is contained in:
Rob Winch 2017-06-14 14:05:52 -05:00
parent 406e1e6951
commit 9141a8a7c0
10 changed files with 281 additions and 15 deletions

View File

@ -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<SecurityWebFilterChain> securityWebFilterChains;
@Bean
public WebFilterChainFilter springSecurityFilterChain() {
return WebFilterChainFilter.fromSecurityWebFilterChainsList(securityWebFilterChains);
}
}

View File

@ -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> 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<WebFilter> 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() {

View File

@ -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:

View File

@ -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()
);
}
}
}
}

View File

@ -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();
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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<WebFilter> filters;
private final Flux<SecurityWebFilterChain> filters;
public WebFilterChainFilter(List<WebFilter> filters) {
super();
public WebFilterChainFilter(Flux<SecurityWebFilterChain> filters) {
this.filters = filters;
}
@Override
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));
}
}