Improve @CurrentSecurityContext meta-annotations

Closes gh-15551
This commit is contained in:
DingHao 2024-08-11 22:43:52 +08:00 committed by Josh Cummings
parent 079b5b91f2
commit ed16c86115
10 changed files with 332 additions and 63 deletions

View File

@ -98,6 +98,7 @@ class WebMvcSecurityConfiguration implements WebMvcConfigurer, ApplicationContex
CurrentSecurityContextArgumentResolver currentSecurityContextArgumentResolver = new CurrentSecurityContextArgumentResolver(); CurrentSecurityContextArgumentResolver currentSecurityContextArgumentResolver = new CurrentSecurityContextArgumentResolver();
currentSecurityContextArgumentResolver.setBeanResolver(this.beanResolver); currentSecurityContextArgumentResolver.setBeanResolver(this.beanResolver);
currentSecurityContextArgumentResolver.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); currentSecurityContextArgumentResolver.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
currentSecurityContextArgumentResolver.setTemplateDefaults(this.templateDefaults);
argumentResolvers.add(currentSecurityContextArgumentResolver); argumentResolvers.add(currentSecurityContextArgumentResolver);
argumentResolvers.add(new CsrfTokenArgumentResolver()); argumentResolvers.add(new CsrfTokenArgumentResolver());
} }

View File

@ -109,12 +109,14 @@ class ServerHttpSecurityConfiguration {
@Bean @Bean
static WebFluxConfigurer authenticationPrincipalArgumentResolverConfigurer( static WebFluxConfigurer authenticationPrincipalArgumentResolverConfigurer(
ObjectProvider<AuthenticationPrincipalArgumentResolver> authenticationPrincipalArgumentResolver) { ObjectProvider<AuthenticationPrincipalArgumentResolver> authenticationPrincipalArgumentResolver,
ObjectProvider<CurrentSecurityContextArgumentResolver> currentSecurityContextArgumentResolvers) {
return new WebFluxConfigurer() { return new WebFluxConfigurer() {
@Override @Override
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
configurer.addCustomResolver(authenticationPrincipalArgumentResolver.getObject()); configurer.addCustomResolver(authenticationPrincipalArgumentResolver.getObject(),
currentSecurityContextArgumentResolvers.getObject());
} }
}; };
@ -133,12 +135,14 @@ class ServerHttpSecurityConfiguration {
} }
@Bean @Bean
CurrentSecurityContextArgumentResolver reactiveCurrentSecurityContextArgumentResolver() { CurrentSecurityContextArgumentResolver reactiveCurrentSecurityContextArgumentResolver(
ObjectProvider<AnnotationTemplateExpressionDefaults> templateDefaults) {
CurrentSecurityContextArgumentResolver resolver = new CurrentSecurityContextArgumentResolver( CurrentSecurityContextArgumentResolver resolver = new CurrentSecurityContextArgumentResolver(
this.adapterRegistry); this.adapterRegistry);
if (this.beanFactory != null) { if (this.beanFactory != null) {
resolver.setBeanResolver(new BeanFactoryResolver(this.beanFactory)); resolver.setBeanResolver(new BeanFactoryResolver(this.beanFactory));
} }
templateDefaults.ifAvailable(resolver::setTemplateDefaults);
return resolver; return resolver;
} }

View File

@ -33,6 +33,7 @@ import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.annotation.CurrentSecurityContext;
import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.csrf.CsrfToken; import org.springframework.security.web.csrf.CsrfToken;
@ -115,6 +116,15 @@ public class WebMvcSecurityConfigurationTests {
this.mockMvc.perform(get("/hi")).andExpect(content().string("Hi, Harold!")); this.mockMvc.perform(get("/hi")).andExpect(content().string("Hi, Harold!"));
} }
@Test
public void resolveMetaAnnotationWhenTemplateDefaultsBeanThenResolvesExpression() throws Exception {
this.mockMvc.perform(get("/hello")).andExpect(content().string("user"));
Authentication harold = new TestingAuthenticationToken("harold", "password",
AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContextHolder.getContext().setAuthentication(harold);
this.mockMvc.perform(get("/hello")).andExpect(content().string("harold"));
}
private ResultMatcher assertResult(Object expected) { private ResultMatcher assertResult(Object expected) {
return model().attribute("result", expected); return model().attribute("result", expected);
} }
@ -128,6 +138,15 @@ public class WebMvcSecurityConfigurationTests {
} }
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@CurrentSecurityContext(expression = "authentication.{property}")
@interface CurrentAuthenticationProperty {
String property();
}
@Controller @Controller
static class TestController { static class TestController {
@ -158,6 +177,13 @@ public class WebMvcSecurityConfigurationTests {
} }
} }
@GetMapping("/hello")
@ResponseBody
String getCurrentAuthenticationProperty(
@CurrentAuthenticationProperty(property = "principal") String principal) {
return principal;
}
} }
@Configuration @Configuration

View File

@ -42,6 +42,7 @@ import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.annotation.CurrentSecurityContext;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.PasswordEncodedUser; import org.springframework.security.core.userdetails.PasswordEncodedUser;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
@ -183,6 +184,27 @@ public class ServerHttpSecurityConfigurationTests {
.isEqualTo("Hi, Harold!"); .isEqualTo("Hi, Harold!");
} }
@Test
public void resoleMetaAnnotationWhenTemplateDefaultsBeanThenResolvesExpression() throws Exception {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
Authentication user = new TestingAuthenticationToken("user", "password", "ROLE_USER");
this.webClient.mutateWith(mockAuthentication(user))
.get()
.uri("/hello")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("user");
Authentication harold = new TestingAuthenticationToken("harold", "password", "ROLE_USER");
this.webClient.mutateWith(mockAuthentication(harold))
.get()
.uri("/hello")
.exchange()
.expectBody(String.class)
.isEqualTo("harold");
}
@Configuration @Configuration
static class SubclassConfig extends ServerHttpSecurityConfiguration { static class SubclassConfig extends ServerHttpSecurityConfiguration {
@ -283,6 +305,15 @@ public class ServerHttpSecurityConfigurationTests {
} }
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@CurrentSecurityContext(expression = "authentication.{property}")
@interface CurrentAuthenticationProperty {
String property();
}
@RestController @RestController
static class TestController { static class TestController {
@ -296,6 +327,12 @@ public class ServerHttpSecurityConfigurationTests {
} }
} }
@GetMapping("/hello")
String getCurrentAuthenticationProperty(
@CurrentAuthenticationProperty(property = "principal") String principal) {
return principal;
}
} }
@Configuration @Configuration

View File

@ -17,6 +17,8 @@
package org.springframework.security.messaging.handler.invocation.reactive; package org.springframework.security.messaging.handler.invocation.reactive;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -25,7 +27,6 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.BeanResolver; import org.springframework.expression.BeanResolver;
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser; import org.springframework.expression.ExpressionParser;
@ -34,6 +35,9 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.messaging.Message; import org.springframework.messaging.Message;
import org.springframework.messaging.handler.invocation.reactive.HandlerMethodArgumentResolver; import org.springframework.messaging.handler.invocation.reactive.HandlerMethodArgumentResolver;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AnnotationSynthesizer;
import org.springframework.security.core.annotation.AnnotationSynthesizers;
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
import org.springframework.security.core.annotation.CurrentSecurityContext; import org.springframework.security.core.annotation.CurrentSecurityContext;
import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
@ -88,12 +92,18 @@ import org.springframework.util.StringUtils;
* </pre> * </pre>
* *
* @author Rob Winch * @author Rob Winch
* @author DingHao
* @since 5.2 * @since 5.2
*/ */
public class CurrentSecurityContextArgumentResolver implements HandlerMethodArgumentResolver { public class CurrentSecurityContextArgumentResolver implements HandlerMethodArgumentResolver {
private final Map<MethodParameter, Annotation> cachedAttributes = new ConcurrentHashMap<>();
private ExpressionParser parser = new SpelExpressionParser(); private ExpressionParser parser = new SpelExpressionParser();
private AnnotationSynthesizer<CurrentSecurityContext> synthesizer = AnnotationSynthesizers
.requireUnique(CurrentSecurityContext.class);
private BeanResolver beanResolver; private BeanResolver beanResolver;
private ReactiveAdapterRegistry adapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); private ReactiveAdapterRegistry adapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
@ -118,8 +128,7 @@ public class CurrentSecurityContextArgumentResolver implements HandlerMethodArgu
@Override @Override
public boolean supportsParameter(MethodParameter parameter) { public boolean supportsParameter(MethodParameter parameter) {
return isMonoSecurityContext(parameter) return isMonoSecurityContext(parameter) || findMethodAnnotation(parameter) != null;
|| findMethodAnnotation(CurrentSecurityContext.class, parameter) != null;
} }
private boolean isMonoSecurityContext(MethodParameter parameter) { private boolean isMonoSecurityContext(MethodParameter parameter) {
@ -149,7 +158,7 @@ public class CurrentSecurityContextArgumentResolver implements HandlerMethodArgu
} }
private Object resolveSecurityContext(MethodParameter parameter, Object securityContext) { private Object resolveSecurityContext(MethodParameter parameter, Object securityContext) {
CurrentSecurityContext contextAnno = findMethodAnnotation(CurrentSecurityContext.class, parameter); CurrentSecurityContext contextAnno = findMethodAnnotation(parameter);
if (contextAnno != null) { if (contextAnno != null) {
return resolveSecurityContextFromAnnotation(contextAnno, parameter, securityContext); return resolveSecurityContextFromAnnotation(contextAnno, parameter, securityContext);
} }
@ -193,26 +202,28 @@ public class CurrentSecurityContextArgumentResolver implements HandlerMethodArgu
return !typeToCheck.isAssignableFrom(value.getClass()); return !typeToCheck.isAssignableFrom(value.getClass());
} }
/**
* Configure CurrentSecurityContext template resolution
* <p>
* By default, this value is <code>null</code>, which indicates that templates should
* not be resolved.
* @param templateDefaults - whether to resolve CurrentSecurityContext templates
* parameters
* @since 6.4
*/
public void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) {
this.synthesizer = AnnotationSynthesizers.requireUnique(CurrentSecurityContext.class, templateDefaults);
}
/** /**
* Obtains the specified {@link Annotation} on the specified {@link MethodParameter}. * Obtains the specified {@link Annotation} on the specified {@link MethodParameter}.
* @param annotationClass the class of the {@link Annotation} to find on the
* {@link MethodParameter}
* @param parameter the {@link MethodParameter} to search for an {@link Annotation} * @param parameter the {@link MethodParameter} to search for an {@link Annotation}
* @return the {@link Annotation} that was found or null. * @return the {@link Annotation} that was found or null.
*/ */
private <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass, MethodParameter parameter) { @SuppressWarnings("unchecked")
T annotation = parameter.getParameterAnnotation(annotationClass); private <T extends Annotation> T findMethodAnnotation(MethodParameter parameter) {
if (annotation != null) { return (T) this.cachedAttributes.computeIfAbsent(parameter,
return annotation; (methodParameter) -> this.synthesizer.synthesize(methodParameter.getParameter()));
}
Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
for (Annotation toSearch : annotationsToSearch) {
annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass);
if (annotation != null) {
return annotation;
}
}
return null;
} }
} }

View File

@ -16,16 +16,20 @@
package org.springframework.security.messaging.handler.invocation.reactive; package org.springframework.security.messaging.handler.invocation.reactive;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authentication.TestAuthentication;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
import org.springframework.security.core.annotation.CurrentSecurityContext; import org.springframework.security.core.annotation.CurrentSecurityContext;
import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
@ -171,6 +175,39 @@ public class CurrentSecurityContextArgumentResolverTests {
assertThat(result.block().getAuthentication().getPrincipal()).isEqualTo(authentication.getPrincipal()); assertThat(result.block().getAuthentication().getPrincipal()).isEqualTo(authentication.getPrincipal());
} }
@Test
public void resolveArgumentCustomMetaAnnotation() {
Authentication authentication = TestAuthentication.authenticatedUser();
CustomSecurityContext securityContext = new CustomSecurityContext();
securityContext.setAuthentication(authentication);
Mono<UserDetails> result = (Mono<UserDetails>) this.resolver
.resolveArgument(arg0("showUserCustomMetaAnnotation"), null)
.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)))
.block();
assertThat(result.block()).isEqualTo(authentication.getPrincipal());
}
@Test
public void resolveArgumentCustomMetaAnnotationTpl() {
this.resolver.setTemplateDefaults(new AnnotationTemplateExpressionDefaults());
Authentication authentication = TestAuthentication.authenticatedUser();
CustomSecurityContext securityContext = new CustomSecurityContext();
securityContext.setAuthentication(authentication);
Mono<UserDetails> result = (Mono<UserDetails>) this.resolver
.resolveArgument(arg0("showUserCustomMetaAnnotationTpl"), null)
.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)))
.block();
assertThat(result.block()).isEqualTo(authentication.getPrincipal());
}
private void showUserCustomMetaAnnotation(
@AliasedCurrentSecurityContext(expression = "authentication.principal") Mono<UserDetails> user) {
}
private void showUserCustomMetaAnnotationTpl(
@CurrentAuthenticationProperty(property = "principal") Mono<UserDetails> user) {
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
private void monoCustomSecurityContext(Mono<CustomSecurityContext> securityContext) { private void monoCustomSecurityContext(Mono<CustomSecurityContext> securityContext) {
} }
@ -186,6 +223,25 @@ public class CurrentSecurityContextArgumentResolverTests {
} }
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@CurrentSecurityContext
@interface AliasedCurrentSecurityContext {
@AliasFor(annotation = CurrentSecurityContext.class)
String expression() default "";
}
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@CurrentSecurityContext(expression = "authentication.{property}")
@interface CurrentAuthenticationProperty {
String property() default "";
}
static class CustomSecurityContext implements SecurityContext { static class CustomSecurityContext implements SecurityContext {
private Authentication authentication; private Authentication authentication;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,14 +17,18 @@
package org.springframework.security.web.method.annotation; package org.springframework.security.web.method.annotation;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.BeanResolver; import org.springframework.expression.BeanResolver;
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser; import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.security.core.annotation.AnnotationSynthesizer;
import org.springframework.security.core.annotation.AnnotationSynthesizers;
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
import org.springframework.security.core.annotation.CurrentSecurityContext; import org.springframework.security.core.annotation.CurrentSecurityContext;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
@ -72,6 +76,7 @@ import org.springframework.web.method.support.ModelAndViewContainer;
* </p> * </p>
* *
* @author Dan Zheng * @author Dan Zheng
* @author DingHao
* @since 5.2 * @since 5.2
*/ */
public final class CurrentSecurityContextArgumentResolver implements HandlerMethodArgumentResolver { public final class CurrentSecurityContextArgumentResolver implements HandlerMethodArgumentResolver {
@ -79,14 +84,19 @@ public final class CurrentSecurityContextArgumentResolver implements HandlerMeth
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy(); .getContextHolderStrategy();
private final Map<MethodParameter, Annotation> cachedAttributes = new ConcurrentHashMap<>();
private ExpressionParser parser = new SpelExpressionParser(); private ExpressionParser parser = new SpelExpressionParser();
private AnnotationSynthesizer<CurrentSecurityContext> synthesizer = AnnotationSynthesizers
.requireUnique(CurrentSecurityContext.class);
private BeanResolver beanResolver; private BeanResolver beanResolver;
@Override @Override
public boolean supportsParameter(MethodParameter parameter) { public boolean supportsParameter(MethodParameter parameter) {
return SecurityContext.class.isAssignableFrom(parameter.getParameterType()) return SecurityContext.class.isAssignableFrom(parameter.getParameterType())
|| findMethodAnnotation(CurrentSecurityContext.class, parameter) != null; || findMethodAnnotation(parameter) != null;
} }
@Override @Override
@ -96,7 +106,7 @@ public final class CurrentSecurityContextArgumentResolver implements HandlerMeth
if (securityContext == null) { if (securityContext == null) {
return null; return null;
} }
CurrentSecurityContext annotation = findMethodAnnotation(CurrentSecurityContext.class, parameter); CurrentSecurityContext annotation = findMethodAnnotation(parameter);
if (annotation != null) { if (annotation != null) {
return resolveSecurityContextFromAnnotation(parameter, annotation, securityContext); return resolveSecurityContextFromAnnotation(parameter, annotation, securityContext);
} }
@ -124,6 +134,19 @@ public final class CurrentSecurityContextArgumentResolver implements HandlerMeth
this.beanResolver = beanResolver; this.beanResolver = beanResolver;
} }
/**
* Configure CurrentSecurityContext template resolution
* <p>
* By default, this value is <code>null</code>, which indicates that templates should
* not be resolved.
* @param templateDefaults - whether to resolve CurrentSecurityContext templates
* parameters
* @since 6.4
*/
public void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) {
this.synthesizer = AnnotationSynthesizers.requireUnique(CurrentSecurityContext.class, templateDefaults);
}
private Object resolveSecurityContextFromAnnotation(MethodParameter parameter, CurrentSecurityContext annotation, private Object resolveSecurityContextFromAnnotation(MethodParameter parameter, CurrentSecurityContext annotation,
SecurityContext securityContext) { SecurityContext securityContext) {
Object securityContextResult = securityContext; Object securityContextResult = securityContext;
@ -149,24 +172,13 @@ public final class CurrentSecurityContextArgumentResolver implements HandlerMeth
/** /**
* Obtain the specified {@link Annotation} on the specified {@link MethodParameter}. * Obtain the specified {@link Annotation} on the specified {@link MethodParameter}.
* @param annotationClass the class of the {@link Annotation} to find on the
* {@link MethodParameter}
* @param parameter the {@link MethodParameter} to search for an {@link Annotation} * @param parameter the {@link MethodParameter} to search for an {@link Annotation}
* @return the {@link Annotation} that was found or null. * @return the {@link Annotation} that was found or null.
*/ */
private <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass, MethodParameter parameter) { @SuppressWarnings("unchecked")
T annotation = parameter.getParameterAnnotation(annotationClass); private <T extends Annotation> T findMethodAnnotation(MethodParameter parameter) {
if (annotation != null) { return (T) this.cachedAttributes.computeIfAbsent(parameter,
return annotation; (methodParameter) -> this.synthesizer.synthesize(methodParameter.getParameter()));
}
Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
for (Annotation toSearch : annotationsToSearch) {
annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass);
if (annotation != null) {
return annotation;
}
}
return null;
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,6 +17,8 @@
package org.springframework.security.web.reactive.result.method.annotation; package org.springframework.security.web.reactive.result.method.annotation;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -25,12 +27,14 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.BeanResolver; import org.springframework.expression.BeanResolver;
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser; import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.security.core.annotation.AnnotationSynthesizer;
import org.springframework.security.core.annotation.AnnotationSynthesizers;
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
import org.springframework.security.core.annotation.CurrentSecurityContext; import org.springframework.security.core.annotation.CurrentSecurityContext;
import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
@ -44,12 +48,18 @@ import org.springframework.web.server.ServerWebExchange;
* Resolves the {@link SecurityContext} * Resolves the {@link SecurityContext}
* *
* @author Dan Zheng * @author Dan Zheng
* @author DingHao
* @since 5.2 * @since 5.2
*/ */
public class CurrentSecurityContextArgumentResolver extends HandlerMethodArgumentResolverSupport { public class CurrentSecurityContextArgumentResolver extends HandlerMethodArgumentResolverSupport {
private final Map<MethodParameter, Annotation> cachedAttributes = new ConcurrentHashMap<>();
private ExpressionParser parser = new SpelExpressionParser(); private ExpressionParser parser = new SpelExpressionParser();
private AnnotationSynthesizer<CurrentSecurityContext> synthesizer = AnnotationSynthesizers
.requireUnique(CurrentSecurityContext.class);
private BeanResolver beanResolver; private BeanResolver beanResolver;
public CurrentSecurityContextArgumentResolver(ReactiveAdapterRegistry adapterRegistry) { public CurrentSecurityContextArgumentResolver(ReactiveAdapterRegistry adapterRegistry) {
@ -65,10 +75,22 @@ public class CurrentSecurityContextArgumentResolver extends HandlerMethodArgumen
this.beanResolver = beanResolver; this.beanResolver = beanResolver;
} }
/**
* Configure CurrentSecurityContext template resolution
* <p>
* By default, this value is <code>null</code>, which indicates that templates should
* not be resolved.
* @param templateDefaults - whether to resolve CurrentSecurityContext templates
* parameters
* @since 6.4
*/
public void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) {
this.synthesizer = AnnotationSynthesizers.requireUnique(CurrentSecurityContext.class, templateDefaults);
}
@Override @Override
public boolean supportsParameter(MethodParameter parameter) { public boolean supportsParameter(MethodParameter parameter) {
return isMonoSecurityContext(parameter) return isMonoSecurityContext(parameter) || findMethodAnnotation(parameter) != null;
|| findMethodAnnotation(CurrentSecurityContext.class, parameter) != null;
} }
private boolean isMonoSecurityContext(MethodParameter parameter) { private boolean isMonoSecurityContext(MethodParameter parameter) {
@ -108,7 +130,7 @@ public class CurrentSecurityContextArgumentResolver extends HandlerMethodArgumen
* @return the resolved object from expression. * @return the resolved object from expression.
*/ */
private Object resolveSecurityContext(MethodParameter parameter, SecurityContext securityContext) { private Object resolveSecurityContext(MethodParameter parameter, SecurityContext securityContext) {
CurrentSecurityContext annotation = findMethodAnnotation(CurrentSecurityContext.class, parameter); CurrentSecurityContext annotation = findMethodAnnotation(parameter);
if (annotation != null) { if (annotation != null) {
return resolveSecurityContextFromAnnotation(annotation, parameter, securityContext); return resolveSecurityContextFromAnnotation(annotation, parameter, securityContext);
} }
@ -162,24 +184,13 @@ public class CurrentSecurityContextArgumentResolver extends HandlerMethodArgumen
/** /**
* Obtains the specified {@link Annotation} on the specified {@link MethodParameter}. * Obtains the specified {@link Annotation} on the specified {@link MethodParameter}.
* @param annotationClass the class of the {@link Annotation} to find on the
* {@link MethodParameter}
* @param parameter the {@link MethodParameter} to search for an {@link Annotation} * @param parameter the {@link MethodParameter} to search for an {@link Annotation}
* @return the {@link Annotation} that was found or null. * @return the {@link Annotation} that was found or null.
*/ */
private <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass, MethodParameter parameter) { @SuppressWarnings("unchecked")
T annotation = parameter.getParameterAnnotation(annotationClass); private <T extends Annotation> T findMethodAnnotation(MethodParameter parameter) {
if (annotation != null) { return (T) this.cachedAttributes.computeIfAbsent(parameter,
return annotation; (methodParameter) -> this.synthesizer.synthesize(methodParameter.getParameter()));
}
Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
for (Annotation toSearch : annotationsToSearch) {
annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass);
if (annotation != null) {
return annotation;
}
}
return null;
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -27,10 +27,12 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AliasFor;
import org.springframework.expression.BeanResolver; import org.springframework.expression.BeanResolver;
import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
import org.springframework.security.core.annotation.CurrentSecurityContext; import org.springframework.security.core.annotation.CurrentSecurityContext;
import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
@ -247,6 +249,23 @@ public class CurrentSecurityContextArgumentResolverTests {
.resolveArgument(showCurrentSecurityWithErrorOnInvalidTypeMisMatch(), null, null, null)); .resolveArgument(showCurrentSecurityWithErrorOnInvalidTypeMisMatch(), null, null, null));
} }
@Test
public void resolveArgumentCustomMetaAnnotation() {
String principal = "current_authentcation";
setAuthenticationPrincipal(principal);
String p = (String) this.resolver.resolveArgument(showUserCustomMetaAnnotation(), null, null, null);
assertThat(p).isEqualTo(principal);
}
@Test
public void resolveArgumentCustomMetaAnnotationTpl() {
String principal = "current_authentcation";
setAuthenticationPrincipal(principal);
this.resolver.setTemplateDefaults(new AnnotationTemplateExpressionDefaults());
String p = (String) this.resolver.resolveArgument(showUserCustomMetaAnnotationTpl(), null, null, null);
assertThat(p).isEqualTo(principal);
}
private MethodParameter showSecurityContextNoAnnotationTypeMismatch() { private MethodParameter showSecurityContextNoAnnotationTypeMismatch() {
return getMethodParameter("showSecurityContextNoAnnotation", String.class); return getMethodParameter("showSecurityContextNoAnnotation", String.class);
} }
@ -307,6 +326,14 @@ public class CurrentSecurityContextArgumentResolverTests {
return getMethodParameter("showCurrentAuthentication", Authentication.class); return getMethodParameter("showCurrentAuthentication", Authentication.class);
} }
public MethodParameter showUserCustomMetaAnnotation() {
return getMethodParameter("showUserCustomMetaAnnotation", String.class);
}
public MethodParameter showUserCustomMetaAnnotationTpl() {
return getMethodParameter("showUserCustomMetaAnnotationTpl", String.class);
}
public MethodParameter showCurrentSecurityWithErrorOnInvalidType() { public MethodParameter showCurrentSecurityWithErrorOnInvalidType() {
return getMethodParameter("showCurrentSecurityWithErrorOnInvalidType", SecurityContext.class); return getMethodParameter("showCurrentSecurityWithErrorOnInvalidType", SecurityContext.class);
} }
@ -394,6 +421,14 @@ public class CurrentSecurityContextArgumentResolverTests {
public void showCurrentAuthentication(@CurrentAuthentication Authentication authentication) { public void showCurrentAuthentication(@CurrentAuthentication Authentication authentication) {
} }
public void showUserCustomMetaAnnotation(
@AliasedCurrentSecurityContext(expression = "authentication.principal") String name) {
}
public void showUserCustomMetaAnnotationTpl(
@CurrentAuthenticationProperty(property = "principal") String name) {
}
public void showCurrentSecurityWithErrorOnInvalidType( public void showCurrentSecurityWithErrorOnInvalidType(
@CurrentSecurityWithErrorOnInvalidType SecurityContext context) { @CurrentSecurityWithErrorOnInvalidType SecurityContext context) {
} }
@ -447,4 +482,23 @@ public class CurrentSecurityContextArgumentResolverTests {
} }
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@CurrentSecurityContext
@interface AliasedCurrentSecurityContext {
@AliasFor(annotation = CurrentSecurityContext.class)
String expression() default "";
}
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@CurrentSecurityContext(expression = "authentication.{property}")
@interface CurrentAuthenticationProperty {
String property() default "";
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -31,10 +31,12 @@ import reactor.util.context.Context;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.annotation.AliasFor;
import org.springframework.expression.BeanResolver; import org.springframework.expression.BeanResolver;
import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
import org.springframework.security.core.annotation.CurrentSecurityContext; import org.springframework.security.core.annotation.CurrentSecurityContext;
import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
@ -402,6 +404,42 @@ public class CurrentSecurityContextArgumentResolverTests {
ReactiveSecurityContextHolder.clearContext(); ReactiveSecurityContextHolder.clearContext();
} }
@Test
public void resolveArgumentCustomMetaAnnotation() {
MethodParameter parameter = ResolvableMethod.on(getClass())
.named("showUserCustomMetaAnnotation")
.build()
.arg(Mono.class, String.class);
Authentication auth = buildAuthenticationWithPrincipal("current_authentication");
Context context = ReactiveSecurityContextHolder.withAuthentication(auth);
Mono<Object> argument = this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange);
String principal = (String) argument.contextWrite(context).cast(Mono.class).block().block();
assertThat(principal).isSameAs(auth.getPrincipal());
ReactiveSecurityContextHolder.clearContext();
}
@Test
public void resolveArgumentCustomMetaAnnotationTpl() {
this.resolver.setTemplateDefaults(new AnnotationTemplateExpressionDefaults());
MethodParameter parameter = ResolvableMethod.on(getClass())
.named("showUserCustomMetaAnnotationTpl")
.build()
.arg(Mono.class, String.class);
Authentication auth = buildAuthenticationWithPrincipal("current_authentication");
Context context = ReactiveSecurityContextHolder.withAuthentication(auth);
Mono<Object> argument = this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange);
String principal = (String) argument.contextWrite(context).cast(Mono.class).block().block();
assertThat(principal).isSameAs(auth.getPrincipal());
ReactiveSecurityContextHolder.clearContext();
}
void showUserCustomMetaAnnotation(
@AliasedCurrentSecurityContext(expression = "authentication.principal") Mono<String> user) {
}
void showUserCustomMetaAnnotationTpl(@CurrentAuthenticationProperty(property = "principal") Mono<String> user) {
}
void securityContext(@CurrentSecurityContext Mono<SecurityContext> monoSecurityContext) { void securityContext(@CurrentSecurityContext Mono<SecurityContext> monoSecurityContext) {
} }
@ -479,6 +517,25 @@ public class CurrentSecurityContextArgumentResolverTests {
} }
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@CurrentSecurityContext
@interface AliasedCurrentSecurityContext {
@AliasFor(annotation = CurrentSecurityContext.class)
String expression() default "";
}
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@CurrentSecurityContext(expression = "authentication.{property}")
@interface CurrentAuthenticationProperty {
String property() default "";
}
static class CustomSecurityContext implements SecurityContext { static class CustomSecurityContext implements SecurityContext {
private Authentication authentication; private Authentication authentication;