diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java index f00cecd394..ce955df3e3 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java +++ b/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()); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java index 507f7b85bf..cf1d3ab7a1 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java +++ b/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> extends AbstractHttpConfigurer, H> { + private boolean requireExplicitSave; + /** * Creates a new instance * @see HttpSecurity#securityContext() @@ -79,23 +82,45 @@ public final class SecurityContextConfigurer> return this; } - @Override - @SuppressWarnings("unchecked") - public void configure(H http) { - SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class); + public SecurityContextConfigurer 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(); } - 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); + return securityContextRepository; + } + + @Override + @SuppressWarnings("unchecked") + public void configure(H http) { + SecurityContextRepository securityContextRepository = getSecurityContextRepository(); + if (this.requireExplicitSave) { + SecurityContextHolderFilter securityContextHolderFilter = postProcess( + new SecurityContextHolderFilter(securityContextRepository)); + http.addFilter(securityContextHolderFilter); + } + 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); } } diff --git a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java index 7c04067173..6a49b4a46f 100644 --- a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java +++ b/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(); diff --git a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java index 74d12d0e94..4c859c000a 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java +++ b/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; diff --git a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java index 970245d134..7d0be016ce 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java +++ b/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()); diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java index 3a72f62585..f2c1ebd0f0 100644 --- a/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java +++ b/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 diff --git a/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParser.java index 53a2b0946b..147166c471 100644 --- a/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParser.java +++ b/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 authenticationProviders; private final Map entryPoints; @@ -97,14 +99,15 @@ final class Saml2LoginBeanDefinitionParser implements BeanDefinitionParser { Saml2LoginBeanDefinitionParser(List csrfIgnoreRequestMatchers, BeanReference portMapper, BeanReference portResolver, BeanReference requestCache, boolean allowSessionCreation, - BeanReference authenticationManager, List authenticationProviders, - Map entryPoints) { + BeanReference authenticationManager, BeanReference authenticationFilterSecurityContextRepositoryRef, + List authenticationProviders, Map 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); diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-6.0.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-6.0.rnc index d2568fb19d..b54f8b7d43 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-6.0.rnc +++ b/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 &= diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-6.0.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-6.0.xsd index 5af38a04f7..9c6937292f 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-6.0.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-6.0.xsd @@ -1215,6 +1215,13 @@ + + + Optional attribute that specifies that the SecurityContext should require explicit saving + rather than being synchronized from the SecurityContextHolder. Defaults to "false". + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java index f1faf2f76e..d675f00e9b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java +++ b/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> 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 } diff --git a/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java b/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java index 63d7f0886d..5b9255996a 100644 --- a/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java +++ b/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(); diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSave.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSave.xml new file mode 100644 index 0000000000..381fff8dca --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSave.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSaveAndExplicitRepository.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSaveAndExplicitRepository.xml new file mode 100644 index 0000000000..4406065ff6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSaveAndExplicitRepository.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.odg b/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.odg new file mode 100644 index 0000000000..95247c4ba9 Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.odg differ diff --git a/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.png b/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.png new file mode 100644 index 0000000000..25159f0c45 Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authentication/securitycontextholderfilter.png differ diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index a966491c52..3549b9b627 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/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`. diff --git a/docs/modules/ROOT/pages/servlet/authentication/persistence.adoc b/docs/modules/ROOT/pages/servlet/authentication/persistence.adoc index 044b08ee69..6d88332634 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/persistence.adoc +++ b/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"] +---- + + + + +---- +==== + + +[[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. \ No newline at end of file +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"] +---- + + + +---- +==== \ No newline at end of file diff --git a/test/src/main/java/org/springframework/security/test/web/support/WebTestUtils.java b/test/src/main/java/org/springframework/security/test/web/support/WebTestUtils.java index 8fe7a31ffa..98519935a0 100644 --- a/test/src/main/java/org/springframework/security/test/web/support/WebTestUtils.java +++ b/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"); } - return (SecurityContextRepository) ReflectionTestUtils.getField(filter, "repo"); + SecurityContextHolderFilter holderFilter = findFilter(request, SecurityContextHolderFilter.class); + if (holderFilter != null) { + return (SecurityContextRepository) ReflectionTestUtils.getField(holderFilter, "securityContextRepository"); + } + return DEFAULT_CONTEXT_REPO; } /** diff --git a/web/src/main/java/org/springframework/security/web/context/SecurityContextHolderFilter.java b/web/src/main/java/org/springframework/security/web/context/SecurityContextHolderFilter.java new file mode 100644 index 0000000000..4aba0d3bf9 --- /dev/null +++ b/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; + } + +} diff --git a/web/src/test/java/org/springframework/security/web/context/SecurityContextHolderFilterTests.java b/web/src/test/java/org/springframework/security/web/context/SecurityContextHolderFilterTests.java new file mode 100644 index 0000000000..b309cab5e9 --- /dev/null +++ b/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 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(); + } + +}