瀏覽代碼

Add Reactive LogoutBuilder

Fixes gh-4541
shazin.sadakath@gmail.com 8 年之前
父節點
當前提交
79e749790f

+ 74 - 39
config/src/main/java/org/springframework/security/config/web/server/HttpSecurity.java

@@ -15,64 +15,45 @@
  */
 package org.springframework.security.config.web.server;
 
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
 import org.springframework.core.Ordered;
 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.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.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.*;
+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.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.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.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 
-import static org.springframework.security.web.server.DelegatingAuthenticationEntryPoint.*;
+import static org.springframework.security.web.server.DelegatingAuthenticationEntryPoint.DelegateEntry;
 
 /**
  * @author Rob Winch
@@ -89,6 +70,8 @@ public class HttpSecurity {
 
 	private FormLoginBuilder formLogin;
 
+	private LogoutBuilder logout;
+
 	private ReactiveAuthenticationManager authenticationManager;
 
 	private SecurityContextRepository securityContextRepository;
@@ -158,6 +141,13 @@ public class HttpSecurity {
 		return this.authorizeExchangeBuilder;
 	}
 
+	public LogoutBuilder logout() {
+		if (this.logout == null) {
+			this.logout = new LogoutBuilder();
+		}
+		return this.logout;
+	}
+
 	public HttpSecurity authenticationManager(ReactiveAuthenticationManager manager) {
 		this.authenticationManager = manager;
 		return this;
@@ -187,7 +177,10 @@ public class HttpSecurity {
 				this.webFilters.add(new OrderedWebFilter(new LoginPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING.getOrder()));
 			}
 			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);
 		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 final WebFilter webFilter;
 		private final int order;

+ 124 - 0
config/src/test/java/org/springframework/security/config/web/server/LogoutBuilderTests.java

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

+ 12 - 1
webflux/src/main/java/org/springframework/security/web/server/authentication/logout/LogoutWebFiter.java → webflux/src/main/java/org/springframework/security/web/server/authentication/logout/LogoutWebFilter.java

@@ -16,6 +16,7 @@
 
 package org.springframework.security.web.server.authentication.logout;
 
+import org.springframework.util.Assert;
 import reactor.core.publisher.Mono;
 
 import org.springframework.security.authentication.AnonymousAuthenticationToken;
@@ -32,7 +33,7 @@ import org.springframework.web.server.WebFilterChain;
  * @author Rob Winch
  * @since 5.0
  */
-public class LogoutWebFiter implements WebFilter {
+public class LogoutWebFilter implements WebFilter {
 	private AnonymousAuthenticationToken anonymousAuthenticationToken = new AnonymousAuthenticationToken("key", "anonymous",
 		AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
 	private LogoutHandler logoutHandler = new SecurityContextRepositoryLogoutHandler();
@@ -54,4 +55,14 @@ public class LogoutWebFiter implements WebFilter {
 			.cast(Authentication.class)
 			.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;
+	}
 }