Kaynağa Gözat

Add SecurityContextHolderFilter

Closes gh-9635
Rob Winch 3 yıl önce
ebeveyn
işleme
972039e65c
21 değiştirilmiş dosya ile 571 ekleme ve 43 silme
  1. 2 0
      config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java
  2. 12 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java
  3. 37 12
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurer.java
  4. 29 14
      config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java
  5. 29 3
      config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java
  6. 3 1
      config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java
  7. 12 3
      config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java
  8. 14 2
      config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParser.java
  9. 3 0
      config/src/main/resources/org/springframework/security/config/spring-security-6.0.rnc
  10. 7 0
      config/src/main/resources/org/springframework/security/config/spring-security-6.0.xsd
  11. 57 4
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java
  12. 33 0
      config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java
  13. 34 0
      config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSave.xml
  14. 38 0
      config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSaveAndExplicitRepository.xml
  15. BIN
      docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.odg
  16. BIN
      docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.png
  17. 6 0
      docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc
  18. 66 1
      docs/modules/ROOT/pages/servlet/authentication/persistence.adoc
  19. 8 3
      test/src/main/java/org/springframework/security/test/web/support/WebTestUtils.java
  20. 86 0
      web/src/main/java/org/springframework/security/web/context/SecurityContextHolderFilter.java
  21. 95 0
      web/src/test/java/org/springframework/security/web/context/SecurityContextHolderFilterTests.java

+ 2 - 0
config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java

@@ -37,6 +37,7 @@ import org.springframework.security.web.authentication.ui.DefaultLoginPageGenera
 import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter;
 import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
 import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
+import org.springframework.security.web.context.SecurityContextHolderFilter;
 import org.springframework.security.web.context.SecurityContextPersistenceFilter;
 import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
 import org.springframework.security.web.csrf.CsrfFilter;
@@ -70,6 +71,7 @@ final class FilterOrderRegistration {
 		put(ChannelProcessingFilter.class, order.next());
 		order.next(); // gh-8105
 		put(WebAsyncManagerIntegrationFilter.class, order.next());
+		put(SecurityContextHolderFilter.class, order.next());
 		put(SecurityContextPersistenceFilter.class, order.next());
 		put(HeaderWriterFilter.class, order.next());
 		put(CorsFilter.class, order.next());

+ 12 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java

@@ -37,6 +37,7 @@ import org.springframework.security.web.authentication.SavedRequestAwareAuthenti
 import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
 import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
+import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.security.web.savedrequest.RequestCache;
 import org.springframework.security.web.util.matcher.AndRequestMatcher;
 import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
@@ -144,6 +145,11 @@ public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecur
 		return getSelf();
 	}
 
+	public T securityContextRepository(SecurityContextRepository securityContextRepository) {
+		this.authFilter.setSecurityContextRepository(securityContextRepository);
+		return getSelf();
+	}
+
 	/**
 	 * Create the {@link RequestMatcher} given a loginProcessingUrl
 	 * @param loginProcessingUrl creates the {@link RequestMatcher} based upon the
@@ -285,6 +291,12 @@ public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecur
 		if (rememberMeServices != null) {
 			this.authFilter.setRememberMeServices(rememberMeServices);
 		}
+		SecurityContextConfigurer securityContextConfigurer = http.getConfigurer(SecurityContextConfigurer.class);
+		if (securityContextConfigurer != null && securityContextConfigurer.isRequireExplicitSave()) {
+			SecurityContextRepository securityContextRepository = securityContextConfigurer
+					.getSecurityContextRepository();
+			this.authFilter.setSecurityContextRepository(securityContextRepository);
+		}
 		F filter = postProcess(this.authFilter);
 		http.addFilter(filter);
 	}

+ 37 - 12
config/src/main/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurer.java

@@ -22,6 +22,7 @@ import org.springframework.security.config.http.SessionCreationPolicy;
 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.SecurityContextHolderFilter;
 import org.springframework.security.web.context.SecurityContextPersistenceFilter;
 import org.springframework.security.web.context.SecurityContextRepository;
 
@@ -62,6 +63,8 @@ import org.springframework.security.web.context.SecurityContextRepository;
 public final class SecurityContextConfigurer<H extends HttpSecurityBuilder<H>>
 		extends AbstractHttpConfigurer<SecurityContextConfigurer<H>, H> {
 
+	private boolean requireExplicitSave;
+
 	/**
 	 * Creates a new instance
 	 * @see HttpSecurity#securityContext()
@@ -79,23 +82,45 @@ public final class SecurityContextConfigurer<H extends HttpSecurityBuilder<H>>
 		return this;
 	}
 
+	public SecurityContextConfigurer<H> requireExplicitSave(boolean requireExplicitSave) {
+		this.requireExplicitSave = requireExplicitSave;
+		return this;
+	}
+
+	boolean isRequireExplicitSave() {
+		return this.requireExplicitSave;
+	}
+
+	SecurityContextRepository getSecurityContextRepository() {
+		SecurityContextRepository securityContextRepository = getBuilder()
+				.getSharedObject(SecurityContextRepository.class);
+		if (securityContextRepository == null) {
+			securityContextRepository = new HttpSessionSecurityContextRepository();
+		}
+		return securityContextRepository;
+	}
+
 	@Override
 	@SuppressWarnings("unchecked")
 	public void configure(H http) {
-		SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
-		if (securityContextRepository == null) {
-			securityContextRepository = new HttpSessionSecurityContextRepository();
+		SecurityContextRepository securityContextRepository = getSecurityContextRepository();
+		if (this.requireExplicitSave) {
+			SecurityContextHolderFilter securityContextHolderFilter = postProcess(
+					new SecurityContextHolderFilter(securityContextRepository));
+			http.addFilter(securityContextHolderFilter);
 		}
-		SecurityContextPersistenceFilter securityContextFilter = new SecurityContextPersistenceFilter(
-				securityContextRepository);
-		SessionManagementConfigurer<?> sessionManagement = http.getConfigurer(SessionManagementConfigurer.class);
-		SessionCreationPolicy sessionCreationPolicy = (sessionManagement != null)
-				? sessionManagement.getSessionCreationPolicy() : null;
-		if (SessionCreationPolicy.ALWAYS == sessionCreationPolicy) {
-			securityContextFilter.setForceEagerSessionCreation(true);
+		else {
+			SecurityContextPersistenceFilter securityContextFilter = new SecurityContextPersistenceFilter(
+					securityContextRepository);
+			SessionManagementConfigurer<?> sessionManagement = http.getConfigurer(SessionManagementConfigurer.class);
+			SessionCreationPolicy sessionCreationPolicy = (sessionManagement != null)
+					? sessionManagement.getSessionCreationPolicy() : null;
+			if (SessionCreationPolicy.ALWAYS == sessionCreationPolicy) {
+				securityContextFilter.setForceEagerSessionCreation(true);
+			}
+			securityContextFilter = postProcess(securityContextFilter);
+			http.addFilter(securityContextFilter);
 		}
-		securityContextFilter = postProcess(securityContextFilter);
-		http.addFilter(securityContextFilter);
 	}
 
 }

+ 29 - 14
config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java

@@ -216,8 +216,8 @@ final class AuthenticationConfigBuilder {
 
 	AuthenticationConfigBuilder(Element element, boolean forceAutoConfig, ParserContext pc,
 			SessionCreationPolicy sessionPolicy, BeanReference requestCache, BeanReference authenticationManager,
-			BeanReference sessionStrategy, BeanReference portMapper, BeanReference portResolver,
-			BeanMetadataElement csrfLogoutHandler) {
+			BeanReference authenticationFilterSecurityContextRepositoryRef, BeanReference sessionStrategy,
+			BeanReference portMapper, BeanReference portResolver, BeanMetadataElement csrfLogoutHandler) {
 		this.httpElt = element;
 		this.pc = pc;
 		this.requestCache = requestCache;
@@ -231,9 +231,10 @@ final class AuthenticationConfigBuilder {
 		createRememberMeFilter(authenticationManager);
 		createBasicFilter(authenticationManager);
 		createBearerTokenAuthenticationFilter(authenticationManager);
-		createFormLoginFilter(sessionStrategy, authenticationManager);
-		createOAuth2ClientFilters(sessionStrategy, requestCache, authenticationManager);
-		createSaml2LoginFilter(authenticationManager);
+		createFormLoginFilter(sessionStrategy, authenticationManager, authenticationFilterSecurityContextRepositoryRef);
+		createOAuth2ClientFilters(sessionStrategy, requestCache, authenticationManager,
+				authenticationFilterSecurityContextRepositoryRef);
+		createSaml2LoginFilter(authenticationManager, authenticationFilterSecurityContextRepositoryRef);
 		createX509Filter(authenticationManager);
 		createJeeFilter(authenticationManager);
 		createLogoutFilter();
@@ -269,7 +270,8 @@ final class AuthenticationConfigBuilder {
 		this.rememberMeProviderRef = new RuntimeBeanReference(id);
 	}
 
-	void createFormLoginFilter(BeanReference sessionStrategy, BeanReference authManager) {
+	void createFormLoginFilter(BeanReference sessionStrategy, BeanReference authManager,
+			BeanReference authenticationFilterSecurityContextRepositoryRef) {
 		Element formLoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.FORM_LOGIN);
 		RootBeanDefinition formFilter = null;
 		if (formLoginElt != null || this.autoConfig) {
@@ -285,6 +287,10 @@ final class AuthenticationConfigBuilder {
 		if (formFilter != null) {
 			formFilter.getPropertyValues().addPropertyValue("allowSessionCreation", this.allowSessionCreation);
 			formFilter.getPropertyValues().addPropertyValue("authenticationManager", authManager);
+			if (authenticationFilterSecurityContextRepositoryRef != null) {
+				formFilter.getPropertyValues().addPropertyValue("securityContextRepository",
+						authenticationFilterSecurityContextRepositoryRef);
+			}
 			// Id is required by login page filter
 			this.formFilterId = this.pc.getReaderContext().generateBeanName(formFilter);
 			this.pc.registerBeanComponent(new BeanComponentDefinition(formFilter, this.formFilterId));
@@ -293,13 +299,15 @@ final class AuthenticationConfigBuilder {
 	}
 
 	void createOAuth2ClientFilters(BeanReference sessionStrategy, BeanReference requestCache,
-			BeanReference authenticationManager) {
-		createOAuth2LoginFilter(sessionStrategy, authenticationManager);
-		createOAuth2ClientFilter(requestCache, authenticationManager);
+			BeanReference authenticationManager, BeanReference authenticationFilterSecurityContextRepositoryRef) {
+		createOAuth2LoginFilter(sessionStrategy, authenticationManager,
+				authenticationFilterSecurityContextRepositoryRef);
+		createOAuth2ClientFilter(requestCache, authenticationManager, authenticationFilterSecurityContextRepositoryRef);
 		registerOAuth2ClientPostProcessors();
 	}
 
-	void createOAuth2LoginFilter(BeanReference sessionStrategy, BeanReference authManager) {
+	void createOAuth2LoginFilter(BeanReference sessionStrategy, BeanReference authManager,
+			BeanReference authenticationFilterSecurityContextRepositoryRef) {
 		Element oauth2LoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OAUTH2_LOGIN);
 		if (oauth2LoginElt == null) {
 			return;
@@ -311,6 +319,10 @@ final class AuthenticationConfigBuilder {
 		BeanDefinition defaultAuthorizedClientRepository = parser.getDefaultAuthorizedClientRepository();
 		registerDefaultAuthorizedClientRepositoryIfNecessary(defaultAuthorizedClientRepository);
 		oauth2LoginFilterBean.getPropertyValues().addPropertyValue("authenticationManager", authManager);
+		if (authenticationFilterSecurityContextRepositoryRef != null) {
+			oauth2LoginFilterBean.getPropertyValues().addPropertyValue("securityContextRepository",
+					authenticationFilterSecurityContextRepositoryRef);
+		}
 
 		// retrieve the other bean result
 		BeanDefinition oauth2LoginAuthProvider = parser.getOAuth2LoginAuthenticationProvider();
@@ -340,14 +352,15 @@ final class AuthenticationConfigBuilder {
 		this.oauth2LoginOidcAuthenticationProviderRef = new RuntimeBeanReference(oauth2LoginOidcAuthProviderId);
 	}
 
-	void createOAuth2ClientFilter(BeanReference requestCache, BeanReference authenticationManager) {
+	void createOAuth2ClientFilter(BeanReference requestCache, BeanReference authenticationManager,
+			BeanReference authenticationFilterSecurityContextRepositoryRef) {
 		Element oauth2ClientElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OAUTH2_CLIENT);
 		if (oauth2ClientElt == null) {
 			return;
 		}
 		this.oauth2ClientEnabled = true;
 		OAuth2ClientBeanDefinitionParser parser = new OAuth2ClientBeanDefinitionParser(requestCache,
-				authenticationManager);
+				authenticationManager, authenticationFilterSecurityContextRepositoryRef);
 		parser.parse(oauth2ClientElt, this.pc);
 		BeanDefinition defaultAuthorizedClientRepository = parser.getDefaultAuthorizedClientRepository();
 		registerDefaultAuthorizedClientRepositoryIfNecessary(defaultAuthorizedClientRepository);
@@ -392,14 +405,16 @@ final class AuthenticationConfigBuilder {
 		}
 	}
 
-	private void createSaml2LoginFilter(BeanReference authenticationManager) {
+	private void createSaml2LoginFilter(BeanReference authenticationManager,
+			BeanReference authenticationFilterSecurityContextRepositoryRef) {
 		Element saml2LoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.SAML2_LOGIN);
 		if (saml2LoginElt == null) {
 			return;
 		}
 		Saml2LoginBeanDefinitionParser parser = new Saml2LoginBeanDefinitionParser(this.csrfIgnoreRequestMatchers,
 				this.portMapper, this.portResolver, this.requestCache, this.allowSessionCreation, authenticationManager,
-				this.authenticationProviders, this.defaultEntryPointMappings);
+				authenticationFilterSecurityContextRepositoryRef, this.authenticationProviders,
+				this.defaultEntryPointMappings);
 		BeanDefinition saml2WebSsoAuthenticationFilter = parser.parse(saml2LoginElt, this.pc);
 		this.saml2AuthorizationRequestFilter = parser.getSaml2WebSsoAuthenticationRequestFilter();
 

+ 29 - 3
config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java

@@ -59,6 +59,7 @@ import org.springframework.security.web.authentication.session.RegisterSessionAu
 import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
 import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
 import org.springframework.security.web.context.NullSecurityContextRepository;
+import org.springframework.security.web.context.SecurityContextHolderFilter;
 import org.springframework.security.web.context.SecurityContextPersistenceFilter;
 import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
 import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
@@ -104,6 +105,8 @@ class HttpConfigurationBuilder {
 
 	private static final String ATT_SECURITY_CONTEXT_REPOSITORY = "security-context-repository-ref";
 
+	private static final String ATT_SECURITY_CONTEXT_EXPLICIT_SAVE = "security-context-explicit-save";
+
 	private static final String ATT_INVALID_SESSION_STRATEGY_REF = "invalid-session-strategy-ref";
 
 	private static final String ATT_DISABLE_URL_REWRITING = "disable-url-rewriting";
@@ -202,8 +205,7 @@ class HttpConfigurationBuilder {
 		this.sessionPolicy = !StringUtils.hasText(createSession) ? SessionCreationPolicy.IF_REQUIRED
 				: createPolicy(createSession);
 		createCsrfFilter();
-		createSecurityContextRepository();
-		createSecurityContextPersistenceFilter();
+		createSecurityPersistence();
 		createSessionManagementFilters();
 		createWebAsyncManagerFilter();
 		createRequestCacheFilter();
@@ -279,9 +281,27 @@ class HttpConfigurationBuilder {
 		return lowerCase ? path.toLowerCase() : path;
 	}
 
+	BeanReference getSecurityContextRepositoryForAuthenticationFilters() {
+		return (isExplicitSave()) ? this.contextRepoRef : null;
+	}
+
+	private void createSecurityPersistence() {
+		createSecurityContextRepository();
+		if (isExplicitSave()) {
+			createSecurityContextHolderFilter();
+		}
+		else {
+			createSecurityContextPersistenceFilter();
+		}
+	}
+
+	private boolean isExplicitSave() {
+		String explicitSaveAttr = this.httpElt.getAttribute(ATT_SECURITY_CONTEXT_EXPLICIT_SAVE);
+		return Boolean.parseBoolean(explicitSaveAttr);
+	}
+
 	private void createSecurityContextPersistenceFilter() {
 		BeanDefinitionBuilder scpf = BeanDefinitionBuilder.rootBeanDefinition(SecurityContextPersistenceFilter.class);
-		String disableUrlRewriting = this.httpElt.getAttribute(ATT_DISABLE_URL_REWRITING);
 		switch (this.sessionPolicy) {
 		case ALWAYS:
 			scpf.addPropertyValue("forceEagerSessionCreation", Boolean.TRUE);
@@ -332,6 +352,12 @@ class HttpConfigurationBuilder {
 		this.contextRepoRef = new RuntimeBeanReference(repoRef);
 	}
 
+	private void createSecurityContextHolderFilter() {
+		BeanDefinitionBuilder filter = BeanDefinitionBuilder.rootBeanDefinition(SecurityContextHolderFilter.class);
+		filter.addConstructorArgValue(this.contextRepoRef);
+		this.securityContextPersistenceFilter = filter.getBeanDefinition();
+	}
+
 	private void createSessionManagementFilters() {
 		Element sessionMgmtElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.SESSION_MANAGEMENT);
 		Element sessionCtrlElt = null;

+ 3 - 1
config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java

@@ -144,9 +144,11 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
 		boolean forceAutoConfig = isDefaultHttpConfig(element);
 		HttpConfigurationBuilder httpBldr = new HttpConfigurationBuilder(element, forceAutoConfig, pc, portMapper,
 				portResolver, authenticationManager);
+		httpBldr.getSecurityContextRepositoryForAuthenticationFilters();
 		AuthenticationConfigBuilder authBldr = new AuthenticationConfigBuilder(element, forceAutoConfig, pc,
 				httpBldr.getSessionCreationPolicy(), httpBldr.getRequestCache(), authenticationManager,
-				httpBldr.getSessionStrategy(), portMapper, portResolver, httpBldr.getCsrfLogoutHandler());
+				httpBldr.getSecurityContextRepositoryForAuthenticationFilters(), httpBldr.getSessionStrategy(),
+				portMapper, portResolver, httpBldr.getCsrfLogoutHandler());
 		httpBldr.setLogoutHandlers(authBldr.getLogoutHandlers());
 		httpBldr.setEntryPoint(authBldr.getEntryPointBean());
 		httpBldr.setAccessDeniedHandler(authBldr.getAccessDeniedHandlerBean());

+ 12 - 3
config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java

@@ -50,6 +50,8 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser {
 
 	private final BeanReference authenticationManager;
 
+	private final BeanReference authenticationFilterSecurityContextRepositoryRef;
+
 	private BeanDefinition defaultAuthorizedClientRepository;
 
 	private BeanDefinition authorizationRequestRedirectFilter;
@@ -58,9 +60,11 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser {
 
 	private BeanDefinition authorizationCodeAuthenticationProvider;
 
-	OAuth2ClientBeanDefinitionParser(BeanReference requestCache, BeanReference authenticationManager) {
+	OAuth2ClientBeanDefinitionParser(BeanReference requestCache, BeanReference authenticationManager,
+			BeanReference authenticationFilterSecurityContextRepositoryRef) {
 		this.requestCache = requestCache;
 		this.authenticationManager = authenticationManager;
+		this.authenticationFilterSecurityContextRepositoryRef = authenticationFilterSecurityContextRepositoryRef;
 	}
 
 	@Override
@@ -92,11 +96,16 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser {
 		this.authorizationRequestRedirectFilter = authorizationRequestRedirectFilterBuilder
 				.addPropertyValue("authorizationRequestRepository", authorizationRequestRepository)
 				.addPropertyValue("requestCache", this.requestCache).getBeanDefinition();
-		this.authorizationCodeGrantFilter = BeanDefinitionBuilder
+		BeanDefinitionBuilder authorizationCodeGrantFilterBldr = BeanDefinitionBuilder
 				.rootBeanDefinition(OAuth2AuthorizationCodeGrantFilter.class)
 				.addConstructorArgValue(clientRegistrationRepository).addConstructorArgValue(authorizedClientRepository)
 				.addConstructorArgValue(this.authenticationManager)
-				.addPropertyValue("authorizationRequestRepository", authorizationRequestRepository).getBeanDefinition();
+				.addPropertyValue("authorizationRequestRepository", authorizationRequestRepository);
+		if (this.authenticationFilterSecurityContextRepositoryRef != null) {
+			authorizationCodeGrantFilterBldr.addPropertyValue("securityContextRepository",
+					this.authenticationFilterSecurityContextRepositoryRef);
+		}
+		this.authorizationCodeGrantFilter = authorizationCodeGrantFilterBldr.getBeanDefinition();
 
 		BeanMetadataElement accessTokenResponseClient = getAccessTokenResponseClient(authorizationCodeGrantElt);
 		this.authorizationCodeAuthenticationProvider = BeanDefinitionBuilder

+ 14 - 2
config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParser.java

@@ -85,6 +85,8 @@ final class Saml2LoginBeanDefinitionParser implements BeanDefinitionParser {
 
 	private final BeanReference authenticationManager;
 
+	private final BeanReference authenticationFilterSecurityContextRepositoryRef;
+
 	private final List<BeanReference> authenticationProviders;
 
 	private final Map<BeanDefinition, BeanMetadataElement> entryPoints;
@@ -97,14 +99,15 @@ final class Saml2LoginBeanDefinitionParser implements BeanDefinitionParser {
 
 	Saml2LoginBeanDefinitionParser(List<BeanDefinition> csrfIgnoreRequestMatchers, BeanReference portMapper,
 			BeanReference portResolver, BeanReference requestCache, boolean allowSessionCreation,
-			BeanReference authenticationManager, List<BeanReference> authenticationProviders,
-			Map<BeanDefinition, BeanMetadataElement> entryPoints) {
+			BeanReference authenticationManager, BeanReference authenticationFilterSecurityContextRepositoryRef,
+			List<BeanReference> authenticationProviders, Map<BeanDefinition, BeanMetadataElement> entryPoints) {
 		this.csrfIgnoreRequestMatchers = csrfIgnoreRequestMatchers;
 		this.portMapper = portMapper;
 		this.portResolver = portResolver;
 		this.requestCache = requestCache;
 		this.allowSessionCreation = allowSessionCreation;
 		this.authenticationManager = authenticationManager;
+		this.authenticationFilterSecurityContextRepositoryRef = authenticationFilterSecurityContextRepositoryRef;
 		this.authenticationProviders = authenticationProviders;
 		this.entryPoints = entryPoints;
 	}
@@ -148,6 +151,7 @@ final class Saml2LoginBeanDefinitionParser implements BeanDefinitionParser {
 		resolveAuthenticationSuccessHandler(element, saml2WebSsoAuthenticationFilterBuilder);
 		resolveAuthenticationFailureHandler(element, saml2WebSsoAuthenticationFilterBuilder);
 		resolveAuthenticationManager(element, saml2WebSsoAuthenticationFilterBuilder);
+		resolveSecurityContextRepository(element, saml2WebSsoAuthenticationFilterBuilder);
 		// Configure the Saml2WebSsoAuthenticationRequestFilter
 		this.saml2WebSsoAuthenticationRequestFilter = BeanDefinitionBuilder
 				.rootBeanDefinition(Saml2WebSsoAuthenticationRequestFilter.class)
@@ -176,6 +180,14 @@ final class Saml2LoginBeanDefinitionParser implements BeanDefinitionParser {
 		}
 	}
 
+	private void resolveSecurityContextRepository(Element element,
+			BeanDefinitionBuilder saml2WebSsoAuthenticationFilterBuilder) {
+		if (this.authenticationFilterSecurityContextRepositoryRef != null) {
+			saml2WebSsoAuthenticationFilterBuilder.addPropertyValue("securityContextRepository",
+					this.authenticationFilterSecurityContextRepositoryRef);
+		}
+	}
+
 	private void resolveLoginPage(Element element, ParserContext parserContext) {
 		String loginPage = element.getAttribute(ATT_LOGIN_PAGE);
 		Object source = parserContext.extractSource(element);

+ 3 - 0
config/src/main/resources/org/springframework/security/config/spring-security-6.0.rnc

@@ -333,6 +333,9 @@ http.attlist &=
 http.attlist &=
 	## A reference to a SecurityContextRepository bean. This can be used to customize how the SecurityContext is stored between requests.
 	attribute security-context-repository-ref {xsd:token}?
+http.attlist &=
+	## Optional attribute that specifies that the SecurityContext should require explicit saving rather than being synchronized from the SecurityContextHolder. Defaults to "false".
+	attribute security-context-explicit-save {xsd:boolean}?
 http.attlist &=
 	request-matcher?
 http.attlist &=

+ 7 - 0
config/src/main/resources/org/springframework/security/config/spring-security-6.0.xsd

@@ -1215,6 +1215,13 @@
                 </xs:documentation>
          </xs:annotation>
       </xs:attribute>
+      <xs:attribute name="security-context-explicit-save" type="xs:boolean">
+         <xs:annotation>
+            <xs:documentation>Optional attribute that specifies that the SecurityContext should require explicit saving
+                rather than being synchronized from the SecurityContextHolder. Defaults to "false".
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
       <xs:attribute name="request-matcher">
          <xs:annotation>
             <xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'mvc'

+ 57 - 4
config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java

@@ -16,6 +16,10 @@
 
 package org.springframework.security.config.annotation.web.configurers;
 
+import java.util.List;
+import java.util.stream.Collectors;
+
+import jakarta.servlet.Filter;
 import jakarta.servlet.http.HttpSession;
 
 import org.junit.jupiter.api.Test;
@@ -33,8 +37,11 @@ import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.userdetails.PasswordEncodedUser;
+import org.springframework.security.web.FilterChainProxy;
 import org.springframework.security.web.context.HttpRequestResponseHolder;
+import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
 import org.springframework.security.web.context.NullSecurityContextRepository;
+import org.springframework.security.web.context.SecurityContextHolderFilter;
 import org.springframework.security.web.context.SecurityContextPersistenceFilter;
 import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
@@ -110,6 +117,27 @@ public class SecurityContextConfigurerTests {
 		assertThat(session).isNull();
 	}
 
+	@Test
+	public void requireExplicitSave() throws Exception {
+		HttpSessionSecurityContextRepository repository = new HttpSessionSecurityContextRepository();
+		SpringTestContext testContext = this.spring.register(RequireExplicitSaveConfig.class);
+		testContext.autowire();
+		FilterChainProxy filterChainProxy = testContext.getContext().getBean(FilterChainProxy.class);
+		// @formatter:off
+		List<Class<? extends Filter>> filterTypes = filterChainProxy.getFilters("/")
+				.stream()
+				.map(Filter::getClass)
+				.collect(Collectors.toList());
+		assertThat(filterTypes)
+				.contains(SecurityContextHolderFilter.class)
+				.doesNotContain(SecurityContextPersistenceFilter.class);
+		// @formatter:on
+		MvcResult mvcResult = this.mvc.perform(formLogin()).andReturn();
+		SecurityContext securityContext = repository
+				.loadContext(new HttpRequestResponseHolder(mvcResult.getRequest(), mvcResult.getResponse()));
+		assertThat(securityContext.getAuthentication()).isNotNull();
+	}
+
 	@EnableWebSecurity
 	static class ObjectPostProcessorConfig extends WebSecurityConfigurerAdapter {
 
@@ -241,14 +269,39 @@ public class SecurityContextConfigurerTests {
 	@EnableWebSecurity
 	static class NullSecurityContextRepositoryInLambdaConfig extends WebSecurityConfigurerAdapter {
 
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+					.formLogin(withDefaults())
+					.securityContext((securityContext) ->
+							securityContext
+									.securityContextRepository(new NullSecurityContextRepository())
+					);
+			// @formatter:on
+		}
+
+		@Override
+		protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+			// @formatter:off
+			auth
+					.inMemoryAuthentication()
+					.withUser(PasswordEncodedUser.user());
+			// @formatter:on
+		}
+
+	}
+
+	@EnableWebSecurity
+	static class RequireExplicitSaveConfig extends WebSecurityConfigurerAdapter {
+
 		@Override
 		protected void configure(HttpSecurity http) throws Exception {
 			// @formatter:off
 			http
 				.formLogin(withDefaults())
-				.securityContext((securityContext) ->
-					securityContext
-						.securityContextRepository(new NullSecurityContextRepository())
+				.securityContext((securityContext) -> securityContext
+					.requireExplicitSave(true)
 				);
 			// @formatter:on
 		}
@@ -258,7 +311,7 @@ public class SecurityContextConfigurerTests {
 			// @formatter:off
 			auth
 				.inMemoryAuthentication()
-					.withUser(PasswordEncodedUser.user());
+				.withUser(PasswordEncodedUser.user());
 			// @formatter:on
 		}
 

+ 33 - 0
config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java

@@ -121,9 +121,11 @@ import static org.mockito.BDDMockito.willAnswer;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.x509;
+import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
 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;
@@ -461,6 +463,37 @@ public class MiscHttpConfigTests {
 				any(HttpServletResponse.class));
 	}
 
+	@Test
+	public void getWhenExplicitSaveAndRepositoryAndAuthenticatingThenConsultsCustomSecurityContextRepository()
+			throws Exception {
+		this.spring.configLocations(xml("ExplicitSaveAndExplicitRepository")).autowire();
+		SecurityContextRepository repository = this.spring.getContext().getBean(SecurityContextRepository.class);
+		SecurityContext context = new SecurityContextImpl(new TestingAuthenticationToken("user", "password"));
+		given(repository.loadContext(any(HttpRequestResponseHolder.class))).willReturn(context);
+		// @formatter:off
+		MvcResult result = this.mvc.perform(formLogin())
+				.andExpect(status().is3xxRedirection())
+				.andExpect(authenticated())
+				.andReturn();
+		// @formatter:on
+		verify(repository, atLeastOnce()).saveContext(any(SecurityContext.class), any(HttpServletRequest.class),
+				any(HttpServletResponse.class));
+	}
+
+	@Test
+	public void getWhenExplicitSaveAndExplicitSaveAndAuthenticatingThenConsultsCustomSecurityContextRepository()
+			throws Exception {
+		this.spring.configLocations(xml("ExplicitSave")).autowire();
+		SecurityContextRepository repository = this.spring.getContext().getBean(SecurityContextRepository.class);
+		// @formatter:off
+		MvcResult result = this.mvc.perform(formLogin())
+				.andExpect(status().is3xxRedirection())
+				.andReturn();
+		// @formatter:on
+		assertThat(repository.loadContext(new HttpRequestResponseHolder(result.getRequest(), result.getResponse()))
+				.getAuthentication()).isNotNull();
+	}
+
 	@Test
 	public void getWhenUsingInterceptUrlExpressionsThenAuthorizesAccordingly() throws Exception {
 		this.spring.configLocations(xml("InterceptUrlExpressions")).autowire();

+ 34 - 0
config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSave.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2002-2018 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
+  ~
+  ~       https://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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http security-context-explicit-save="true">
+		<form-login/>
+		<intercept-url pattern="/**" access="authenticated"/>
+	</http>
+
+	<b:import resource="MiscHttpConfigTests-controllers.xml"/>
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 38 - 0
config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSaveAndExplicitRepository.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2002-2018 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
+  ~
+  ~       https://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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http create-session="always" security-context-repository-ref="repo" security-context-explicit-save="true">
+		<form-login/>
+		<intercept-url pattern="/**" access="authenticated"/>
+	</http>
+
+	<b:bean name="repo" class="org.mockito.Mockito" factory-method="mock">
+		<b:constructor-arg value="org.springframework.security.web.context.SecurityContextRepository"/>
+	</b:bean>
+
+	<b:import resource="MiscHttpConfigTests-controllers.xml"/>
+	<b:import resource="userservice.xml"/>
+</b:beans>

BIN
docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.odg


BIN
docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.png


+ 6 - 0
docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc

@@ -125,6 +125,12 @@ A request pattern can be mapped to an empty filter chain, by setting this attrib
 No security will be applied and none of Spring Security's features will be available.
 
 
+[[nsa-http-security-context-explicit-save]]
+* **security-context-explicit-save**
+If true, use `SecurityContextHolderFilter` instead of `SecurityContextPersistenceFilter`.
+Requires explicit save
+
+
 [[nsa-http-security-context-repository-ref]]
 * **security-context-repository-ref**
 Allows injection of a custom `SecurityContextRepository` into the `SecurityContextPersistenceFilter`.

+ 66 - 1
docs/modules/ROOT/pages/servlet/authentication/persistence.adoc

@@ -88,6 +88,34 @@ Depending on the servlet container implementation, the error means that any `Sec
 When the error dispatch is made, there is no `SecurityContext` established.
 This means that the error page cannot use the `SecurityContext` for authorization or displaying the current user unless the `SecurityContext` is persisted somehow.
 
+.Use RequestAttributeSecurityContextRepository
+====
+.Java
+[source,java,role="primary"]
+----
+public SecurityFilterChain filterChain(HttpSecurity http) {
+	http
+		// ...
+		.securityContext((securityContext) -> securityContext
+			.securityContextRepository(new RequestAttributeSecurityContextRepository())
+		);
+	return http.build();
+}
+----
+
+.XML
+[source,xml,role="secondary"]
+----
+<http security-context-repository-ref="contextRepository">
+	<!-- ... -->
+</http>
+<b:bean name="contextRepository"
+	class="org.springframework.security.web.context.RequestAttributeSecurityContextRepository" />
+----
+====
+
+
+[[securitycontextpersistencefilter]]
 == SecurityContextPersistenceFilter
 
 The {security-api-url}org/springframework/security/web/context/SecurityContextPersistenceFilter.html[`SecurityContextPersistenceFilter`] is responsible for persisting the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`].
@@ -104,4 +132,41 @@ For example, if a redirect is sent to the client the response is immediately wri
 This means that establishing an `HttpSession` would not be possible in step 3 because the session id could not be included in the already written response.
 Another situation that can happen is that if a client authenticates successfully, the response is committed before `SecurityContextPersistenceFilter` completes, and the client makes a second request before the `SecurityContextPersistenceFilter` completes the wrong authentication could be present in the second request.
 
-To avoid these problems, the `SecurityContextPersistenceFilter` wraps both the `HttpServletRequest` and the `HttpServletResponse` to detect if the `SecurityContext` has changed and if so save the `SecurityContext` just before the response is committed.
+To avoid these problems, the `SecurityContextPersistenceFilter` wraps both the `HttpServletRequest` and the `HttpServletResponse` to detect if the `SecurityContext` has changed and if so save the `SecurityContext` just before the response is committed.
+
+[[securitycontextholderfilter]]
+== SecurityContextHolderFilter
+
+The {security-api-url}org/springframework/security/web/context/SecurityContextHolderFilter.html[`SecurityContextHolderFilter`] is responsible for loading the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`].
+
+image::{figures}/securitycontextholderfilter.png[]
+
+<1> Before running the rest of the application, `SecurityContextHolderFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
+<2> Next, the application is ran.
+
+Unlike, xref:servlet/authentication/persistence.adoc#securitycontextpersistencefilter[`SecurityContextPersisteneFilter`], `SecurityContextHolderFilter` only loads the `SecurityContext` it does not save the `SecurityContext`.
+This means that when using `SecurityContextHolderFilter`, it is required that the `SecurityContext` is explicitly saved.
+
+.Explicit Saving of SecurityContext
+====
+.Java
+[source,java,role="primary"]
+----
+public SecurityFilterChain filterChain(HttpSecurity http) {
+	http
+		// ...
+		.securityContext((securityContext) -> securityContext
+			.requireExplicitSave(true)
+		);
+	return http.build();
+}
+----
+
+.XML
+[source,xml,role="secondary"]
+----
+<http security-context-explicit-save="true">
+	<!-- ... -->
+</http>
+----
+====

+ 8 - 3
test/src/main/java/org/springframework/security/test/web/support/WebTestUtils.java

@@ -26,6 +26,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 import org.springframework.security.config.BeanIds;
 import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
 import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
+import org.springframework.security.web.context.SecurityContextHolderFilter;
 import org.springframework.security.web.context.SecurityContextPersistenceFilter;
 import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.security.web.csrf.CsrfFilter;
@@ -61,10 +62,14 @@ public abstract class WebTestUtils {
 	 */
 	public static SecurityContextRepository getSecurityContextRepository(HttpServletRequest request) {
 		SecurityContextPersistenceFilter filter = findFilter(request, SecurityContextPersistenceFilter.class);
-		if (filter == null) {
-			return DEFAULT_CONTEXT_REPO;
+		if (filter != null) {
+			return (SecurityContextRepository) ReflectionTestUtils.getField(filter, "repo");
+		}
+		SecurityContextHolderFilter holderFilter = findFilter(request, SecurityContextHolderFilter.class);
+		if (holderFilter != null) {
+			return (SecurityContextRepository) ReflectionTestUtils.getField(holderFilter, "securityContextRepository");
 		}
-		return (SecurityContextRepository) ReflectionTestUtils.getField(filter, "repo");
+		return DEFAULT_CONTEXT_REPO;
 	}
 
 	/**

+ 86 - 0
web/src/main/java/org/springframework/security/web/context/SecurityContextHolderFilter.java

@@ -0,0 +1,86 @@
+/*
+ * Copyright 2002-2022 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
+ *
+ *      https://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.context;
+
+import java.io.IOException;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.Assert;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+/**
+ * A {@link jakarta.servlet.Filter} that uses the {@link SecurityContextRepository} to
+ * obtain the {@link SecurityContext} and set it on the {@link SecurityContextHolder}.
+ * This is similar to {@link SecurityContextPersistenceFilter} except that the
+ * {@link SecurityContextRepository#saveContext(SecurityContext, HttpServletRequest, HttpServletResponse)}
+ * must be explicitly invoked to save the {@link SecurityContext}. This improves the
+ * efficiency and provides better flexibility by allowing different authentication
+ * mechanisms to choose individually if authentication should be persisted.
+ *
+ * @author Rob Winch
+ * @since 5.7
+ */
+public class SecurityContextHolderFilter extends OncePerRequestFilter {
+
+	private final SecurityContextRepository securityContextRepository;
+
+	private boolean shouldNotFilterErrorDispatch;
+
+	/**
+	 * Creates a new instance.
+	 * @param securityContextRepository the repository to use. Cannot be null.
+	 */
+	public SecurityContextHolderFilter(SecurityContextRepository securityContextRepository) {
+		Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
+		this.securityContextRepository = securityContextRepository;
+	}
+
+	@Override
+	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+			throws ServletException, IOException {
+		SecurityContext securityContext = this.securityContextRepository
+				.loadContext(new HttpRequestResponseHolder(request, response));
+		try {
+			SecurityContextHolder.setContext(securityContext);
+			filterChain.doFilter(request, response);
+		}
+		finally {
+			SecurityContextHolder.clearContext();
+		}
+	}
+
+	@Override
+	protected boolean shouldNotFilterErrorDispatch() {
+		return this.shouldNotFilterErrorDispatch;
+	}
+
+	/**
+	 * Disables {@link SecurityContextHolderFilter} for error dispatch.
+	 * @param shouldNotFilterErrorDispatch if the Filter should be disabled for error
+	 * dispatch. Default is false.
+	 */
+	public void setShouldNotFilterErrorDispatch(boolean shouldNotFilterErrorDispatch) {
+		this.shouldNotFilterErrorDispatch = shouldNotFilterErrorDispatch;
+	}
+
+}

+ 95 - 0
web/src/test/java/org/springframework/security/web/context/SecurityContextHolderFilterTests.java

@@ -0,0 +1,95 @@
+/*
+ * Copyright 2002-2022 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
+ *
+ *      https://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.context;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import org.springframework.security.authentication.TestAuthentication;
+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.SecurityContextImpl;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+
+@ExtendWith(MockitoExtension.class)
+class SecurityContextHolderFilterTests {
+
+	@Mock
+	private SecurityContextRepository repository;
+
+	@Mock
+	private HttpServletRequest request;
+
+	@Mock
+	private HttpServletResponse response;
+
+	@Mock
+	private FilterChain chain;
+
+	@Captor
+	private ArgumentCaptor<HttpRequestResponseHolder> requestResponse;
+
+	private SecurityContextHolderFilter filter;
+
+	@BeforeEach
+	void setup() {
+		this.filter = new SecurityContextHolderFilter(this.repository);
+	}
+
+	@AfterEach
+	void cleanup() {
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	void doFilterThenSetsAndClearsSecurityContextHolder() throws Exception {
+		Authentication authentication = TestAuthentication.authenticatedUser();
+		SecurityContext expectedContext = new SecurityContextImpl(authentication);
+		given(this.repository.loadContext(this.requestResponse.capture())).willReturn(expectedContext);
+		FilterChain filterChain = (request, response) -> assertThat(SecurityContextHolder.getContext())
+				.isEqualTo(expectedContext);
+
+		this.filter.doFilter(this.request, this.response, filterChain);
+
+		assertThat(SecurityContextHolder.getContext()).isEqualTo(SecurityContextHolder.createEmptyContext());
+	}
+
+	@Test
+	void shouldNotFilterErrorDispatchWhenDefault() {
+		assertThat(this.filter.shouldNotFilterErrorDispatch()).isFalse();
+	}
+
+	@Test
+	void shouldNotFilterErrorDispatchWhenOverridden() {
+		this.filter.setShouldNotFilterErrorDispatch(true);
+		assertThat(this.filter.shouldNotFilterErrorDispatch()).isTrue();
+	}
+
+}