浏览代码

Add SecurityContextHolderFilter

Closes gh-9635
Rob Winch 3 年之前
父节点
当前提交
87ed31a99c
共有 22 个文件被更改,包括 582 次插入45 次删除
  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. 37 16
      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-5.7.rnc
  10. 7 0
      config/src/main/resources/org/springframework/security/config/spring-security-5.7.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. 二进制
      docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.odg
  16. 二进制
      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. 3 0
      docs/modules/ROOT/pages/whats-new.adoc
  20. 8 3
      test/src/main/java/org/springframework/security/test/web/support/WebTestUtils.java
  21. 86 0
      web/src/main/java/org/springframework/security/web/context/SecurityContextHolderFilter.java
  22. 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.ui.DefaultLogoutPageGeneratingFilter;
 import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
 import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
 import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
 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.SecurityContextPersistenceFilter;
 import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
 import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
 import org.springframework.security.web.csrf.CsrfFilter;
 import org.springframework.security.web.csrf.CsrfFilter;
@@ -70,6 +71,7 @@ final class FilterOrderRegistration {
 		put(ChannelProcessingFilter.class, order.next());
 		put(ChannelProcessingFilter.class, order.next());
 		order.next(); // gh-8105
 		order.next(); // gh-8105
 		put(WebAsyncManagerIntegrationFilter.class, order.next());
 		put(WebAsyncManagerIntegrationFilter.class, order.next());
+		put(SecurityContextHolderFilter.class, order.next());
 		put(SecurityContextPersistenceFilter.class, order.next());
 		put(SecurityContextPersistenceFilter.class, order.next());
 		put(HeaderWriterFilter.class, order.next());
 		put(HeaderWriterFilter.class, order.next());
 		put(CorsFilter.class, order.next());
 		put(CorsFilter.class, order.next());

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

@@ -38,6 +38,7 @@ import org.springframework.security.web.authentication.SavedRequestAwareAuthenti
 import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
 import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
 import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
 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.savedrequest.RequestCache;
 import org.springframework.security.web.util.matcher.AndRequestMatcher;
 import org.springframework.security.web.util.matcher.AndRequestMatcher;
 import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
 import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
@@ -146,6 +147,11 @@ public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecur
 		return getSelf();
 		return getSelf();
 	}
 	}
 
 
+	public T securityContextRepository(SecurityContextRepository securityContextRepository) {
+		this.authFilter.setSecurityContextRepository(securityContextRepository);
+		return getSelf();
+	}
+
 	/**
 	/**
 	 * Create the {@link RequestMatcher} given a loginProcessingUrl
 	 * Create the {@link RequestMatcher} given a loginProcessingUrl
 	 * @param loginProcessingUrl creates the {@link RequestMatcher} based upon the
 	 * @param loginProcessingUrl creates the {@link RequestMatcher} based upon the
@@ -287,6 +293,12 @@ public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecur
 		if (rememberMeServices != null) {
 		if (rememberMeServices != null) {
 			this.authFilter.setRememberMeServices(rememberMeServices);
 			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);
 		F filter = postProcess(this.authFilter);
 		http.addFilter(filter);
 		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.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
 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.SecurityContextPersistenceFilter;
 import org.springframework.security.web.context.SecurityContextRepository;
 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>>
 public final class SecurityContextConfigurer<H extends HttpSecurityBuilder<H>>
 		extends AbstractHttpConfigurer<SecurityContextConfigurer<H>, H> {
 		extends AbstractHttpConfigurer<SecurityContextConfigurer<H>, H> {
 
 
+	private boolean requireExplicitSave;
+
 	/**
 	/**
 	 * Creates a new instance
 	 * Creates a new instance
 	 * @see HttpSecurity#securityContext()
 	 * @see HttpSecurity#securityContext()
@@ -79,23 +82,45 @@ public final class SecurityContextConfigurer<H extends HttpSecurityBuilder<H>>
 		return this;
 		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
 	@Override
 	@SuppressWarnings("unchecked")
 	@SuppressWarnings("unchecked")
 	public void configure(H http) {
 	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);
 	}
 	}
 
 
 }
 }

+ 37 - 16
config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java

@@ -236,8 +236,8 @@ final class AuthenticationConfigBuilder {
 
 
 	AuthenticationConfigBuilder(Element element, boolean forceAutoConfig, ParserContext pc,
 	AuthenticationConfigBuilder(Element element, boolean forceAutoConfig, ParserContext pc,
 			SessionCreationPolicy sessionPolicy, BeanReference requestCache, BeanReference authenticationManager,
 			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.httpElt = element;
 		this.pc = pc;
 		this.pc = pc;
 		this.requestCache = requestCache;
 		this.requestCache = requestCache;
@@ -251,10 +251,12 @@ final class AuthenticationConfigBuilder {
 		createRememberMeFilter(authenticationManager);
 		createRememberMeFilter(authenticationManager);
 		createBasicFilter(authenticationManager);
 		createBasicFilter(authenticationManager);
 		createBearerTokenAuthenticationFilter(authenticationManager);
 		createBearerTokenAuthenticationFilter(authenticationManager);
-		createFormLoginFilter(sessionStrategy, authenticationManager);
-		createOAuth2ClientFilters(sessionStrategy, requestCache, authenticationManager);
-		createOpenIDLoginFilter(sessionStrategy, authenticationManager);
-		createSaml2LoginFilter(authenticationManager);
+		createFormLoginFilter(sessionStrategy, authenticationManager, authenticationFilterSecurityContextRepositoryRef);
+		createOAuth2ClientFilters(sessionStrategy, requestCache, authenticationManager,
+				authenticationFilterSecurityContextRepositoryRef);
+		createOpenIDLoginFilter(sessionStrategy, authenticationManager,
+				authenticationFilterSecurityContextRepositoryRef);
+		createSaml2LoginFilter(authenticationManager, authenticationFilterSecurityContextRepositoryRef);
 		createX509Filter(authenticationManager);
 		createX509Filter(authenticationManager);
 		createJeeFilter(authenticationManager);
 		createJeeFilter(authenticationManager);
 		createLogoutFilter();
 		createLogoutFilter();
@@ -290,7 +292,8 @@ final class AuthenticationConfigBuilder {
 		this.rememberMeProviderRef = new RuntimeBeanReference(id);
 		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);
 		Element formLoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.FORM_LOGIN);
 		RootBeanDefinition formFilter = null;
 		RootBeanDefinition formFilter = null;
 		if (formLoginElt != null || this.autoConfig) {
 		if (formLoginElt != null || this.autoConfig) {
@@ -306,6 +309,10 @@ final class AuthenticationConfigBuilder {
 		if (formFilter != null) {
 		if (formFilter != null) {
 			formFilter.getPropertyValues().addPropertyValue("allowSessionCreation", this.allowSessionCreation);
 			formFilter.getPropertyValues().addPropertyValue("allowSessionCreation", this.allowSessionCreation);
 			formFilter.getPropertyValues().addPropertyValue("authenticationManager", authManager);
 			formFilter.getPropertyValues().addPropertyValue("authenticationManager", authManager);
+			if (authenticationFilterSecurityContextRepositoryRef != null) {
+				formFilter.getPropertyValues().addPropertyValue("securityContextRepository",
+						authenticationFilterSecurityContextRepositoryRef);
+			}
 			// Id is required by login page filter
 			// Id is required by login page filter
 			this.formFilterId = this.pc.getReaderContext().generateBeanName(formFilter);
 			this.formFilterId = this.pc.getReaderContext().generateBeanName(formFilter);
 			this.pc.registerBeanComponent(new BeanComponentDefinition(formFilter, this.formFilterId));
 			this.pc.registerBeanComponent(new BeanComponentDefinition(formFilter, this.formFilterId));
@@ -314,13 +321,15 @@ final class AuthenticationConfigBuilder {
 	}
 	}
 
 
 	void createOAuth2ClientFilters(BeanReference sessionStrategy, BeanReference requestCache,
 	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();
 		registerOAuth2ClientPostProcessors();
 	}
 	}
 
 
-	void createOAuth2LoginFilter(BeanReference sessionStrategy, BeanReference authManager) {
+	void createOAuth2LoginFilter(BeanReference sessionStrategy, BeanReference authManager,
+			BeanReference authenticationFilterSecurityContextRepositoryRef) {
 		Element oauth2LoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OAUTH2_LOGIN);
 		Element oauth2LoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OAUTH2_LOGIN);
 		if (oauth2LoginElt == null) {
 		if (oauth2LoginElt == null) {
 			return;
 			return;
@@ -332,6 +341,10 @@ final class AuthenticationConfigBuilder {
 		BeanDefinition defaultAuthorizedClientRepository = parser.getDefaultAuthorizedClientRepository();
 		BeanDefinition defaultAuthorizedClientRepository = parser.getDefaultAuthorizedClientRepository();
 		registerDefaultAuthorizedClientRepositoryIfNecessary(defaultAuthorizedClientRepository);
 		registerDefaultAuthorizedClientRepositoryIfNecessary(defaultAuthorizedClientRepository);
 		oauth2LoginFilterBean.getPropertyValues().addPropertyValue("authenticationManager", authManager);
 		oauth2LoginFilterBean.getPropertyValues().addPropertyValue("authenticationManager", authManager);
+		if (authenticationFilterSecurityContextRepositoryRef != null) {
+			oauth2LoginFilterBean.getPropertyValues().addPropertyValue("securityContextRepository",
+					authenticationFilterSecurityContextRepositoryRef);
+		}
 
 
 		// retrieve the other bean result
 		// retrieve the other bean result
 		BeanDefinition oauth2LoginAuthProvider = parser.getOAuth2LoginAuthenticationProvider();
 		BeanDefinition oauth2LoginAuthProvider = parser.getOAuth2LoginAuthenticationProvider();
@@ -361,14 +374,15 @@ final class AuthenticationConfigBuilder {
 		this.oauth2LoginOidcAuthenticationProviderRef = new RuntimeBeanReference(oauth2LoginOidcAuthProviderId);
 		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);
 		Element oauth2ClientElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OAUTH2_CLIENT);
 		if (oauth2ClientElt == null) {
 		if (oauth2ClientElt == null) {
 			return;
 			return;
 		}
 		}
 		this.oauth2ClientEnabled = true;
 		this.oauth2ClientEnabled = true;
 		OAuth2ClientBeanDefinitionParser parser = new OAuth2ClientBeanDefinitionParser(requestCache,
 		OAuth2ClientBeanDefinitionParser parser = new OAuth2ClientBeanDefinitionParser(requestCache,
-				authenticationManager);
+				authenticationManager, authenticationFilterSecurityContextRepositoryRef);
 		parser.parse(oauth2ClientElt, this.pc);
 		parser.parse(oauth2ClientElt, this.pc);
 		BeanDefinition defaultAuthorizedClientRepository = parser.getDefaultAuthorizedClientRepository();
 		BeanDefinition defaultAuthorizedClientRepository = parser.getDefaultAuthorizedClientRepository();
 		registerDefaultAuthorizedClientRepositoryIfNecessary(defaultAuthorizedClientRepository);
 		registerDefaultAuthorizedClientRepositoryIfNecessary(defaultAuthorizedClientRepository);
@@ -413,7 +427,8 @@ final class AuthenticationConfigBuilder {
 		}
 		}
 	}
 	}
 
 
-	void createOpenIDLoginFilter(BeanReference sessionStrategy, BeanReference authManager) {
+	void createOpenIDLoginFilter(BeanReference sessionStrategy, BeanReference authManager,
+			BeanReference authenticationFilterSecurityContextRepositoryRef) {
 		Element openIDLoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OPENID_LOGIN);
 		Element openIDLoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OPENID_LOGIN);
 		RootBeanDefinition openIDFilter = null;
 		RootBeanDefinition openIDFilter = null;
 		if (openIDLoginElt != null) {
 		if (openIDLoginElt != null) {
@@ -422,6 +437,10 @@ final class AuthenticationConfigBuilder {
 		if (openIDFilter != null) {
 		if (openIDFilter != null) {
 			openIDFilter.getPropertyValues().addPropertyValue("allowSessionCreation", this.allowSessionCreation);
 			openIDFilter.getPropertyValues().addPropertyValue("allowSessionCreation", this.allowSessionCreation);
 			openIDFilter.getPropertyValues().addPropertyValue("authenticationManager", authManager);
 			openIDFilter.getPropertyValues().addPropertyValue("authenticationManager", authManager);
+			if (authenticationFilterSecurityContextRepositoryRef != null) {
+				openIDFilter.getPropertyValues().addPropertyValue("securityContextRepository",
+						authenticationFilterSecurityContextRepositoryRef);
+			}
 			// Required by login page filter
 			// Required by login page filter
 			this.openIDFilterId = this.pc.getReaderContext().generateBeanName(openIDFilter);
 			this.openIDFilterId = this.pc.getReaderContext().generateBeanName(openIDFilter);
 			this.pc.registerBeanComponent(new BeanComponentDefinition(openIDFilter, this.openIDFilterId));
 			this.pc.registerBeanComponent(new BeanComponentDefinition(openIDFilter, this.openIDFilterId));
@@ -430,14 +449,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);
 		Element saml2LoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.SAML2_LOGIN);
 		if (saml2LoginElt == null) {
 		if (saml2LoginElt == null) {
 			return;
 			return;
 		}
 		}
 		Saml2LoginBeanDefinitionParser parser = new Saml2LoginBeanDefinitionParser(this.csrfIgnoreRequestMatchers,
 		Saml2LoginBeanDefinitionParser parser = new Saml2LoginBeanDefinitionParser(this.csrfIgnoreRequestMatchers,
 				this.portMapper, this.portResolver, this.requestCache, this.allowSessionCreation, authenticationManager,
 				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);
 		BeanDefinition saml2WebSsoAuthenticationFilter = parser.parse(saml2LoginElt, this.pc);
 		this.saml2AuthorizationRequestFilter = parser.getSaml2WebSsoAuthenticationRequestFilter();
 		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.authentication.session.SessionFixationProtectionStrategy;
 import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
 import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
 import org.springframework.security.web.context.NullSecurityContextRepository;
 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.SecurityContextPersistenceFilter;
 import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
 import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
 import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
 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_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_INVALID_SESSION_STRATEGY_REF = "invalid-session-strategy-ref";
 
 
 	private static final String ATT_DISABLE_URL_REWRITING = "disable-url-rewriting";
 	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
 		this.sessionPolicy = !StringUtils.hasText(createSession) ? SessionCreationPolicy.IF_REQUIRED
 				: createPolicy(createSession);
 				: createPolicy(createSession);
 		createCsrfFilter();
 		createCsrfFilter();
-		createSecurityContextRepository();
-		createSecurityContextPersistenceFilter();
+		createSecurityPersistence();
 		createSessionManagementFilters();
 		createSessionManagementFilters();
 		createWebAsyncManagerFilter();
 		createWebAsyncManagerFilter();
 		createRequestCacheFilter();
 		createRequestCacheFilter();
@@ -279,9 +281,27 @@ class HttpConfigurationBuilder {
 		return lowerCase ? path.toLowerCase() : path;
 		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() {
 	private void createSecurityContextPersistenceFilter() {
 		BeanDefinitionBuilder scpf = BeanDefinitionBuilder.rootBeanDefinition(SecurityContextPersistenceFilter.class);
 		BeanDefinitionBuilder scpf = BeanDefinitionBuilder.rootBeanDefinition(SecurityContextPersistenceFilter.class);
-		String disableUrlRewriting = this.httpElt.getAttribute(ATT_DISABLE_URL_REWRITING);
 		switch (this.sessionPolicy) {
 		switch (this.sessionPolicy) {
 		case ALWAYS:
 		case ALWAYS:
 			scpf.addPropertyValue("forceEagerSessionCreation", Boolean.TRUE);
 			scpf.addPropertyValue("forceEagerSessionCreation", Boolean.TRUE);
@@ -332,6 +352,12 @@ class HttpConfigurationBuilder {
 		this.contextRepoRef = new RuntimeBeanReference(repoRef);
 		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() {
 	private void createSessionManagementFilters() {
 		Element sessionMgmtElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.SESSION_MANAGEMENT);
 		Element sessionMgmtElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.SESSION_MANAGEMENT);
 		Element sessionCtrlElt = null;
 		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);
 		boolean forceAutoConfig = isDefaultHttpConfig(element);
 		HttpConfigurationBuilder httpBldr = new HttpConfigurationBuilder(element, forceAutoConfig, pc, portMapper,
 		HttpConfigurationBuilder httpBldr = new HttpConfigurationBuilder(element, forceAutoConfig, pc, portMapper,
 				portResolver, authenticationManager);
 				portResolver, authenticationManager);
+		httpBldr.getSecurityContextRepositoryForAuthenticationFilters();
 		AuthenticationConfigBuilder authBldr = new AuthenticationConfigBuilder(element, forceAutoConfig, pc,
 		AuthenticationConfigBuilder authBldr = new AuthenticationConfigBuilder(element, forceAutoConfig, pc,
 				httpBldr.getSessionCreationPolicy(), httpBldr.getRequestCache(), authenticationManager,
 				httpBldr.getSessionCreationPolicy(), httpBldr.getRequestCache(), authenticationManager,
-				httpBldr.getSessionStrategy(), portMapper, portResolver, httpBldr.getCsrfLogoutHandler());
+				httpBldr.getSecurityContextRepositoryForAuthenticationFilters(), httpBldr.getSessionStrategy(),
+				portMapper, portResolver, httpBldr.getCsrfLogoutHandler());
 		httpBldr.setLogoutHandlers(authBldr.getLogoutHandlers());
 		httpBldr.setLogoutHandlers(authBldr.getLogoutHandlers());
 		httpBldr.setEntryPoint(authBldr.getEntryPointBean());
 		httpBldr.setEntryPoint(authBldr.getEntryPointBean());
 		httpBldr.setAccessDeniedHandler(authBldr.getAccessDeniedHandlerBean());
 		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 authenticationManager;
 
 
+	private final BeanReference authenticationFilterSecurityContextRepositoryRef;
+
 	private BeanDefinition defaultAuthorizedClientRepository;
 	private BeanDefinition defaultAuthorizedClientRepository;
 
 
 	private BeanDefinition authorizationRequestRedirectFilter;
 	private BeanDefinition authorizationRequestRedirectFilter;
@@ -58,9 +60,11 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser {
 
 
 	private BeanDefinition authorizationCodeAuthenticationProvider;
 	private BeanDefinition authorizationCodeAuthenticationProvider;
 
 
-	OAuth2ClientBeanDefinitionParser(BeanReference requestCache, BeanReference authenticationManager) {
+	OAuth2ClientBeanDefinitionParser(BeanReference requestCache, BeanReference authenticationManager,
+			BeanReference authenticationFilterSecurityContextRepositoryRef) {
 		this.requestCache = requestCache;
 		this.requestCache = requestCache;
 		this.authenticationManager = authenticationManager;
 		this.authenticationManager = authenticationManager;
+		this.authenticationFilterSecurityContextRepositoryRef = authenticationFilterSecurityContextRepositoryRef;
 	}
 	}
 
 
 	@Override
 	@Override
@@ -92,11 +96,16 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser {
 		this.authorizationRequestRedirectFilter = authorizationRequestRedirectFilterBuilder
 		this.authorizationRequestRedirectFilter = authorizationRequestRedirectFilterBuilder
 				.addPropertyValue("authorizationRequestRepository", authorizationRequestRepository)
 				.addPropertyValue("authorizationRequestRepository", authorizationRequestRepository)
 				.addPropertyValue("requestCache", this.requestCache).getBeanDefinition();
 				.addPropertyValue("requestCache", this.requestCache).getBeanDefinition();
-		this.authorizationCodeGrantFilter = BeanDefinitionBuilder
+		BeanDefinitionBuilder authorizationCodeGrantFilterBldr = BeanDefinitionBuilder
 				.rootBeanDefinition(OAuth2AuthorizationCodeGrantFilter.class)
 				.rootBeanDefinition(OAuth2AuthorizationCodeGrantFilter.class)
 				.addConstructorArgValue(clientRegistrationRepository).addConstructorArgValue(authorizedClientRepository)
 				.addConstructorArgValue(clientRegistrationRepository).addConstructorArgValue(authorizedClientRepository)
 				.addConstructorArgValue(this.authenticationManager)
 				.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);
 		BeanMetadataElement accessTokenResponseClient = getAccessTokenResponseClient(authorizationCodeGrantElt);
 		this.authorizationCodeAuthenticationProvider = BeanDefinitionBuilder
 		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 authenticationManager;
 
 
+	private final BeanReference authenticationFilterSecurityContextRepositoryRef;
+
 	private final List<BeanReference> authenticationProviders;
 	private final List<BeanReference> authenticationProviders;
 
 
 	private final Map<BeanDefinition, BeanMetadataElement> entryPoints;
 	private final Map<BeanDefinition, BeanMetadataElement> entryPoints;
@@ -97,14 +99,15 @@ final class Saml2LoginBeanDefinitionParser implements BeanDefinitionParser {
 
 
 	Saml2LoginBeanDefinitionParser(List<BeanDefinition> csrfIgnoreRequestMatchers, BeanReference portMapper,
 	Saml2LoginBeanDefinitionParser(List<BeanDefinition> csrfIgnoreRequestMatchers, BeanReference portMapper,
 			BeanReference portResolver, BeanReference requestCache, boolean allowSessionCreation,
 			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.csrfIgnoreRequestMatchers = csrfIgnoreRequestMatchers;
 		this.portMapper = portMapper;
 		this.portMapper = portMapper;
 		this.portResolver = portResolver;
 		this.portResolver = portResolver;
 		this.requestCache = requestCache;
 		this.requestCache = requestCache;
 		this.allowSessionCreation = allowSessionCreation;
 		this.allowSessionCreation = allowSessionCreation;
 		this.authenticationManager = authenticationManager;
 		this.authenticationManager = authenticationManager;
+		this.authenticationFilterSecurityContextRepositoryRef = authenticationFilterSecurityContextRepositoryRef;
 		this.authenticationProviders = authenticationProviders;
 		this.authenticationProviders = authenticationProviders;
 		this.entryPoints = entryPoints;
 		this.entryPoints = entryPoints;
 	}
 	}
@@ -148,6 +151,7 @@ final class Saml2LoginBeanDefinitionParser implements BeanDefinitionParser {
 		resolveAuthenticationSuccessHandler(element, saml2WebSsoAuthenticationFilterBuilder);
 		resolveAuthenticationSuccessHandler(element, saml2WebSsoAuthenticationFilterBuilder);
 		resolveAuthenticationFailureHandler(element, saml2WebSsoAuthenticationFilterBuilder);
 		resolveAuthenticationFailureHandler(element, saml2WebSsoAuthenticationFilterBuilder);
 		resolveAuthenticationManager(element, saml2WebSsoAuthenticationFilterBuilder);
 		resolveAuthenticationManager(element, saml2WebSsoAuthenticationFilterBuilder);
+		resolveSecurityContextRepository(element, saml2WebSsoAuthenticationFilterBuilder);
 		// Configure the Saml2WebSsoAuthenticationRequestFilter
 		// Configure the Saml2WebSsoAuthenticationRequestFilter
 		this.saml2WebSsoAuthenticationRequestFilter = BeanDefinitionBuilder
 		this.saml2WebSsoAuthenticationRequestFilter = BeanDefinitionBuilder
 				.rootBeanDefinition(Saml2WebSsoAuthenticationRequestFilter.class)
 				.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) {
 	private void resolveLoginPage(Element element, ParserContext parserContext) {
 		String loginPage = element.getAttribute(ATT_LOGIN_PAGE);
 		String loginPage = element.getAttribute(ATT_LOGIN_PAGE);
 		Object source = parserContext.extractSource(element);
 		Object source = parserContext.extractSource(element);

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

@@ -333,6 +333,9 @@ http.attlist &=
 http.attlist &=
 http.attlist &=
 	## A reference to a SecurityContextRepository bean. This can be used to customize how the SecurityContext is stored between requests.
 	## 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}?
 	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 &=
 http.attlist &=
 	request-matcher?
 	request-matcher?
 http.attlist &=
 http.attlist &=

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

@@ -1237,6 +1237,13 @@
                 </xs:documentation>
                 </xs:documentation>
          </xs:annotation>
          </xs:annotation>
       </xs:attribute>
       </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:attribute name="request-matcher">
          <xs:annotation>
          <xs:annotation>
             <xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'mvc'
             <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;
 package org.springframework.security.config.annotation.web.configurers;
 
 
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.servlet.Filter;
 import javax.servlet.http.HttpSession;
 import javax.servlet.http.HttpSession;
 
 
 import org.junit.jupiter.api.Test;
 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.config.test.SpringTestContextExtension;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.userdetails.PasswordEncodedUser;
 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.HttpRequestResponseHolder;
+import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
 import org.springframework.security.web.context.NullSecurityContextRepository;
 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.SecurityContextPersistenceFilter;
 import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
 import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
@@ -110,6 +117,27 @@ public class SecurityContextConfigurerTests {
 		assertThat(session).isNull();
 		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
 	@EnableWebSecurity
 	static class ObjectPostProcessorConfig extends WebSecurityConfigurerAdapter {
 	static class ObjectPostProcessorConfig extends WebSecurityConfigurerAdapter {
 
 
@@ -241,14 +269,39 @@ public class SecurityContextConfigurerTests {
 	@EnableWebSecurity
 	@EnableWebSecurity
 	static class NullSecurityContextRepositoryInLambdaConfig extends WebSecurityConfigurerAdapter {
 	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
 		@Override
 		protected void configure(HttpSecurity http) throws Exception {
 		protected void configure(HttpSecurity http) throws Exception {
 			// @formatter:off
 			// @formatter:off
 			http
 			http
 				.formLogin(withDefaults())
 				.formLogin(withDefaults())
-				.securityContext((securityContext) ->
-					securityContext
-						.securityContextRepository(new NullSecurityContextRepository())
+				.securityContext((securityContext) -> securityContext
+					.requireExplicitSave(true)
 				);
 				);
 			// @formatter:on
 			// @formatter:on
 		}
 		}
@@ -258,7 +311,7 @@ public class SecurityContextConfigurerTests {
 			// @formatter:off
 			// @formatter:off
 			auth
 			auth
 				.inMemoryAuthentication()
 				.inMemoryAuthentication()
-					.withUser(PasswordEncodedUser.user());
+				.withUser(PasswordEncodedUser.user());
 			// @formatter:on
 			// @formatter:on
 		}
 		}
 
 

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

@@ -122,9 +122,11 @@ import static org.mockito.BDDMockito.willAnswer;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 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.csrf;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
 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.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.delete;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 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.post;
@@ -462,6 +464,37 @@ public class MiscHttpConfigTests {
 				any(HttpServletResponse.class));
 				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
 	@Test
 	public void getWhenUsingInterceptUrlExpressionsThenAuthorizesAccordingly() throws Exception {
 	public void getWhenUsingInterceptUrlExpressionsThenAuthorizesAccordingly() throws Exception {
 		this.spring.configLocations(xml("InterceptUrlExpressions")).autowire();
 		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>

二进制
docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.odg


二进制
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.
 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]]
 [[nsa-http-security-context-repository-ref]]
 * **security-context-repository-ref**
 * **security-context-repository-ref**
 Allows injection of a custom `SecurityContextRepository` into the `SecurityContextPersistenceFilter`.
 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.
 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.
 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
 == 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`].
 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.
 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.
 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>
+----
+====

+ 3 - 0
docs/modules/ROOT/pages/whats-new.adoc

@@ -3,3 +3,6 @@
 
 
 Spring Security 5.7 provides a number of new features.
 Spring Security 5.7 provides a number of new features.
 Below are the highlights of the release.
 Below are the highlights of the release.
+
+* xref:servlet/authentication/persistence.adoc#requestattributesecuritycontextrepository[`RequestAttributeSecurityContextRepository`]
+* xref:servlet/authentication/persistence.adoc#securitycontextholderfilter[`SecurityContextHolderFilter`] - Ability to require explicit saving of the `SecurityContext`.

+ 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.config.BeanIds;
 import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
 import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
 import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
 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.SecurityContextPersistenceFilter;
 import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.security.web.csrf.CsrfFilter;
 import org.springframework.security.web.csrf.CsrfFilter;
@@ -61,10 +62,14 @@ public abstract class WebTestUtils {
 	 */
 	 */
 	public static SecurityContextRepository getSecurityContextRepository(HttpServletRequest request) {
 	public static SecurityContextRepository getSecurityContextRepository(HttpServletRequest request) {
 		SecurityContextPersistenceFilter filter = findFilter(request, SecurityContextPersistenceFilter.class);
 		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 javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.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 javax.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 javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.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();
+	}
+
+}