Skip to content

Commit a218d3e

Browse files
committed
Use SecurityContextHolderStrategy for Async Requests
Issue gh-11060 Issue gh-11061
1 parent 5086409 commit a218d3e

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

+13-1
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

+1
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ private void createWebAsyncManagerFilter() {
587587
boolean asyncSupported = ClassUtils.hasMethod(ServletRequest.class, "startAsync");
588588
if (asyncSupported) {
589589
this.webAsyncManagerFilter = new RootBeanDefinition(WebAsyncManagerIntegrationFilter.class);
590+
this.webAsyncManagerFilter.getPropertyValues().add("securityContextHolderStrategy", this.holderStrategyRef);
590591
}
591592
}
592593

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

+23-3
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@
3535
import org.springframework.mock.web.MockHttpSession;
3636
import org.springframework.security.access.AccessDeniedException;
3737
import org.springframework.security.authentication.TestingAuthenticationToken;
38+
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
3839
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
3940
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
4041
import org.springframework.security.config.test.SpringTestContext;
4142
import org.springframework.security.config.test.SpringTestContextExtension;
42-
import org.springframework.security.core.context.SecurityContextHolder;
43+
import org.springframework.security.core.Authentication;
44+
import org.springframework.security.core.context.SecurityContextHolderStrategy;
4345
import org.springframework.security.core.userdetails.User;
4446
import org.springframework.security.core.userdetails.UserDetails;
4547
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -54,6 +56,8 @@
5456

5557
import static org.assertj.core.api.Assertions.assertThat;
5658
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
59+
import static org.mockito.Mockito.atLeastOnce;
60+
import static org.mockito.Mockito.verify;
5761
import static org.springframework.security.config.Customizer.withDefaults;
5862
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
5963
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
@@ -134,6 +138,22 @@ public void loadConfigWhenDefaultConfigThenWebAsyncManagerIntegrationFilterAdded
134138
// @formatter:on
135139
}
136140

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

245265
@GetMapping("/name")
246-
Callable<String> name() {
247-
return () -> SecurityContextHolder.getContext().getAuthentication().getName();
266+
Callable<String> name(Authentication authentication) {
267+
return () -> authentication.getName();
248268
}
249269

250270
}

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

+24
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;
@@ -127,12 +128,15 @@
127128
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
128129
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
129130
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
131+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
130132
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.x509;
133+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
131134
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
132135
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
133136
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
134137
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
135138
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
139+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
136140
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
137141

138142
/**
@@ -762,6 +766,21 @@ public void getWhenUsingCustomAccessDecisionManagerThenAuthorizesAccordingly() t
762766
// @formatter:on
763767
}
764768

769+
@Test
770+
public void asyncDispatchWhenCustomSecurityContextHolderStrategyThenUses() throws Exception {
771+
this.spring.configLocations(xml("WithSecurityContextHolderStrategy")).autowire();
772+
// @formatter:off
773+
MockHttpServletRequestBuilder requestWithBob = get("/name").with(user("Bob"));
774+
MvcResult mvcResult = this.mvc.perform(requestWithBob)
775+
.andExpect(request().asyncStarted())
776+
.andReturn();
777+
this.mvc.perform(asyncDispatch(mvcResult))
778+
.andExpect(status().isOk())
779+
.andExpect(content().string("Bob"));
780+
// @formatter:on
781+
verify(this.spring.getContext().getBean(SecurityContextHolderStrategy.class), atLeastOnce()).getContext();
782+
}
783+
765784
/**
766785
* SEC-1893
767786
*/
@@ -905,6 +924,11 @@ String details(Authentication authentication) {
905924
return authentication.getDetails().getClass().getName();
906925
}
907926

927+
@GetMapping("/name")
928+
Callable<String> name(Authentication authentication) {
929+
return () -> authentication.getName();
930+
}
931+
908932
}
909933

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

+19-4
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;
@@ -43,6 +44,9 @@ public final class SecurityContextCallableProcessingInterceptor implements Calla
4344

4445
private volatile SecurityContext securityContext;
4546

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

7478
@Override
7579
public <T> void preProcess(NativeWebRequest request, Callable<T> task) {
76-
SecurityContextHolder.setContext(this.securityContext);
80+
this.securityContextHolderStrategy.setContext(this.securityContext);
7781
}
7882

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

8499
private void setSecurityContext(SecurityContext securityContext) {

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

+21-3
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 jakarta.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)