mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-07-07 19:22:14 +00:00
Add Reactive LogoutBuilder
Fixes gh-4541
This commit is contained in:
parent
c77cc72cd3
commit
79e749790f
@ -15,64 +15,45 @@
|
|||||||
*/
|
*/
|
||||||
package org.springframework.security.config.web.server;
|
package org.springframework.security.config.web.server;
|
||||||
|
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||||
|
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
|
||||||
|
import org.springframework.security.authorization.AuthorityAuthorizationManager;
|
||||||
|
import org.springframework.security.authorization.AuthorizationDecision;
|
||||||
|
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||||
|
import org.springframework.security.web.server.*;
|
||||||
|
import org.springframework.security.web.server.authentication.*;
|
||||||
|
import org.springframework.security.web.server.authentication.logout.LogoutHandler;
|
||||||
|
import org.springframework.security.web.server.authentication.logout.LogoutWebFilter;
|
||||||
|
import org.springframework.security.web.server.authentication.logout.SecurityContextRepositoryLogoutHandler;
|
||||||
|
import org.springframework.security.web.server.authentication.www.HttpBasicAuthenticationEntryPoint;
|
||||||
|
import org.springframework.security.web.server.authorization.AuthorizationContext;
|
||||||
|
import org.springframework.security.web.server.authorization.AuthorizationWebFilter;
|
||||||
|
import org.springframework.security.web.server.authorization.DelegatingReactiveAuthorizationManager;
|
||||||
|
import org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter;
|
||||||
|
import org.springframework.security.web.server.context.*;
|
||||||
|
import org.springframework.security.web.server.header.*;
|
||||||
|
import org.springframework.security.web.server.ui.LoginPageGeneratingWebFilter;
|
||||||
|
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
|
||||||
|
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.util.Assert;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import org.springframework.web.server.WebFilter;
|
||||||
|
import org.springframework.web.server.WebFilterChain;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.core.Ordered;
|
import static org.springframework.security.web.server.DelegatingAuthenticationEntryPoint.DelegateEntry;
|
||||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.security.web.server.DelegatingAuthenticationEntryPoint;
|
|
||||||
import org.springframework.security.web.server.authentication.AuthenticationFailureHandler;
|
|
||||||
import org.springframework.security.web.server.authentication.logout.LogoutWebFiter;
|
|
||||||
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
|
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
|
||||||
import org.springframework.web.server.WebFilterChain;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
|
||||||
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
|
|
||||||
import org.springframework.security.authorization.AuthorityAuthorizationManager;
|
|
||||||
import org.springframework.security.authorization.AuthorizationDecision;
|
|
||||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
|
||||||
import org.springframework.security.web.server.AuthenticationEntryPoint;
|
|
||||||
import org.springframework.security.web.server.FormLoginAuthenticationConverter;
|
|
||||||
import org.springframework.security.web.server.HttpBasicAuthenticationConverter;
|
|
||||||
import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
|
|
||||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
|
||||||
import org.springframework.security.web.server.authentication.AuthenticationEntryPointFailureHandler;
|
|
||||||
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
|
|
||||||
import org.springframework.security.web.server.authentication.RedirectAuthenticationEntryPoint;
|
|
||||||
import org.springframework.security.web.server.authentication.RedirectAuthenticationSuccessHandler;
|
|
||||||
import org.springframework.security.web.server.authentication.www.HttpBasicAuthenticationEntryPoint;
|
|
||||||
import org.springframework.security.web.server.authorization.AuthorizationContext;
|
|
||||||
import org.springframework.security.web.server.authorization.AuthorizationWebFilter;
|
|
||||||
import org.springframework.security.web.server.authorization.DelegatingReactiveAuthorizationManager;
|
|
||||||
import org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter;
|
|
||||||
import org.springframework.security.web.server.context.AuthenticationReactorContextFilter;
|
|
||||||
import org.springframework.security.web.server.context.SecurityContextRepository;
|
|
||||||
import org.springframework.security.web.server.context.SecurityContextRepositoryWebFilter;
|
|
||||||
import org.springframework.security.web.server.context.ServerWebExchangeAttributeSecurityContextRepository;
|
|
||||||
import org.springframework.security.web.server.context.WebSessionSecurityContextRepository;
|
|
||||||
import org.springframework.security.web.server.header.CacheControlHttpHeadersWriter;
|
|
||||||
import org.springframework.security.web.server.header.CompositeHttpHeadersWriter;
|
|
||||||
import org.springframework.security.web.server.header.ContentTypeOptionsHttpHeadersWriter;
|
|
||||||
import org.springframework.security.web.server.header.HttpHeaderWriterWebFilter;
|
|
||||||
import org.springframework.security.web.server.header.HttpHeadersWriter;
|
|
||||||
import org.springframework.security.web.server.header.StrictTransportSecurityHttpHeadersWriter;
|
|
||||||
import org.springframework.security.web.server.header.XFrameOptionsHttpHeadersWriter;
|
|
||||||
import org.springframework.security.web.server.header.XXssProtectionHttpHeadersWriter;
|
|
||||||
import org.springframework.security.web.server.ui.LoginPageGeneratingWebFilter;
|
|
||||||
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.util.Assert;
|
|
||||||
import org.springframework.web.server.WebFilter;
|
|
||||||
|
|
||||||
import static org.springframework.security.web.server.DelegatingAuthenticationEntryPoint.*;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Rob Winch
|
* @author Rob Winch
|
||||||
@ -89,6 +70,8 @@ public class HttpSecurity {
|
|||||||
|
|
||||||
private FormLoginBuilder formLogin;
|
private FormLoginBuilder formLogin;
|
||||||
|
|
||||||
|
private LogoutBuilder logout;
|
||||||
|
|
||||||
private ReactiveAuthenticationManager authenticationManager;
|
private ReactiveAuthenticationManager authenticationManager;
|
||||||
|
|
||||||
private SecurityContextRepository securityContextRepository;
|
private SecurityContextRepository securityContextRepository;
|
||||||
@ -158,6 +141,13 @@ public class HttpSecurity {
|
|||||||
return this.authorizeExchangeBuilder;
|
return this.authorizeExchangeBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LogoutBuilder logout() {
|
||||||
|
if (this.logout == null) {
|
||||||
|
this.logout = new LogoutBuilder();
|
||||||
|
}
|
||||||
|
return this.logout;
|
||||||
|
}
|
||||||
|
|
||||||
public HttpSecurity authenticationManager(ReactiveAuthenticationManager manager) {
|
public HttpSecurity authenticationManager(ReactiveAuthenticationManager manager) {
|
||||||
this.authenticationManager = manager;
|
this.authenticationManager = manager;
|
||||||
return this;
|
return this;
|
||||||
@ -187,7 +177,10 @@ public class HttpSecurity {
|
|||||||
this.webFilters.add(new OrderedWebFilter(new LoginPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING.getOrder()));
|
this.webFilters.add(new OrderedWebFilter(new LoginPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING.getOrder()));
|
||||||
}
|
}
|
||||||
this.formLogin.configure(this);
|
this.formLogin.configure(this);
|
||||||
this.addFilterAt(new LogoutWebFiter(), SecurityWebFiltersOrder.LOGOUT);
|
this.addFilterAt(new LogoutWebFilter(), SecurityWebFiltersOrder.LOGOUT);
|
||||||
|
}
|
||||||
|
if(this.logout != null) {
|
||||||
|
this.logout.configure(this);
|
||||||
}
|
}
|
||||||
this.addFilterAt(new AuthenticationReactorContextFilter(), SecurityWebFiltersOrder.AUTHENTICATION_CONTEXT);
|
this.addFilterAt(new AuthenticationReactorContextFilter(), SecurityWebFiltersOrder.AUTHENTICATION_CONTEXT);
|
||||||
if(this.authorizeExchangeBuilder != null) {
|
if(this.authorizeExchangeBuilder != null) {
|
||||||
@ -536,6 +529,48 @@ public class HttpSecurity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Shazin Sadakath
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public final class LogoutBuilder {
|
||||||
|
|
||||||
|
private LogoutHandler logoutHandler = new SecurityContextRepositoryLogoutHandler();
|
||||||
|
private String logoutUrl = "/logout";
|
||||||
|
private ServerWebExchangeMatcher requiresLogout = ServerWebExchangeMatchers
|
||||||
|
.pathMatchers(logoutUrl);
|
||||||
|
|
||||||
|
public LogoutBuilder logoutHandler(LogoutHandler logoutHandler) {
|
||||||
|
Assert.notNull(logoutHandler, "logoutHandler must not be null");
|
||||||
|
this.logoutHandler = logoutHandler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LogoutBuilder logoutUrl(String logoutUrl) {
|
||||||
|
Assert.notNull(logoutHandler, "logoutUrl must not be null");
|
||||||
|
this.logoutUrl = logoutUrl;
|
||||||
|
this.requiresLogout = ServerWebExchangeMatchers.pathMatchers(logoutUrl);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpSecurity and() {
|
||||||
|
return HttpSecurity.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void configure(HttpSecurity http) {
|
||||||
|
LogoutWebFilter logoutWebFilter = createLogoutWebFilter(http);
|
||||||
|
http.addFilterAt(logoutWebFilter, SecurityWebFiltersOrder.LOGOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LogoutWebFilter createLogoutWebFilter(HttpSecurity http) {
|
||||||
|
LogoutWebFilter logoutWebFilter = new LogoutWebFilter();
|
||||||
|
logoutWebFilter.setLogoutHandler(this.logoutHandler);
|
||||||
|
logoutWebFilter.setRequiresLogout(this.requiresLogout);
|
||||||
|
|
||||||
|
return logoutWebFilter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class OrderedWebFilter implements WebFilter, Ordered {
|
private static class OrderedWebFilter implements WebFilter, Ordered {
|
||||||
private final WebFilter webFilter;
|
private final WebFilter webFilter;
|
||||||
private final int order;
|
private final int order;
|
||||||
|
@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* 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.web.server;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.openqa.selenium.WebDriver;
|
||||||
|
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.UserDetailsRepositoryAuthenticationManager;
|
||||||
|
import org.springframework.security.core.userdetails.MapUserDetailsRepository;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder;
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Shazin Sadakath
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public class LogoutBuilderTests {
|
||||||
|
|
||||||
|
private UserDetails user = User.withUsername("user").password("password").roles("USER").build();
|
||||||
|
private HttpSecurity http = HttpSecurity.http();
|
||||||
|
|
||||||
|
ReactiveAuthenticationManager manager = new UserDetailsRepositoryAuthenticationManager(new MapUserDetailsRepository(this.user));
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultLogout() {
|
||||||
|
SecurityWebFilterChain securityWebFilter = this.http
|
||||||
|
.authenticationManager(this.manager)
|
||||||
|
.authorizeExchange()
|
||||||
|
.anyExchange().authenticated()
|
||||||
|
.and()
|
||||||
|
.formLogin().and()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
WebTestClient webTestClient = WebTestClientBuilder
|
||||||
|
.bindToWebFilters(securityWebFilter)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
|
||||||
|
.webTestClientSetup(webTestClient)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
FormLoginTests.DefaultLoginPage loginPage = FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class)
|
||||||
|
.assertAt();
|
||||||
|
|
||||||
|
loginPage = loginPage.loginForm()
|
||||||
|
.username("user")
|
||||||
|
.password("invalid")
|
||||||
|
.submit(FormLoginTests.DefaultLoginPage.class)
|
||||||
|
.assertError();
|
||||||
|
|
||||||
|
FormLoginTests.HomePage homePage = loginPage.loginForm()
|
||||||
|
.username("user")
|
||||||
|
.password("password")
|
||||||
|
.submit(FormLoginTests.HomePage.class);
|
||||||
|
|
||||||
|
homePage.assertAt();
|
||||||
|
|
||||||
|
driver.get("http://localhost/logout");
|
||||||
|
|
||||||
|
FormLoginTests.DefaultLoginPage.create(driver)
|
||||||
|
.assertAt()
|
||||||
|
.assertLogout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void customLogout() {
|
||||||
|
SecurityWebFilterChain securityWebFilter = this.http
|
||||||
|
.authenticationManager(this.manager)
|
||||||
|
.authorizeExchange()
|
||||||
|
.anyExchange().authenticated()
|
||||||
|
.and()
|
||||||
|
.formLogin().and()
|
||||||
|
.logout().logoutUrl("/custom-logout").and()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
WebTestClient webTestClient = WebTestClientBuilder
|
||||||
|
.bindToWebFilters(securityWebFilter)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
|
||||||
|
.webTestClientSetup(webTestClient)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
FormLoginTests.DefaultLoginPage loginPage = FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class)
|
||||||
|
.assertAt();
|
||||||
|
|
||||||
|
loginPage = loginPage.loginForm()
|
||||||
|
.username("user")
|
||||||
|
.password("invalid")
|
||||||
|
.submit(FormLoginTests.DefaultLoginPage.class)
|
||||||
|
.assertError();
|
||||||
|
|
||||||
|
FormLoginTests.HomePage homePage = loginPage.loginForm()
|
||||||
|
.username("user")
|
||||||
|
.password("password")
|
||||||
|
.submit(FormLoginTests.HomePage.class);
|
||||||
|
|
||||||
|
homePage.assertAt();
|
||||||
|
|
||||||
|
driver.get("http://localhost/custom-logout");
|
||||||
|
|
||||||
|
FormLoginTests.DefaultLoginPage.create(driver)
|
||||||
|
.assertAt()
|
||||||
|
.assertLogout();
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package org.springframework.security.web.server.authentication.logout;
|
package org.springframework.security.web.server.authentication.logout;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||||
@ -32,7 +33,7 @@ import org.springframework.web.server.WebFilterChain;
|
|||||||
* @author Rob Winch
|
* @author Rob Winch
|
||||||
* @since 5.0
|
* @since 5.0
|
||||||
*/
|
*/
|
||||||
public class LogoutWebFiter implements WebFilter {
|
public class LogoutWebFilter implements WebFilter {
|
||||||
private AnonymousAuthenticationToken anonymousAuthenticationToken = new AnonymousAuthenticationToken("key", "anonymous",
|
private AnonymousAuthenticationToken anonymousAuthenticationToken = new AnonymousAuthenticationToken("key", "anonymous",
|
||||||
AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
|
AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
|
||||||
private LogoutHandler logoutHandler = new SecurityContextRepositoryLogoutHandler();
|
private LogoutHandler logoutHandler = new SecurityContextRepositoryLogoutHandler();
|
||||||
@ -54,4 +55,14 @@ public class LogoutWebFiter implements WebFilter {
|
|||||||
.cast(Authentication.class)
|
.cast(Authentication.class)
|
||||||
.defaultIfEmpty(this.anonymousAuthenticationToken);
|
.defaultIfEmpty(this.anonymousAuthenticationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final void setLogoutHandler(LogoutHandler logoutHandler) {
|
||||||
|
Assert.notNull(logoutHandler, "logoutHandler must not be null");
|
||||||
|
this.logoutHandler = logoutHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void setRequiresLogout(ServerWebExchangeMatcher serverWebExchangeMatcher) {
|
||||||
|
Assert.notNull(serverWebExchangeMatcher, "serverWebExchangeMatcher must not be null");
|
||||||
|
this.requiresLogout = serverWebExchangeMatcher;
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user