Add SecurityContextHolderFilter

Closes gh-9635
This commit is contained in:
Rob Winch 2022-02-18 15:14:34 -06:00
parent f9619cef68
commit 972039e65c
21 changed files with 572 additions and 44 deletions

View File

@ -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());

View File

@ -37,6 +37,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;
@ -144,6 +145,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
@ -285,6 +291,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);
} }

View File

@ -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;
} }
@Override public SecurityContextConfigurer<H> requireExplicitSave(boolean requireExplicitSave) {
@SuppressWarnings("unchecked") this.requireExplicitSave = requireExplicitSave;
public void configure(H http) { return this;
SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class); }
boolean isRequireExplicitSave() {
return this.requireExplicitSave;
}
SecurityContextRepository getSecurityContextRepository() {
SecurityContextRepository securityContextRepository = getBuilder()
.getSharedObject(SecurityContextRepository.class);
if (securityContextRepository == null) { if (securityContextRepository == null) {
securityContextRepository = new HttpSessionSecurityContextRepository(); securityContextRepository = new HttpSessionSecurityContextRepository();
} }
SecurityContextPersistenceFilter securityContextFilter = new SecurityContextPersistenceFilter( return securityContextRepository;
securityContextRepository); }
SessionManagementConfigurer<?> sessionManagement = http.getConfigurer(SessionManagementConfigurer.class);
SessionCreationPolicy sessionCreationPolicy = (sessionManagement != null) @Override
? sessionManagement.getSessionCreationPolicy() : null; @SuppressWarnings("unchecked")
if (SessionCreationPolicy.ALWAYS == sessionCreationPolicy) { public void configure(H http) {
securityContextFilter.setForceEagerSessionCreation(true); 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);
} }
} }

View File

@ -216,8 +216,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, BeanReference authenticationFilterSecurityContextRepositoryRef, BeanReference sessionStrategy,
BeanMetadataElement csrfLogoutHandler) { BeanReference portMapper, BeanReference portResolver, BeanMetadataElement csrfLogoutHandler) {
this.httpElt = element; this.httpElt = element;
this.pc = pc; this.pc = pc;
this.requestCache = requestCache; this.requestCache = requestCache;
@ -231,9 +231,10 @@ final class AuthenticationConfigBuilder {
createRememberMeFilter(authenticationManager); createRememberMeFilter(authenticationManager);
createBasicFilter(authenticationManager); createBasicFilter(authenticationManager);
createBearerTokenAuthenticationFilter(authenticationManager); createBearerTokenAuthenticationFilter(authenticationManager);
createFormLoginFilter(sessionStrategy, authenticationManager); createFormLoginFilter(sessionStrategy, authenticationManager, authenticationFilterSecurityContextRepositoryRef);
createOAuth2ClientFilters(sessionStrategy, requestCache, authenticationManager); createOAuth2ClientFilters(sessionStrategy, requestCache, authenticationManager,
createSaml2LoginFilter(authenticationManager); authenticationFilterSecurityContextRepositoryRef);
createSaml2LoginFilter(authenticationManager, authenticationFilterSecurityContextRepositoryRef);
createX509Filter(authenticationManager); createX509Filter(authenticationManager);
createJeeFilter(authenticationManager); createJeeFilter(authenticationManager);
createLogoutFilter(); createLogoutFilter();
@ -269,7 +270,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) {
@ -285,6 +287,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));
@ -293,13 +299,15 @@ final class AuthenticationConfigBuilder {
} }
void createOAuth2ClientFilters(BeanReference sessionStrategy, BeanReference requestCache, void createOAuth2ClientFilters(BeanReference sessionStrategy, BeanReference requestCache,
BeanReference authenticationManager) { BeanReference authenticationManager, BeanReference authenticationFilterSecurityContextRepositoryRef) {
createOAuth2LoginFilter(sessionStrategy, authenticationManager); createOAuth2LoginFilter(sessionStrategy, authenticationManager,
createOAuth2ClientFilter(requestCache, 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;
@ -311,6 +319,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();
@ -340,14 +352,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);
@ -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); 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();

View File

@ -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(); createSecurityPersistence();
createSecurityContextPersistenceFilter();
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;

View File

@ -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());

View File

@ -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

View File

@ -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, BeanReference authenticationManager, BeanReference authenticationFilterSecurityContextRepositoryRef,
Map<BeanDefinition, BeanMetadataElement> entryPoints) { 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);

View File

@ -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 &=

View File

@ -1215,6 +1215,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'

View File

@ -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 jakarta.servlet.Filter;
import jakarta.servlet.http.HttpSession; import jakarta.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((securityContext) -> securityContext
securityContext .requireExplicitSave(true)
.securityContextRepository(new NullSecurityContextRepository())
); );
// @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
} }

View File

@ -121,9 +121,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;
@ -461,6 +463,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();

View File

@ -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>

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@ -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`.

View File

@ -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>
----
====

View File

@ -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) { if (filter != null) {
return DEFAULT_CONTEXT_REPO; 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;
} }
/** /**

View File

@ -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;
}
}

View File

@ -0,0 +1,95 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.context;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.TestAuthentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
class SecurityContextHolderFilterTests {
@Mock
private SecurityContextRepository repository;
@Mock
private HttpServletRequest request;
@Mock
private HttpServletResponse response;
@Mock
private FilterChain chain;
@Captor
private ArgumentCaptor<HttpRequestResponseHolder> requestResponse;
private SecurityContextHolderFilter filter;
@BeforeEach
void setup() {
this.filter = new SecurityContextHolderFilter(this.repository);
}
@AfterEach
void cleanup() {
SecurityContextHolder.clearContext();
}
@Test
void doFilterThenSetsAndClearsSecurityContextHolder() throws Exception {
Authentication authentication = TestAuthentication.authenticatedUser();
SecurityContext expectedContext = new SecurityContextImpl(authentication);
given(this.repository.loadContext(this.requestResponse.capture())).willReturn(expectedContext);
FilterChain filterChain = (request, response) -> assertThat(SecurityContextHolder.getContext())
.isEqualTo(expectedContext);
this.filter.doFilter(this.request, this.response, filterChain);
assertThat(SecurityContextHolder.getContext()).isEqualTo(SecurityContextHolder.createEmptyContext());
}
@Test
void shouldNotFilterErrorDispatchWhenDefault() {
assertThat(this.filter.shouldNotFilterErrorDispatch()).isFalse();
}
@Test
void shouldNotFilterErrorDispatchWhenOverridden() {
this.filter.setShouldNotFilterErrorDispatch(true);
assertThat(this.filter.shouldNotFilterErrorDispatch()).isTrue();
}
}