فهرست منبع

Merge branch '5.8.x' into 6.0.x

Marcus Da Coregio 2 سال پیش
والد
کامیت
a484044591

+ 12 - 1
config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -35,6 +35,8 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl
 import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
 import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
 import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
+import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
+import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.OrRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -326,6 +328,7 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
 	 */
 	private LogoutFilter createLogoutFilter(H http) {
 		this.contextLogoutHandler.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
+		this.contextLogoutHandler.setSecurityContextRepository(getSecurityContextRepository(http));
 		this.logoutHandlers.add(this.contextLogoutHandler);
 		this.logoutHandlers.add(postProcess(new LogoutSuccessEventPublishingLogoutHandler()));
 		LogoutHandler[] handlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
@@ -337,6 +340,14 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
 		return result;
 	}
 
+	private SecurityContextRepository getSecurityContextRepository(H http) {
+		SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
+		if (securityContextRepository == null) {
+			securityContextRepository = new HttpSessionSecurityContextRepository();
+		}
+		return securityContextRepository;
+	}
+
 	private RequestMatcher getLogoutRequestMatcher(H http) {
 		if (this.logoutRequestMatcher != null) {
 			return this.logoutRequestMatcher;

+ 82 - 1
config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -16,6 +16,8 @@
 
 package org.springframework.security.config.annotation.web.configurers;
 
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
 import org.apache.http.HttpHeaders;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -25,6 +27,8 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockHttpSession;
+import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
 import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -39,6 +43,8 @@ import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.authentication.RememberMeServices;
 import org.springframework.security.web.authentication.logout.LogoutFilter;
 import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
+import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
@@ -48,6 +54,7 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 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;
@@ -324,6 +331,80 @@ public class LogoutConfigurerTests {
 		this.mvc.perform(post("/logout").with(csrf())).andExpect(status().isNotFound());
 	}
 
+	@Test
+	public void logoutWhenCustomSecurityContextRepositoryThenUses() throws Exception {
+		CustomSecurityContextRepositoryConfig.repository = mock(SecurityContextRepository.class);
+		this.spring.register(CustomSecurityContextRepositoryConfig.class).autowire();
+		// @formatter:off
+		MockHttpServletRequestBuilder logoutRequest = post("/logout")
+				.with(csrf())
+				.with(user("user"))
+				.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE);
+		this.mvc.perform(logoutRequest)
+				.andExpect(status().isFound())
+				.andExpect(redirectedUrl("/login?logout"));
+		// @formatter:on
+		int invocationCount = 2; // 1 from user() post processor and 1 from
+									// SecurityContextLogoutHandler
+		verify(CustomSecurityContextRepositoryConfig.repository, times(invocationCount)).saveContext(any(),
+				any(HttpServletRequest.class), any(HttpServletResponse.class));
+	}
+
+	@Test
+	public void logoutWhenNoSecurityContextRepositoryThenHttpSessionSecurityContextRepository() throws Exception {
+		this.spring.register(InvalidateHttpSessionFalseConfig.class).autowire();
+		MockHttpSession session = mock(MockHttpSession.class);
+		// @formatter:off
+		MockHttpServletRequestBuilder logoutRequest = post("/logout")
+				.with(csrf())
+				.with(user("user"))
+				.session(session)
+				.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE);
+		this.mvc.perform(logoutRequest)
+				.andExpect(status().isFound())
+				.andExpect(redirectedUrl("/login?logout"))
+				.andReturn();
+		// @formatter:on
+		verify(session).removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
+	}
+
+	@Configuration
+	@EnableWebSecurity
+	static class InvalidateHttpSessionFalseConfig {
+
+		@Bean
+		SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.logout((logout) -> logout.invalidateHttpSession(false))
+				.securityContext((context) -> context.requireExplicitSave(true));
+			return http.build();
+			// @formatter:on
+		}
+
+	}
+
+	@Configuration
+	@EnableWebSecurity
+	static class CustomSecurityContextRepositoryConfig {
+
+		static SecurityContextRepository repository;
+
+		@Bean
+		SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.logout(Customizer.withDefaults())
+				.securityContext((context) -> context
+					.requireExplicitSave(true)
+					.securityContextRepository(repository)
+				);
+			return http.build();
+			// @formatter:on
+		}
+
+	}
+
 	@Configuration
 	@EnableWebSecurity
 	static class NullLogoutSuccessHandlerConfig {

+ 1 - 0
docs/modules/ROOT/pages/servlet/authentication/logout.adoc

@@ -12,6 +12,7 @@ The default is that accessing the URL `/logout` logs the user out by:
 - Invalidating the HTTP Session
 - Cleaning up any RememberMe authentication that was configured
 - Clearing the `SecurityContextHolder`
+- Clearing the `SecurityContextRepository`
 - Redirecting to `/login?logout`
 
 Similar to configuring login capabilities, however, you also have various options to further customize your logout requirements:

+ 16 - 0
web/src/main/java/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.java

@@ -27,6 +27,8 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextHolderStrategy;
+import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
+import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.util.Assert;
 
 /**
@@ -53,6 +55,8 @@ public class SecurityContextLogoutHandler implements LogoutHandler {
 
 	private boolean clearAuthentication = true;
 
+	private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
+
 	/**
 	 * Requires the request to be passed in.
 	 * @param request from which to obtain a HTTP session (cannot be null)
@@ -76,6 +80,8 @@ public class SecurityContextLogoutHandler implements LogoutHandler {
 		if (this.clearAuthentication) {
 			context.setAuthentication(null);
 		}
+		SecurityContext emptyContext = this.securityContextHolderStrategy.createEmptyContext();
+		this.securityContextRepository.saveContext(emptyContext, request, response);
 	}
 
 	public boolean isInvalidateHttpSession() {
@@ -114,4 +120,14 @@ public class SecurityContextLogoutHandler implements LogoutHandler {
 		this.clearAuthentication = clearAuthentication;
 	}
 
+	/**
+	 * Sets the {@link SecurityContextRepository} to use. Default is
+	 * {@link HttpSessionSecurityContextRepository}.
+	 * @param securityContextRepository the {@link SecurityContextRepository} to use.
+	 */
+	public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
+		Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
+		this.securityContextRepository = securityContextRepository;
+	}
+
 }

+ 38 - 8
web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -149,13 +149,46 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
 		SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,
 				SaveContextOnUpdateOrErrorResponseWrapper.class);
 		if (responseWrapper == null) {
-			boolean httpSessionExists = request.getSession(false) != null;
-			SecurityContext initialContext = this.securityContextHolderStrategy.createEmptyContext();
-			responseWrapper = new SaveToSessionResponseWrapper(response, request, httpSessionExists, initialContext);
+			saveContextInHttpSession(context, request);
+			return;
 		}
 		responseWrapper.saveContext(context);
 	}
 
+	private void saveContextInHttpSession(SecurityContext context, HttpServletRequest request) {
+		if (isTransient(context) || isTransient(context.getAuthentication())) {
+			return;
+		}
+		SecurityContext emptyContext = generateNewContext();
+		if (emptyContext.equals(context)) {
+			HttpSession session = request.getSession(false);
+			removeContextFromSession(context, session);
+		}
+		else {
+			boolean createSession = this.allowSessionCreation;
+			HttpSession session = request.getSession(createSession);
+			setContextInSession(context, session);
+		}
+	}
+
+	private void setContextInSession(SecurityContext context, HttpSession session) {
+		if (session != null) {
+			session.setAttribute(this.springSecurityContextKey, context);
+			if (this.logger.isDebugEnabled()) {
+				this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, session));
+			}
+		}
+	}
+
+	private void removeContextFromSession(SecurityContext context, HttpSession session) {
+		if (session != null) {
+			session.removeAttribute(this.springSecurityContextKey);
+			if (this.logger.isDebugEnabled()) {
+				this.logger.debug(LogMessage.format("Removed %s from HttpSession [%s]", context, session));
+			}
+		}
+	}
+
 	@Override
 	public boolean containsContext(HttpServletRequest request) {
 		HttpSession session = request.getSession(false);
@@ -392,11 +425,8 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
 				// We may have a new session, so check also whether the context attribute
 				// is set SEC-1561
 				if (contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null) {
-					httpSession.setAttribute(springSecurityContextKey, context);
+					HttpSessionSecurityContextRepository.this.saveContextInHttpSession(context, this.request);
 					this.isSaveContextInvoked = true;
-					if (this.logger.isDebugEnabled()) {
-						this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, httpSession));
-					}
 				}
 			}
 		}

+ 40 - 2
web/src/test/java/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandlerTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2016 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -27,12 +27,19 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
+import org.springframework.security.web.context.SecurityContextRepository;
+import org.springframework.test.util.ReflectionTestUtils;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
 /**
  * @author Rob Winch
- *
  */
 public class SecurityContextLogoutHandlerTests {
 
@@ -76,4 +83,35 @@ public class SecurityContextLogoutHandlerTests {
 		assertThat(beforeContext.getAuthentication()).isSameAs(beforeAuthentication);
 	}
 
+	@Test
+	public void logoutWhenSecurityContextRepositoryThenSaveEmptyContext() {
+		SecurityContextRepository repository = mock(SecurityContextRepository.class);
+		this.handler.setSecurityContextRepository(repository);
+		this.handler.logout(this.request, this.response, SecurityContextHolder.getContext().getAuthentication());
+		verify(repository).saveContext(eq(SecurityContextHolder.createEmptyContext()), any(), any());
+	}
+
+	@Test
+	public void logoutWhenClearAuthenticationFalseThenSaveEmptyContext() {
+		SecurityContextRepository repository = mock(SecurityContextRepository.class);
+		this.handler.setSecurityContextRepository(repository);
+		this.handler.setClearAuthentication(false);
+		this.handler.logout(this.request, this.response, SecurityContextHolder.getContext().getAuthentication());
+		verify(repository).saveContext(eq(SecurityContextHolder.createEmptyContext()), any(), any());
+	}
+
+	@Test
+	public void constructorWhenDefaultSecurityContextRepositoryThenHttpSessionSecurityContextRepository() {
+		SecurityContextRepository securityContextRepository = (SecurityContextRepository) ReflectionTestUtils
+				.getField(this.handler, "securityContextRepository");
+		assertThat(securityContextRepository).isInstanceOf(HttpSessionSecurityContextRepository.class);
+	}
+
+	@Test
+	public void setSecurityContextRepositoryWhenNullThenException() {
+		assertThatExceptionOfType(IllegalArgumentException.class)
+				.isThrownBy(() -> this.handler.setSecurityContextRepository(null))
+				.withMessage("securityContextRepository cannot be null");
+	}
+
 }

+ 49 - 1
web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -52,6 +52,7 @@ import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextImpl;
 import org.springframework.security.core.context.TransientSecurityContext;
+import org.springframework.security.core.userdetails.PasswordEncodedUser;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetails;
 
@@ -748,6 +749,53 @@ public class HttpSessionSecurityContextRepositoryTests {
 		assertThat(session).isNull();
 	}
 
+	@Test
+	public void saveContextWhenSecurityContextEmptyThenRemoveAttributeFromSession() {
+		HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
+		MockHttpSession session = (MockHttpSession) request.getSession(true);
+		session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, emptyContext);
+		repo.saveContext(emptyContext, request, response);
+		Object attributeAfterSave = session
+				.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
+		assertThat(attributeAfterSave).isNull();
+	}
+
+	@Test
+	public void saveContextWhenSecurityContextEmptyAndNoSessionThenDoesNotCreateSession() {
+		HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
+		repo.saveContext(emptyContext, request, response);
+		assertThat(request.getSession(false)).isNull();
+	}
+
+	@Test
+	public void saveContextWhenSecurityContextThenSaveInSession() {
+		HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		SecurityContext context = createSecurityContext(PasswordEncodedUser.user());
+		repo.saveContext(context, request, response);
+		Object savedContext = request.getSession()
+				.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
+		assertThat(savedContext).isEqualTo(context);
+	}
+
+	@Test
+	public void saveContextWhenTransientAuthenticationThenDoNotSave() {
+		HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		SecurityContext context = SecurityContextHolder.createEmptyContext();
+		context.setAuthentication(new SomeTransientAuthentication());
+		repo.saveContext(context, request, response);
+		assertThat(request.getSession(false)).isNull();
+	}
+
 	private SecurityContext createSecurityContext(UserDetails userDetails) {
 		UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated(userDetails,
 				userDetails.getPassword(), userDetails.getAuthorities());