Skip to content

Commit 27de315

Browse files
committed
Use SecurityContextHolderStrategy for Async Requests
Issue gh-11060 Issue gh-11061
1 parent 135e602 commit 27de315

File tree

7 files changed

+152
-11
lines changed

7 files changed

+152
-11
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
3434
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
3535
import org.springframework.security.config.annotation.web.configurers.DefaultLoginPageConfigurer;
36+
import org.springframework.security.core.context.SecurityContextHolder;
37+
import org.springframework.security.core.context.SecurityContextHolderStrategy;
3638
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
3739

3840
import static org.springframework.security.config.Customizer.withDefaults;
@@ -58,6 +60,9 @@ class HttpSecurityConfiguration {
5860

5961
private ApplicationContext context;
6062

63+
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
64+
.getContextHolderStrategy();
65+
6166
@Autowired
6267
void setObjectPostProcessor(ObjectPostProcessor<Object> objectPostProcessor) {
6368
this.objectPostProcessor = objectPostProcessor;
@@ -77,6 +82,11 @@ void setApplicationContext(ApplicationContext context) {
7782
this.context = context;
7883
}
7984

85+
@Autowired(required = false)
86+
void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
87+
this.securityContextHolderStrategy = securityContextHolderStrategy;
88+
}
89+
8090
@Bean(HTTPSECURITY_BEAN_NAME)
8191
@Scope("prototype")
8292
HttpSecurity httpSecurity() throws Exception {
@@ -86,10 +96,12 @@ HttpSecurity httpSecurity() throws Exception {
8696
this.objectPostProcessor, passwordEncoder);
8797
authenticationBuilder.parentAuthenticationManager(authenticationManager());
8898
HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects());
99+
WebAsyncManagerIntegrationFilter webAsyncManagerIntegrationFilter = new WebAsyncManagerIntegrationFilter();
100+
webAsyncManagerIntegrationFilter.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
89101
// @formatter:off
90102
http
91103
.csrf(withDefaults())
92-
.addFilter(new WebAsyncManagerIntegrationFilter())
104+
.addFilter(webAsyncManagerIntegrationFilter)
93105
.exceptionHandling(withDefaults())
94106
.headers(withDefaults())
95107
.sessionManagement(withDefaults())

config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,7 @@ private void createWebAsyncManagerFilter() {
588588
boolean asyncSupported = ClassUtils.hasMethod(ServletRequest.class, "startAsync");
589589
if (asyncSupported) {
590590
this.webAsyncManagerFilter = new RootBeanDefinition(WebAsyncManagerIntegrationFilter.class);
591+
this.webAsyncManagerFilter.getPropertyValues().add("securityContextHolderStrategy", this.holderStrategyRef);
591592
}
592593
}
593594

config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@
3636
import org.springframework.mock.web.MockHttpSession;
3737
import org.springframework.security.access.AccessDeniedException;
3838
import org.springframework.security.authentication.TestingAuthenticationToken;
39+
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
3940
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
4041
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
4142
import org.springframework.security.config.test.SpringTestContext;
4243
import org.springframework.security.config.test.SpringTestContextExtension;
43-
import org.springframework.security.core.context.SecurityContextHolder;
44+
import org.springframework.security.core.Authentication;
45+
import org.springframework.security.core.context.SecurityContextHolderStrategy;
4446
import org.springframework.security.core.userdetails.User;
4547
import org.springframework.security.core.userdetails.UserDetails;
4648
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -55,6 +57,8 @@
5557

5658
import static org.assertj.core.api.Assertions.assertThat;
5759
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
60+
import static org.mockito.Mockito.atLeastOnce;
61+
import static org.mockito.Mockito.verify;
5862
import static org.springframework.security.config.Customizer.withDefaults;
5963
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
6064
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
@@ -135,6 +139,22 @@ public void loadConfigWhenDefaultConfigThenWebAsyncManagerIntegrationFilterAdded
135139
// @formatter:on
136140
}
137141

142+
@Test
143+
public void asyncDispatchWhenCustomSecurityContextHolderStrategyThenUses() throws Exception {
144+
this.spring.register(DefaultWithFilterChainConfig.class, SecurityContextChangedListenerConfig.class,
145+
NameController.class).autowire();
146+
// @formatter:off
147+
MockHttpServletRequestBuilder requestWithBob = get("/name").with(user("Bob"));
148+
MvcResult mvcResult = this.mockMvc.perform(requestWithBob)
149+
.andExpect(request().asyncStarted())
150+
.andReturn();
151+
this.mockMvc.perform(asyncDispatch(mvcResult))
152+
.andExpect(status().isOk())
153+
.andExpect(content().string("Bob"));
154+
// @formatter:on
155+
verify(this.spring.getContext().getBean(SecurityContextHolderStrategy.class), atLeastOnce()).getContext();
156+
}
157+
138158
@Test
139159
public void getWhenDefaultFilterChainBeanThenAnonymousPermitted() throws Exception {
140160
this.spring.register(AuthorizeRequestsConfig.class, UserDetailsConfig.class, BaseController.class).autowire();
@@ -244,8 +264,8 @@ public void configureWhenDefaultConfigurerAsSpringFactoryThenDefaultConfigurerAp
244264
static class NameController {
245265

246266
@GetMapping("/name")
247-
Callable<String> name() {
248-
return () -> SecurityContextHolder.getContext().getAuthentication().getName();
267+
Callable<String> name(Authentication authentication) {
268+
return () -> authentication.getName();
249269
}
250270

251271
}

config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.Iterator;
2828
import java.util.List;
2929
import java.util.Map;
30+
import java.util.concurrent.Callable;
3031
import java.util.stream.Collectors;
3132

3233
import javax.security.auth.Subject;
@@ -129,12 +130,15 @@
129130
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
130131
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
131132
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
133+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
132134
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.x509;
135+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
133136
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
134137
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
135138
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
136139
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
137140
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
141+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
138142
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
139143

140144
/**
@@ -766,6 +770,21 @@ public void getWhenUsingCustomAccessDecisionManagerThenAuthorizesAccordingly() t
766770
// @formatter:on
767771
}
768772

773+
@Test
774+
public void asyncDispatchWhenCustomSecurityContextHolderStrategyThenUses() throws Exception {
775+
this.spring.configLocations(xml("WithSecurityContextHolderStrategy")).autowire();
776+
// @formatter:off
777+
MockHttpServletRequestBuilder requestWithBob = get("/name").with(user("Bob"));
778+
MvcResult mvcResult = this.mvc.perform(requestWithBob)
779+
.andExpect(request().asyncStarted())
780+
.andReturn();
781+
this.mvc.perform(asyncDispatch(mvcResult))
782+
.andExpect(status().isOk())
783+
.andExpect(content().string("Bob"));
784+
// @formatter:on
785+
verify(this.spring.getContext().getBean(SecurityContextHolderStrategy.class), atLeastOnce()).getContext();
786+
}
787+
769788
/**
770789
* SEC-1893
771790
*/
@@ -909,6 +928,11 @@ String details(Authentication authentication) {
909928
return authentication.getDetails().getClass().getName();
910929
}
911930

931+
@GetMapping("/name")
932+
Callable<String> name(Authentication authentication) {
933+
return () -> authentication.getName();
934+
}
935+
912936
}
913937

914938
@RestController
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Copyright 2002-2018 the original author or authors.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ https://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
18+
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
19+
xmlns:mvc="http://www.springframework.org/schema/mvc"
20+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
21+
xmlns="http://www.springframework.org/schema/security"
22+
xsi:schemaLocation="
23+
http://www.springframework.org/schema/security
24+
https://www.springframework.org/schema/security/spring-security.xsd
25+
http://www.springframework.org/schema/beans
26+
https://www.springframework.org/schema/beans/spring-beans.xsd
27+
http://www.springframework.org/schema/mvc
28+
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
29+
30+
<http auto-config="true" security-context-holder-strategy-ref="ref">
31+
<intercept-url pattern="/**" access="authenticated"/>
32+
</http>
33+
34+
<b:bean id="ref" class="org.mockito.Mockito" factory-method="spy">
35+
<b:constructor-arg>
36+
<b:bean class="org.springframework.security.config.MockSecurityContextHolderStrategy"/>
37+
</b:constructor-arg>
38+
</b:bean>
39+
40+
<mvc:annotation-driven>
41+
<mvc:argument-resolvers>
42+
<b:bean class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver">
43+
<b:property name="securityContextHolderStrategy" ref="ref"/>
44+
</b:bean>
45+
</mvc:argument-resolvers>
46+
</mvc:annotation-driven>
47+
48+
<b:bean class="org.springframework.security.config.http.MiscHttpConfigTests.AuthenticationController"/>
49+
50+
<b:import resource="userservice.xml"/>
51+
</b:beans>

web/src/main/java/org/springframework/security/web/context/request/async/SecurityContextCallableProcessingInterceptor.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020

2121
import org.springframework.security.core.context.SecurityContext;
2222
import org.springframework.security.core.context.SecurityContextHolder;
23+
import org.springframework.security.core.context.SecurityContextHolderStrategy;
2324
import org.springframework.util.Assert;
2425
import org.springframework.web.context.request.NativeWebRequest;
2526
import org.springframework.web.context.request.async.CallableProcessingInterceptor;
@@ -44,6 +45,9 @@ public final class SecurityContextCallableProcessingInterceptor extends Callable
4445

4546
private volatile SecurityContext securityContext;
4647

48+
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
49+
.getContextHolderStrategy();
50+
4751
/**
4852
* Create a new {@link SecurityContextCallableProcessingInterceptor} that uses the
4953
* {@link SecurityContext} from the {@link SecurityContextHolder} at the time
@@ -68,18 +72,29 @@ public SecurityContextCallableProcessingInterceptor(SecurityContext securityCont
6872
@Override
6973
public <T> void beforeConcurrentHandling(NativeWebRequest request, Callable<T> task) {
7074
if (this.securityContext == null) {
71-
setSecurityContext(SecurityContextHolder.getContext());
75+
setSecurityContext(this.securityContextHolderStrategy.getContext());
7276
}
7377
}
7478

7579
@Override
7680
public <T> void preProcess(NativeWebRequest request, Callable<T> task) {
77-
SecurityContextHolder.setContext(this.securityContext);
81+
this.securityContextHolderStrategy.setContext(this.securityContext);
7882
}
7983

8084
@Override
8185
public <T> void postProcess(NativeWebRequest request, Callable<T> task, Object concurrentResult) {
82-
SecurityContextHolder.clearContext();
86+
this.securityContextHolderStrategy.clearContext();
87+
}
88+
89+
/**
90+
* Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
91+
* the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
92+
*
93+
* @since 5.8
94+
*/
95+
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
96+
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
97+
this.securityContextHolderStrategy = securityContextHolderStrategy;
8398
}
8499

85100
private void setSecurityContext(SecurityContext securityContext) {

web/src/main/java/org/springframework/security/web/context/request/async/WebAsyncManagerIntegrationFilter.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,6 +25,9 @@
2525
import javax.servlet.http.HttpServletResponse;
2626

2727
import org.springframework.security.core.context.SecurityContext;
28+
import org.springframework.security.core.context.SecurityContextHolder;
29+
import org.springframework.security.core.context.SecurityContextHolderStrategy;
30+
import org.springframework.util.Assert;
2831
import org.springframework.web.context.request.async.WebAsyncManager;
2932
import org.springframework.web.context.request.async.WebAsyncUtils;
3033
import org.springframework.web.filter.OncePerRequestFilter;
@@ -42,17 +45,32 @@ public final class WebAsyncManagerIntegrationFilter extends OncePerRequestFilter
4245

4346
private static final Object CALLABLE_INTERCEPTOR_KEY = new Object();
4447

48+
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
49+
.getContextHolderStrategy();
50+
4551
@Override
4652
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
4753
throws ServletException, IOException {
4854
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
4955
SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager
5056
.getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
5157
if (securityProcessingInterceptor == null) {
52-
asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY,
53-
new SecurityContextCallableProcessingInterceptor());
58+
SecurityContextCallableProcessingInterceptor interceptor = new SecurityContextCallableProcessingInterceptor();
59+
interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
60+
asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY, interceptor);
5461
}
5562
filterChain.doFilter(request, response);
5663
}
5764

65+
/**
66+
* Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
67+
* the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
68+
*
69+
* @since 5.8
70+
*/
71+
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
72+
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
73+
this.securityContextHolderStrategy = securityContextHolderStrategy;
74+
}
75+
5876
}

0 commit comments

Comments
 (0)