Add SecurityContextHolderFilter

Closes gh-9635
This commit is contained in:
Rob Winch 2022-02-18 15:14:34 -06:00
parent dbcb5004b4
commit 87ed31a99c
22 changed files with 583 additions and 46 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.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());

View File

@ -38,6 +38,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;
@ -146,6 +147,11 @@ public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecur
return getSelf();
}
public T securityContextRepository(SecurityContextRepository securityContextRepository) {
this.authFilter.setSecurityContextRepository(securityContextRepository);
return getSelf();
}
/**
* Create the {@link RequestMatcher} given a loginProcessingUrl
* @param loginProcessingUrl creates the {@link RequestMatcher} based upon the
@ -287,6 +293,12 @@ public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecur
if (rememberMeServices != null) {
this.authFilter.setRememberMeServices(rememberMeServices);
}
SecurityContextConfigurer securityContextConfigurer = http.getConfigurer(SecurityContextConfigurer.class);
if (securityContextConfigurer != null && securityContextConfigurer.isRequireExplicitSave()) {
SecurityContextRepository securityContextRepository = securityContextConfigurer
.getSecurityContextRepository();
this.authFilter.setSecurityContextRepository(securityContextRepository);
}
F filter = postProcess(this.authFilter);
http.addFilter(filter);
}

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.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.security.web.context.SecurityContextRepository;
@ -62,6 +63,8 @@ import org.springframework.security.web.context.SecurityContextRepository;
public final class SecurityContextConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<SecurityContextConfigurer<H>, H> {
private boolean requireExplicitSave;
/**
* Creates a new instance
* @see HttpSecurity#securityContext()
@ -79,23 +82,45 @@ public final class SecurityContextConfigurer<H extends HttpSecurityBuilder<H>>
return this;
}
@Override
@SuppressWarnings("unchecked")
public void configure(H http) {
SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
public SecurityContextConfigurer<H> requireExplicitSave(boolean requireExplicitSave) {
this.requireExplicitSave = requireExplicitSave;
return this;
}
boolean isRequireExplicitSave() {
return this.requireExplicitSave;
}
SecurityContextRepository getSecurityContextRepository() {
SecurityContextRepository securityContextRepository = getBuilder()
.getSharedObject(SecurityContextRepository.class);
if (securityContextRepository == null) {
securityContextRepository = new HttpSessionSecurityContextRepository();
}
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);
}
}

View File

@ -236,8 +236,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;
@ -251,10 +251,12 @@ final class AuthenticationConfigBuilder {
createRememberMeFilter(authenticationManager);
createBasicFilter(authenticationManager);
createBearerTokenAuthenticationFilter(authenticationManager);
createFormLoginFilter(sessionStrategy, authenticationManager);
createOAuth2ClientFilters(sessionStrategy, requestCache, authenticationManager);
createOpenIDLoginFilter(sessionStrategy, authenticationManager);
createSaml2LoginFilter(authenticationManager);
createFormLoginFilter(sessionStrategy, authenticationManager, authenticationFilterSecurityContextRepositoryRef);
createOAuth2ClientFilters(sessionStrategy, requestCache, authenticationManager,
authenticationFilterSecurityContextRepositoryRef);
createOpenIDLoginFilter(sessionStrategy, authenticationManager,
authenticationFilterSecurityContextRepositoryRef);
createSaml2LoginFilter(authenticationManager, authenticationFilterSecurityContextRepositoryRef);
createX509Filter(authenticationManager);
createJeeFilter(authenticationManager);
createLogoutFilter();
@ -290,7 +292,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) {
@ -306,6 +309,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));
@ -314,13 +321,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;
@ -332,6 +341,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();
@ -361,14 +374,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);
@ -413,7 +427,8 @@ final class AuthenticationConfigBuilder {
}
}
void createOpenIDLoginFilter(BeanReference sessionStrategy, BeanReference authManager) {
void createOpenIDLoginFilter(BeanReference sessionStrategy, BeanReference authManager,
BeanReference authenticationFilterSecurityContextRepositoryRef) {
Element openIDLoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OPENID_LOGIN);
RootBeanDefinition openIDFilter = null;
if (openIDLoginElt != null) {
@ -422,6 +437,10 @@ final class AuthenticationConfigBuilder {
if (openIDFilter != null) {
openIDFilter.getPropertyValues().addPropertyValue("allowSessionCreation", this.allowSessionCreation);
openIDFilter.getPropertyValues().addPropertyValue("authenticationManager", authManager);
if (authenticationFilterSecurityContextRepositoryRef != null) {
openIDFilter.getPropertyValues().addPropertyValue("securityContextRepository",
authenticationFilterSecurityContextRepositoryRef);
}
// Required by login page filter
this.openIDFilterId = this.pc.getReaderContext().generateBeanName(openIDFilter);
this.pc.registerBeanComponent(new BeanComponentDefinition(openIDFilter, this.openIDFilterId));
@ -430,14 +449,16 @@ final class AuthenticationConfigBuilder {
}
}
private void createSaml2LoginFilter(BeanReference authenticationManager) {
private void createSaml2LoginFilter(BeanReference authenticationManager,
BeanReference authenticationFilterSecurityContextRepositoryRef) {
Element saml2LoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.SAML2_LOGIN);
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();

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.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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -122,9 +122,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;
@ -462,6 +464,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();

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

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

View File

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

View File

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

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

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