mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-03-09 06:50:05 +00:00
Add Multiple Reactive HttpSecurity
Fixes gh-4395
This commit is contained in:
parent
406e1e6951
commit
9141a8a7c0
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user