Explorar o código

Allow configuration of logout through nested builder

Issue: gh-5557
Eleftheria Stein %!s(int64=6) %!d(string=hai) anos
pai
achega
92314b0956

+ 47 - 0
config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java

@@ -773,6 +773,53 @@ public final class HttpSecurity extends
 		return getOrApply(new LogoutConfigurer<>());
 	}
 
+	/**
+	 * Provides logout support. This is automatically applied when using
+	 * {@link WebSecurityConfigurerAdapter}. The default is that accessing the URL
+	 * "/logout" will log the user out by invalidating the HTTP Session, cleaning up any
+	 * {@link #rememberMe()} authentication that was configured, clearing the
+	 * {@link SecurityContextHolder}, and then redirect to "/login?success".
+	 *
+	 * <h2>Example Custom Configuration</h2>
+	 *
+	 * The following customization to log out when the URL "/custom-logout" is invoked.
+	 * Log out will remove the cookie named "remove", not invalidate the HttpSession,
+	 * clear the SecurityContextHolder, and upon completion redirect to "/logout-success".
+	 *
+	 * <pre>
+	 * &#064;Configuration
+	 * &#064;EnableWebSecurity
+	 * public class LogoutSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	&#064;Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests()
+	 * 				.antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;)
+	 * 				.and()
+	 * 			.formLogin()
+	 * 				.and()
+	 * 			// sample logout customization
+	 * 			.logout(logout ->
+	 * 				logout.deleteCookies(&quot;remove&quot;)
+	 * 					.invalidateHttpSession(false)
+	 * 					.logoutUrl(&quot;/custom-logout&quot;)
+	 * 					.logoutSuccessUrl(&quot;/logout-success&quot;)
+	 * 			);
+	 * 	}
+	 * }
+	 * </pre>
+	 *
+	 * @param logoutCustomizer the {@link Customizer} to provide more options for
+	 * the {@link LogoutConfigurer}
+	 * @return the {@link HttpSecurity} for further customizations
+	 * @throws Exception
+	 */
+	public HttpSecurity logout(Customizer<LogoutConfigurer<HttpSecurity>> logoutCustomizer) throws Exception {
+		logoutCustomizer.customize(getOrApply(new LogoutConfigurer<>()));
+		return HttpSecurity.this;
+	}
+
 	/**
 	 * Allows configuring how an anonymous user is represented. This is automatically
 	 * applied when used in conjunction with {@link WebSecurityConfigurerAdapter}. By

+ 88 - 2
config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java

@@ -37,10 +37,15 @@ import org.springframework.test.web.servlet.MockMvc;
 
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
@@ -77,6 +82,26 @@ public class LogoutConfigurerTests {
 		}
 	}
 
+	@Test
+	public void configureWhenDefaultLogoutSuccessHandlerForHasNullLogoutHandlerInLambdaThenException() {
+		assertThatThrownBy(() -> this.spring.register(NullLogoutSuccessHandlerInLambdaConfig.class).autowire())
+				.isInstanceOf(BeanCreationException.class)
+				.hasRootCauseInstanceOf(IllegalArgumentException.class);
+	}
+
+	@EnableWebSecurity
+	static class NullLogoutSuccessHandlerInLambdaConfig extends WebSecurityConfigurerAdapter {
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.logout(logout ->
+					logout.defaultLogoutSuccessHandlerFor(null, mock(RequestMatcher.class))
+				);
+			// @formatter:on
+		}
+	}
+
 	@Test
 	public void configureWhenDefaultLogoutSuccessHandlerForHasNullMatcherThenException() {
 		assertThatThrownBy(() -> this.spring.register(NullMatcherConfig.class).autowire())
@@ -96,6 +121,26 @@ public class LogoutConfigurerTests {
 		}
 	}
 
+	@Test
+	public void configureWhenDefaultLogoutSuccessHandlerForHasNullMatcherInLambdaThenException() {
+		assertThatThrownBy(() -> this.spring.register(NullMatcherInLambdaConfig.class).autowire())
+				.isInstanceOf(BeanCreationException.class)
+				.hasRootCauseInstanceOf(IllegalArgumentException.class);
+	}
+
+	@EnableWebSecurity
+	static class NullMatcherInLambdaConfig extends WebSecurityConfigurerAdapter {
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.logout(logout ->
+					logout.defaultLogoutSuccessHandlerFor(mock(LogoutSuccessHandler.class), null)
+				);
+			// @formatter:on
+		}
+	}
+
 	@Test
 	public void configureWhenRegisteringObjectPostProcessorThenInvokedOnLogoutFilter() {
 		this.spring.register(ObjectPostProcessorConfig.class).autowire();
@@ -263,6 +308,29 @@ public class LogoutConfigurerTests {
 		}
 	}
 
+	@Test
+	public void logoutWhenCustomLogoutUrlInLambdaThenRedirectsToLogin() throws Exception {
+		this.spring.register(CsrfDisabledAndCustomLogoutInLambdaConfig.class).autowire();
+
+		this.mvc.perform(get("/custom/logout"))
+				.andExpect(status().isFound())
+				.andExpect(redirectedUrl("/login?logout"));
+	}
+
+	@EnableWebSecurity
+	static class CsrfDisabledAndCustomLogoutInLambdaConfig extends WebSecurityConfigurerAdapter {
+
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.csrf()
+					.disable()
+				.logout(logout -> logout.logoutUrl("/custom/logout"));
+			// @formatter:on
+		}
+	}
+
 	// SEC-3170
 	@Test
 	public void configureWhenLogoutHandlerNullThenException() {
@@ -283,6 +351,24 @@ public class LogoutConfigurerTests {
 		}
 	}
 
+	@Test
+	public void configureWhenLogoutHandlerNullInLambdaThenException() {
+		assertThatThrownBy(() -> this.spring.register(NullLogoutHandlerInLambdaConfig.class).autowire())
+				.isInstanceOf(BeanCreationException.class)
+				.hasRootCauseInstanceOf(IllegalArgumentException.class);
+	}
+
+	@EnableWebSecurity
+	static class NullLogoutHandlerInLambdaConfig extends WebSecurityConfigurerAdapter {
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.logout(logout -> logout.addLogoutHandler(null));
+			// @formatter:on
+		}
+	}
+
 	// SEC-3170
 	@Test
 	public void rememberMeWhenRememberMeServicesNotLogoutHandlerThenRedirectsToLogin() throws Exception {

+ 74 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.java

@@ -41,9 +41,11 @@ import org.springframework.test.web.servlet.ResultMatcher;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
 /**
  * Tests to verify that all the functionality of <logout> attributes is present
@@ -83,6 +85,23 @@ public class NamespaceHttpLogoutTests {
 		}
 	}
 
+	@Test
+	@WithMockUser
+	public void logoutWhenDisabledInLambdaThenRespondsWithNotFound() throws Exception {
+		this.spring.register(HttpLogoutDisabledInLambdaConfig.class).autowire();
+
+		this.mvc.perform(post("/logout").with(csrf()).with(user("user")))
+				.andExpect(status().isNotFound());
+	}
+
+	@EnableWebSecurity
+	static class HttpLogoutDisabledInLambdaConfig extends WebSecurityConfigurerAdapter {
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			http.logout(AbstractHttpConfigurer::disable);
+		}
+	}
+
 	/**
 	 * http/logout custom
 	 */
@@ -112,6 +131,35 @@ public class NamespaceHttpLogoutTests {
 		}
 	}
 
+	@Test
+	@WithMockUser
+	public void logoutWhenUsingVariousCustomizationsInLambdaThenMatchesNamespace() throws Exception {
+		this.spring.register(CustomHttpLogoutInLambdaConfig.class).autowire();
+
+		this.mvc.perform(post("/custom-logout").with(csrf()))
+				.andExpect(authenticated(false))
+				.andExpect(redirectedUrl("/logout-success"))
+				.andExpect(result -> assertThat(result.getResponse().getCookies()).hasSize(1))
+				.andExpect(cookie().maxAge("remove", 0))
+				.andExpect(session(Objects::nonNull));
+	}
+
+	@EnableWebSecurity
+	static class CustomHttpLogoutInLambdaConfig extends WebSecurityConfigurerAdapter {
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.logout(logout ->
+						logout.deleteCookies("remove")
+							.invalidateHttpSession(false)
+							.logoutUrl("/custom-logout")
+							.logoutSuccessUrl("/logout-success")
+				);
+			// @formatter:on
+		}
+	}
+
 	/**
 	 * http/logout@success-handler-ref
 	 */
@@ -141,6 +189,32 @@ public class NamespaceHttpLogoutTests {
 		}
 	}
 
+	@Test
+	@WithMockUser
+	public void logoutWhenUsingSuccessHandlerRefInLambdaThenMatchesNamespace() throws Exception {
+		this.spring.register(SuccessHandlerRefHttpLogoutInLambdaConfig.class).autowire();
+
+		this.mvc.perform(post("/logout").with(csrf()))
+				.andExpect(authenticated(false))
+				.andExpect(redirectedUrl("/SuccessHandlerRefHttpLogoutConfig"))
+				.andExpect(noCookies())
+				.andExpect(session(Objects::isNull));
+	}
+
+	@EnableWebSecurity
+	static class SuccessHandlerRefHttpLogoutInLambdaConfig extends WebSecurityConfigurerAdapter {
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			SimpleUrlLogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
+			logoutSuccessHandler.setDefaultTargetUrl("/SuccessHandlerRefHttpLogoutConfig");
+
+			// @formatter:off
+			http
+				.logout(logout -> logout.logoutSuccessHandler(logoutSuccessHandler));
+			// @formatter:on
+		}
+	}
+
 	ResultMatcher authenticated(boolean authenticated) {
 		return result -> assertThat(
 				Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())