Skip to content

Commit ed16c86

Browse files
kse-musicjzheaux
authored andcommitted
Improve @CurrentSecurityContext meta-annotations
Closes gh-15551
1 parent 079b5b9 commit ed16c86

File tree

10 files changed

+332
-63
lines changed

10 files changed

+332
-63
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentRes
9898
CurrentSecurityContextArgumentResolver currentSecurityContextArgumentResolver = new CurrentSecurityContextArgumentResolver();
9999
currentSecurityContextArgumentResolver.setBeanResolver(this.beanResolver);
100100
currentSecurityContextArgumentResolver.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
101+
currentSecurityContextArgumentResolver.setTemplateDefaults(this.templateDefaults);
101102
argumentResolvers.add(currentSecurityContextArgumentResolver);
102103
argumentResolvers.add(new CsrfTokenArgumentResolver());
103104
}

config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,14 @@ void setCompromisedPasswordChecker(ReactiveCompromisedPasswordChecker compromise
109109

110110
@Bean
111111
static WebFluxConfigurer authenticationPrincipalArgumentResolverConfigurer(
112-
ObjectProvider<AuthenticationPrincipalArgumentResolver> authenticationPrincipalArgumentResolver) {
112+
ObjectProvider<AuthenticationPrincipalArgumentResolver> authenticationPrincipalArgumentResolver,
113+
ObjectProvider<CurrentSecurityContextArgumentResolver> currentSecurityContextArgumentResolvers) {
113114
return new WebFluxConfigurer() {
114115

115116
@Override
116117
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
117-
configurer.addCustomResolver(authenticationPrincipalArgumentResolver.getObject());
118+
configurer.addCustomResolver(authenticationPrincipalArgumentResolver.getObject(),
119+
currentSecurityContextArgumentResolvers.getObject());
118120
}
119121

120122
};
@@ -133,12 +135,14 @@ AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver(
133135
}
134136

135137
@Bean
136-
CurrentSecurityContextArgumentResolver reactiveCurrentSecurityContextArgumentResolver() {
138+
CurrentSecurityContextArgumentResolver reactiveCurrentSecurityContextArgumentResolver(
139+
ObjectProvider<AnnotationTemplateExpressionDefaults> templateDefaults) {
137140
CurrentSecurityContextArgumentResolver resolver = new CurrentSecurityContextArgumentResolver(
138141
this.adapterRegistry);
139142
if (this.beanFactory != null) {
140143
resolver.setBeanResolver(new BeanFactoryResolver(this.beanFactory));
141144
}
145+
templateDefaults.ifAvailable(resolver::setTemplateDefaults);
142146
return resolver;
143147
}
144148

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.security.core.Authentication;
3434
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
3535
import org.springframework.security.core.annotation.AuthenticationPrincipal;
36+
import org.springframework.security.core.annotation.CurrentSecurityContext;
3637
import org.springframework.security.core.authority.AuthorityUtils;
3738
import org.springframework.security.core.context.SecurityContextHolder;
3839
import org.springframework.security.web.csrf.CsrfToken;
@@ -115,6 +116,15 @@ public void metaAnnotationWhenTemplateDefaultsBeanThenResolvesExpression() throw
115116
this.mockMvc.perform(get("/hi")).andExpect(content().string("Hi, Harold!"));
116117
}
117118

119+
@Test
120+
public void resolveMetaAnnotationWhenTemplateDefaultsBeanThenResolvesExpression() throws Exception {
121+
this.mockMvc.perform(get("/hello")).andExpect(content().string("user"));
122+
Authentication harold = new TestingAuthenticationToken("harold", "password",
123+
AuthorityUtils.createAuthorityList("ROLE_USER"));
124+
SecurityContextHolder.getContext().setAuthentication(harold);
125+
this.mockMvc.perform(get("/hello")).andExpect(content().string("harold"));
126+
}
127+
118128
private ResultMatcher assertResult(Object expected) {
119129
return model().attribute("result", expected);
120130
}
@@ -128,6 +138,15 @@ private ResultMatcher assertResult(Object expected) {
128138

129139
}
130140

141+
@Target({ ElementType.PARAMETER })
142+
@Retention(RetentionPolicy.RUNTIME)
143+
@CurrentSecurityContext(expression = "authentication.{property}")
144+
@interface CurrentAuthenticationProperty {
145+
146+
String property();
147+
148+
}
149+
131150
@Controller
132151
static class TestController {
133152

@@ -158,6 +177,13 @@ String ifUser(@IsUser("harold") boolean isHarold) {
158177
}
159178
}
160179

180+
@GetMapping("/hello")
181+
@ResponseBody
182+
String getCurrentAuthenticationProperty(
183+
@CurrentAuthenticationProperty(property = "principal") String principal) {
184+
return principal;
185+
}
186+
161187
}
162188

163189
@Configuration

config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.springframework.security.core.Authentication;
4343
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
4444
import org.springframework.security.core.annotation.AuthenticationPrincipal;
45+
import org.springframework.security.core.annotation.CurrentSecurityContext;
4546
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
4647
import org.springframework.security.core.userdetails.PasswordEncodedUser;
4748
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
@@ -183,6 +184,27 @@ public void metaAnnotationWhenTemplateDefaultsBeanThenResolvesExpression() throw
183184
.isEqualTo("Hi, Harold!");
184185
}
185186

187+
@Test
188+
public void resoleMetaAnnotationWhenTemplateDefaultsBeanThenResolvesExpression() throws Exception {
189+
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
190+
Authentication user = new TestingAuthenticationToken("user", "password", "ROLE_USER");
191+
this.webClient.mutateWith(mockAuthentication(user))
192+
.get()
193+
.uri("/hello")
194+
.exchange()
195+
.expectStatus()
196+
.isOk()
197+
.expectBody(String.class)
198+
.isEqualTo("user");
199+
Authentication harold = new TestingAuthenticationToken("harold", "password", "ROLE_USER");
200+
this.webClient.mutateWith(mockAuthentication(harold))
201+
.get()
202+
.uri("/hello")
203+
.exchange()
204+
.expectBody(String.class)
205+
.isEqualTo("harold");
206+
}
207+
186208
@Configuration
187209
static class SubclassConfig extends ServerHttpSecurityConfiguration {
188210

@@ -283,6 +305,15 @@ public Mono<CompromisedPasswordDecision> check(String password) {
283305

284306
}
285307

308+
@Target({ ElementType.PARAMETER })
309+
@Retention(RetentionPolicy.RUNTIME)
310+
@CurrentSecurityContext(expression = "authentication.{property}")
311+
@interface CurrentAuthenticationProperty {
312+
313+
String property();
314+
315+
}
316+
286317
@RestController
287318
static class TestController {
288319

@@ -296,6 +327,12 @@ String ifUser(@IsUser("harold") boolean isHarold) {
296327
}
297328
}
298329

330+
@GetMapping("/hello")
331+
String getCurrentAuthenticationProperty(
332+
@CurrentAuthenticationProperty(property = "principal") String principal) {
333+
return principal;
334+
}
335+
299336
}
300337

301338
@Configuration

messaging/src/main/java/org/springframework/security/messaging/handler/invocation/reactive/CurrentSecurityContextArgumentResolver.java

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package org.springframework.security.messaging.handler.invocation.reactive;
1818

1919
import java.lang.annotation.Annotation;
20+
import java.util.Map;
21+
import java.util.concurrent.ConcurrentHashMap;
2022

2123
import org.reactivestreams.Publisher;
2224
import reactor.core.publisher.Mono;
@@ -25,7 +27,6 @@
2527
import org.springframework.core.ReactiveAdapter;
2628
import org.springframework.core.ReactiveAdapterRegistry;
2729
import org.springframework.core.ResolvableType;
28-
import org.springframework.core.annotation.AnnotationUtils;
2930
import org.springframework.expression.BeanResolver;
3031
import org.springframework.expression.Expression;
3132
import org.springframework.expression.ExpressionParser;
@@ -34,6 +35,9 @@
3435
import org.springframework.messaging.Message;
3536
import org.springframework.messaging.handler.invocation.reactive.HandlerMethodArgumentResolver;
3637
import org.springframework.security.core.Authentication;
38+
import org.springframework.security.core.annotation.AnnotationSynthesizer;
39+
import org.springframework.security.core.annotation.AnnotationSynthesizers;
40+
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
3741
import org.springframework.security.core.annotation.CurrentSecurityContext;
3842
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
3943
import org.springframework.security.core.context.SecurityContext;
@@ -88,12 +92,18 @@
8892
* </pre>
8993
*
9094
* @author Rob Winch
95+
* @author DingHao
9196
* @since 5.2
9297
*/
9398
public class CurrentSecurityContextArgumentResolver implements HandlerMethodArgumentResolver {
9499

100+
private final Map<MethodParameter, Annotation> cachedAttributes = new ConcurrentHashMap<>();
101+
95102
private ExpressionParser parser = new SpelExpressionParser();
96103

104+
private AnnotationSynthesizer<CurrentSecurityContext> synthesizer = AnnotationSynthesizers
105+
.requireUnique(CurrentSecurityContext.class);
106+
97107
private BeanResolver beanResolver;
98108

99109
private ReactiveAdapterRegistry adapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
@@ -118,8 +128,7 @@ public void setAdapterRegistry(ReactiveAdapterRegistry adapterRegistry) {
118128

119129
@Override
120130
public boolean supportsParameter(MethodParameter parameter) {
121-
return isMonoSecurityContext(parameter)
122-
|| findMethodAnnotation(CurrentSecurityContext.class, parameter) != null;
131+
return isMonoSecurityContext(parameter) || findMethodAnnotation(parameter) != null;
123132
}
124133

125134
private boolean isMonoSecurityContext(MethodParameter parameter) {
@@ -149,7 +158,7 @@ public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> messag
149158
}
150159

151160
private Object resolveSecurityContext(MethodParameter parameter, Object securityContext) {
152-
CurrentSecurityContext contextAnno = findMethodAnnotation(CurrentSecurityContext.class, parameter);
161+
CurrentSecurityContext contextAnno = findMethodAnnotation(parameter);
153162
if (contextAnno != null) {
154163
return resolveSecurityContextFromAnnotation(contextAnno, parameter, securityContext);
155164
}
@@ -193,26 +202,28 @@ private boolean isInvalidType(MethodParameter parameter, Object value) {
193202
return !typeToCheck.isAssignableFrom(value.getClass());
194203
}
195204

205+
/**
206+
* Configure CurrentSecurityContext template resolution
207+
* <p>
208+
* By default, this value is <code>null</code>, which indicates that templates should
209+
* not be resolved.
210+
* @param templateDefaults - whether to resolve CurrentSecurityContext templates
211+
* parameters
212+
* @since 6.4
213+
*/
214+
public void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) {
215+
this.synthesizer = AnnotationSynthesizers.requireUnique(CurrentSecurityContext.class, templateDefaults);
216+
}
217+
196218
/**
197219
* Obtains the specified {@link Annotation} on the specified {@link MethodParameter}.
198-
* @param annotationClass the class of the {@link Annotation} to find on the
199-
* {@link MethodParameter}
200220
* @param parameter the {@link MethodParameter} to search for an {@link Annotation}
201221
* @return the {@link Annotation} that was found or null.
202222
*/
203-
private <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass, MethodParameter parameter) {
204-
T annotation = parameter.getParameterAnnotation(annotationClass);
205-
if (annotation != null) {
206-
return annotation;
207-
}
208-
Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
209-
for (Annotation toSearch : annotationsToSearch) {
210-
annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass);
211-
if (annotation != null) {
212-
return annotation;
213-
}
214-
}
215-
return null;
223+
@SuppressWarnings("unchecked")
224+
private <T extends Annotation> T findMethodAnnotation(MethodParameter parameter) {
225+
return (T) this.cachedAttributes.computeIfAbsent(parameter,
226+
(methodParameter) -> this.synthesizer.synthesize(methodParameter.getParameter()));
216227
}
217228

218229
}

messaging/src/test/java/org/springframework/security/messaging/handler/invocation/reactive/CurrentSecurityContextArgumentResolverTests.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,20 @@
1616

1717
package org.springframework.security.messaging.handler.invocation.reactive;
1818

19+
import java.lang.annotation.ElementType;
1920
import java.lang.annotation.Retention;
2021
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
2123

2224
import org.junit.jupiter.api.Test;
2325
import reactor.core.publisher.Mono;
2426

2527
import org.springframework.core.MethodParameter;
28+
import org.springframework.core.annotation.AliasFor;
2629
import org.springframework.core.annotation.SynthesizingMethodParameter;
2730
import org.springframework.security.authentication.TestAuthentication;
2831
import org.springframework.security.core.Authentication;
32+
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
2933
import org.springframework.security.core.annotation.CurrentSecurityContext;
3034
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
3135
import org.springframework.security.core.context.SecurityContext;
@@ -171,6 +175,39 @@ public void resolveArgumentWhenMonoCustomSecurityContextNoAnnotationThenFound()
171175
assertThat(result.block().getAuthentication().getPrincipal()).isEqualTo(authentication.getPrincipal());
172176
}
173177

178+
@Test
179+
public void resolveArgumentCustomMetaAnnotation() {
180+
Authentication authentication = TestAuthentication.authenticatedUser();
181+
CustomSecurityContext securityContext = new CustomSecurityContext();
182+
securityContext.setAuthentication(authentication);
183+
Mono<UserDetails> result = (Mono<UserDetails>) this.resolver
184+
.resolveArgument(arg0("showUserCustomMetaAnnotation"), null)
185+
.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)))
186+
.block();
187+
assertThat(result.block()).isEqualTo(authentication.getPrincipal());
188+
}
189+
190+
@Test
191+
public void resolveArgumentCustomMetaAnnotationTpl() {
192+
this.resolver.setTemplateDefaults(new AnnotationTemplateExpressionDefaults());
193+
Authentication authentication = TestAuthentication.authenticatedUser();
194+
CustomSecurityContext securityContext = new CustomSecurityContext();
195+
securityContext.setAuthentication(authentication);
196+
Mono<UserDetails> result = (Mono<UserDetails>) this.resolver
197+
.resolveArgument(arg0("showUserCustomMetaAnnotationTpl"), null)
198+
.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)))
199+
.block();
200+
assertThat(result.block()).isEqualTo(authentication.getPrincipal());
201+
}
202+
203+
private void showUserCustomMetaAnnotation(
204+
@AliasedCurrentSecurityContext(expression = "authentication.principal") Mono<UserDetails> user) {
205+
}
206+
207+
private void showUserCustomMetaAnnotationTpl(
208+
@CurrentAuthenticationProperty(property = "principal") Mono<UserDetails> user) {
209+
}
210+
174211
@SuppressWarnings("unused")
175212
private void monoCustomSecurityContext(Mono<CustomSecurityContext> securityContext) {
176213
}
@@ -186,6 +223,25 @@ private MethodParameter arg0(String methodName) {
186223

187224
}
188225

226+
@Target({ ElementType.PARAMETER })
227+
@Retention(RetentionPolicy.RUNTIME)
228+
@CurrentSecurityContext
229+
@interface AliasedCurrentSecurityContext {
230+
231+
@AliasFor(annotation = CurrentSecurityContext.class)
232+
String expression() default "";
233+
234+
}
235+
236+
@Target({ ElementType.PARAMETER })
237+
@Retention(RetentionPolicy.RUNTIME)
238+
@CurrentSecurityContext(expression = "authentication.{property}")
239+
@interface CurrentAuthenticationProperty {
240+
241+
String property() default "";
242+
243+
}
244+
189245
static class CustomSecurityContext implements SecurityContext {
190246

191247
private Authentication authentication;

0 commit comments

Comments
 (0)