From ed16c86115b1388482b30518e5144f1b65c46bae Mon Sep 17 00:00:00 2001 From: DingHao Date: Sun, 11 Aug 2024 22:43:52 +0800 Subject: [PATCH] Improve @CurrentSecurityContext meta-annotations Closes gh-15551 --- .../WebMvcSecurityConfiguration.java | 1 + .../ServerHttpSecurityConfiguration.java | 10 +++- .../WebMvcSecurityConfigurationTests.java | 26 ++++++++ .../ServerHttpSecurityConfigurationTests.java | 37 ++++++++++++ ...urrentSecurityContextArgumentResolver.java | 49 +++++++++------ ...tSecurityContextArgumentResolverTests.java | 56 ++++++++++++++++++ ...urrentSecurityContextArgumentResolver.java | 50 ++++++++++------ ...urrentSecurityContextArgumentResolver.java | 51 +++++++++------- ...tSecurityContextArgumentResolverTests.java | 56 +++++++++++++++++- ...tSecurityContextArgumentResolverTests.java | 59 ++++++++++++++++++- 10 files changed, 332 insertions(+), 63 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java index 4c4617964c..3f03140a46 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java @@ -98,6 +98,7 @@ class WebMvcSecurityConfiguration implements WebMvcConfigurer, ApplicationContex CurrentSecurityContextArgumentResolver currentSecurityContextArgumentResolver = new CurrentSecurityContextArgumentResolver(); currentSecurityContextArgumentResolver.setBeanResolver(this.beanResolver); currentSecurityContextArgumentResolver.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); + currentSecurityContextArgumentResolver.setTemplateDefaults(this.templateDefaults); argumentResolvers.add(currentSecurityContextArgumentResolver); argumentResolvers.add(new CsrfTokenArgumentResolver()); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java index 74c832ae42..40bbac985d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java @@ -109,12 +109,14 @@ class ServerHttpSecurityConfiguration { @Bean static WebFluxConfigurer authenticationPrincipalArgumentResolverConfigurer( - ObjectProvider authenticationPrincipalArgumentResolver) { + ObjectProvider authenticationPrincipalArgumentResolver, + ObjectProvider currentSecurityContextArgumentResolvers) { return new WebFluxConfigurer() { @Override public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { - configurer.addCustomResolver(authenticationPrincipalArgumentResolver.getObject()); + configurer.addCustomResolver(authenticationPrincipalArgumentResolver.getObject(), + currentSecurityContextArgumentResolvers.getObject()); } }; @@ -133,12 +135,14 @@ class ServerHttpSecurityConfiguration { } @Bean - CurrentSecurityContextArgumentResolver reactiveCurrentSecurityContextArgumentResolver() { + CurrentSecurityContextArgumentResolver reactiveCurrentSecurityContextArgumentResolver( + ObjectProvider templateDefaults) { CurrentSecurityContextArgumentResolver resolver = new CurrentSecurityContextArgumentResolver( this.adapterRegistry); if (this.beanFactory != null) { resolver.setBeanResolver(new BeanFactoryResolver(this.beanFactory)); } + templateDefaults.ifAvailable(resolver::setTemplateDefaults); return resolver; } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfigurationTests.java index 2d1249abc3..ea1c70d050 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfigurationTests.java @@ -33,6 +33,7 @@ import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; 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.context.SecurityContextHolder; import org.springframework.security.web.csrf.CsrfToken; @@ -115,6 +116,15 @@ public class WebMvcSecurityConfigurationTests { 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) { 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 static class TestController { @@ -158,6 +177,13 @@ public class WebMvcSecurityConfigurationTests { } } + @GetMapping("/hello") + @ResponseBody + String getCurrentAuthenticationProperty( + @CurrentAuthenticationProperty(property = "principal") String principal) { + return principal; + } + } @Configuration diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java index b45085edfd..006b3f190a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java @@ -42,6 +42,7 @@ import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; 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.PasswordEncodedUser; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; @@ -183,6 +184,27 @@ public class ServerHttpSecurityConfigurationTests { .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 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 static class TestController { @@ -296,6 +327,12 @@ public class ServerHttpSecurityConfigurationTests { } } + @GetMapping("/hello") + String getCurrentAuthenticationProperty( + @CurrentAuthenticationProperty(property = "principal") String principal) { + return principal; + } + } @Configuration diff --git a/messaging/src/main/java/org/springframework/security/messaging/handler/invocation/reactive/CurrentSecurityContextArgumentResolver.java b/messaging/src/main/java/org/springframework/security/messaging/handler/invocation/reactive/CurrentSecurityContextArgumentResolver.java index ec69e1d389..303b351e55 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/handler/invocation/reactive/CurrentSecurityContextArgumentResolver.java +++ b/messaging/src/main/java/org/springframework/security/messaging/handler/invocation/reactive/CurrentSecurityContextArgumentResolver.java @@ -17,6 +17,8 @@ package org.springframework.security.messaging.handler.invocation.reactive; import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; @@ -25,7 +27,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.expression.BeanResolver; import org.springframework.expression.Expression; 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.handler.invocation.reactive.HandlerMethodArgumentResolver; 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.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContext; @@ -88,12 +92,18 @@ import org.springframework.util.StringUtils; * * * @author Rob Winch + * @author DingHao * @since 5.2 */ public class CurrentSecurityContextArgumentResolver implements HandlerMethodArgumentResolver { + private final Map cachedAttributes = new ConcurrentHashMap<>(); + private ExpressionParser parser = new SpelExpressionParser(); + private AnnotationSynthesizer synthesizer = AnnotationSynthesizers + .requireUnique(CurrentSecurityContext.class); + private BeanResolver beanResolver; private ReactiveAdapterRegistry adapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); @@ -118,8 +128,7 @@ public class CurrentSecurityContextArgumentResolver implements HandlerMethodArgu @Override public boolean supportsParameter(MethodParameter parameter) { - return isMonoSecurityContext(parameter) - || findMethodAnnotation(CurrentSecurityContext.class, parameter) != null; + return isMonoSecurityContext(parameter) || findMethodAnnotation(parameter) != null; } private boolean isMonoSecurityContext(MethodParameter parameter) { @@ -149,7 +158,7 @@ public class CurrentSecurityContextArgumentResolver implements HandlerMethodArgu } private Object resolveSecurityContext(MethodParameter parameter, Object securityContext) { - CurrentSecurityContext contextAnno = findMethodAnnotation(CurrentSecurityContext.class, parameter); + CurrentSecurityContext contextAnno = findMethodAnnotation(parameter); if (contextAnno != null) { return resolveSecurityContextFromAnnotation(contextAnno, parameter, securityContext); } @@ -193,26 +202,28 @@ public class CurrentSecurityContextArgumentResolver implements HandlerMethodArgu return !typeToCheck.isAssignableFrom(value.getClass()); } + /** + * Configure CurrentSecurityContext template resolution + *

+ * By default, this value is null, 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}. - * @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} * @return the {@link Annotation} that was found or null. */ - private T findMethodAnnotation(Class annotationClass, MethodParameter parameter) { - T annotation = parameter.getParameterAnnotation(annotationClass); - if (annotation != null) { - return annotation; - } - Annotation[] annotationsToSearch = parameter.getParameterAnnotations(); - for (Annotation toSearch : annotationsToSearch) { - annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass); - if (annotation != null) { - return annotation; - } - } - return null; + @SuppressWarnings("unchecked") + private T findMethodAnnotation(MethodParameter parameter) { + return (T) this.cachedAttributes.computeIfAbsent(parameter, + (methodParameter) -> this.synthesizer.synthesize(methodParameter.getParameter())); } } diff --git a/messaging/src/test/java/org/springframework/security/messaging/handler/invocation/reactive/CurrentSecurityContextArgumentResolverTests.java b/messaging/src/test/java/org/springframework/security/messaging/handler/invocation/reactive/CurrentSecurityContextArgumentResolverTests.java index 22876bde63..e9bdbd9665 100644 --- a/messaging/src/test/java/org/springframework/security/messaging/handler/invocation/reactive/CurrentSecurityContextArgumentResolverTests.java +++ b/messaging/src/test/java/org/springframework/security/messaging/handler/invocation/reactive/CurrentSecurityContextArgumentResolverTests.java @@ -16,16 +16,20 @@ package org.springframework.security.messaging.handler.invocation.reactive; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.security.authentication.TestAuthentication; 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.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContext; @@ -171,6 +175,39 @@ public class CurrentSecurityContextArgumentResolverTests { assertThat(result.block().getAuthentication().getPrincipal()).isEqualTo(authentication.getPrincipal()); } + @Test + public void resolveArgumentCustomMetaAnnotation() { + Authentication authentication = TestAuthentication.authenticatedUser(); + CustomSecurityContext securityContext = new CustomSecurityContext(); + securityContext.setAuthentication(authentication); + Mono result = (Mono) 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 result = (Mono) 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 user) { + } + + private void showUserCustomMetaAnnotationTpl( + @CurrentAuthenticationProperty(property = "principal") Mono user) { + } + @SuppressWarnings("unused") private void monoCustomSecurityContext(Mono 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 { private Authentication authentication; diff --git a/web/src/main/java/org/springframework/security/web/method/annotation/CurrentSecurityContextArgumentResolver.java b/web/src/main/java/org/springframework/security/web/method/annotation/CurrentSecurityContextArgumentResolver.java index d1a6ba12a7..475d650c9a 100644 --- a/web/src/main/java/org/springframework/security/web/method/annotation/CurrentSecurityContextArgumentResolver.java +++ b/web/src/main/java/org/springframework/security/web/method/annotation/CurrentSecurityContextArgumentResolver.java @@ -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"); * you may not use this file except in compliance with the License. @@ -17,14 +17,18 @@ package org.springframework.security.web.method.annotation; import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.expression.BeanResolver; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; 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.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -72,6 +76,7 @@ import org.springframework.web.method.support.ModelAndViewContainer; *

* * @author Dan Zheng + * @author DingHao * @since 5.2 */ public final class CurrentSecurityContextArgumentResolver implements HandlerMethodArgumentResolver { @@ -79,14 +84,19 @@ public final class CurrentSecurityContextArgumentResolver implements HandlerMeth private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); + private final Map cachedAttributes = new ConcurrentHashMap<>(); + private ExpressionParser parser = new SpelExpressionParser(); + private AnnotationSynthesizer synthesizer = AnnotationSynthesizers + .requireUnique(CurrentSecurityContext.class); + private BeanResolver beanResolver; @Override public boolean supportsParameter(MethodParameter parameter) { return SecurityContext.class.isAssignableFrom(parameter.getParameterType()) - || findMethodAnnotation(CurrentSecurityContext.class, parameter) != null; + || findMethodAnnotation(parameter) != null; } @Override @@ -96,7 +106,7 @@ public final class CurrentSecurityContextArgumentResolver implements HandlerMeth if (securityContext == null) { return null; } - CurrentSecurityContext annotation = findMethodAnnotation(CurrentSecurityContext.class, parameter); + CurrentSecurityContext annotation = findMethodAnnotation(parameter); if (annotation != null) { return resolveSecurityContextFromAnnotation(parameter, annotation, securityContext); } @@ -124,6 +134,19 @@ public final class CurrentSecurityContextArgumentResolver implements HandlerMeth this.beanResolver = beanResolver; } + /** + * Configure CurrentSecurityContext template resolution + *

+ * By default, this value is null, 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, SecurityContext securityContext) { Object securityContextResult = securityContext; @@ -149,24 +172,13 @@ public final class CurrentSecurityContextArgumentResolver implements HandlerMeth /** * 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} * @return the {@link Annotation} that was found or null. */ - private T findMethodAnnotation(Class annotationClass, MethodParameter parameter) { - T annotation = parameter.getParameterAnnotation(annotationClass); - if (annotation != null) { - return annotation; - } - Annotation[] annotationsToSearch = parameter.getParameterAnnotations(); - for (Annotation toSearch : annotationsToSearch) { - annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass); - if (annotation != null) { - return annotation; - } - } - return null; + @SuppressWarnings("unchecked") + private T findMethodAnnotation(MethodParameter parameter) { + return (T) this.cachedAttributes.computeIfAbsent(parameter, + (methodParameter) -> this.synthesizer.synthesize(methodParameter.getParameter())); } } diff --git a/web/src/main/java/org/springframework/security/web/reactive/result/method/annotation/CurrentSecurityContextArgumentResolver.java b/web/src/main/java/org/springframework/security/web/reactive/result/method/annotation/CurrentSecurityContextArgumentResolver.java index fd51d8ac53..432743d2d9 100644 --- a/web/src/main/java/org/springframework/security/web/reactive/result/method/annotation/CurrentSecurityContextArgumentResolver.java +++ b/web/src/main/java/org/springframework/security/web/reactive/result/method/annotation/CurrentSecurityContextArgumentResolver.java @@ -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"); * 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; import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; @@ -25,12 +27,14 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.expression.BeanResolver; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; 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.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContext; @@ -44,12 +48,18 @@ import org.springframework.web.server.ServerWebExchange; * Resolves the {@link SecurityContext} * * @author Dan Zheng + * @author DingHao * @since 5.2 */ public class CurrentSecurityContextArgumentResolver extends HandlerMethodArgumentResolverSupport { + private final Map cachedAttributes = new ConcurrentHashMap<>(); + private ExpressionParser parser = new SpelExpressionParser(); + private AnnotationSynthesizer synthesizer = AnnotationSynthesizers + .requireUnique(CurrentSecurityContext.class); + private BeanResolver beanResolver; public CurrentSecurityContextArgumentResolver(ReactiveAdapterRegistry adapterRegistry) { @@ -65,10 +75,22 @@ public class CurrentSecurityContextArgumentResolver extends HandlerMethodArgumen this.beanResolver = beanResolver; } + /** + * Configure CurrentSecurityContext template resolution + *

+ * By default, this value is null, 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 public boolean supportsParameter(MethodParameter parameter) { - return isMonoSecurityContext(parameter) - || findMethodAnnotation(CurrentSecurityContext.class, parameter) != null; + return isMonoSecurityContext(parameter) || findMethodAnnotation(parameter) != null; } private boolean isMonoSecurityContext(MethodParameter parameter) { @@ -108,7 +130,7 @@ public class CurrentSecurityContextArgumentResolver extends HandlerMethodArgumen * @return the resolved object from expression. */ private Object resolveSecurityContext(MethodParameter parameter, SecurityContext securityContext) { - CurrentSecurityContext annotation = findMethodAnnotation(CurrentSecurityContext.class, parameter); + CurrentSecurityContext annotation = findMethodAnnotation(parameter); if (annotation != null) { return resolveSecurityContextFromAnnotation(annotation, parameter, securityContext); } @@ -162,24 +184,13 @@ public class CurrentSecurityContextArgumentResolver extends HandlerMethodArgumen /** * 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} * @return the {@link Annotation} that was found or null. */ - private T findMethodAnnotation(Class annotationClass, MethodParameter parameter) { - T annotation = parameter.getParameterAnnotation(annotationClass); - if (annotation != null) { - return annotation; - } - Annotation[] annotationsToSearch = parameter.getParameterAnnotations(); - for (Annotation toSearch : annotationsToSearch) { - annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass); - if (annotation != null) { - return annotation; - } - } - return null; + @SuppressWarnings("unchecked") + private T findMethodAnnotation(MethodParameter parameter) { + return (T) this.cachedAttributes.computeIfAbsent(parameter, + (methodParameter) -> this.synthesizer.synthesize(methodParameter.getParameter())); } } diff --git a/web/src/test/java/org/springframework/security/web/method/annotation/CurrentSecurityContextArgumentResolverTests.java b/web/src/test/java/org/springframework/security/web/method/annotation/CurrentSecurityContextArgumentResolverTests.java index 80c33ac9ec..fa35eff2b4 100644 --- a/web/src/test/java/org/springframework/security/web/method/annotation/CurrentSecurityContextArgumentResolverTests.java +++ b/web/src/test/java/org/springframework/security/web/method/annotation/CurrentSecurityContextArgumentResolverTests.java @@ -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"); * 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.springframework.core.MethodParameter; +import org.springframework.core.annotation.AliasFor; import org.springframework.expression.BeanResolver; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.security.authentication.TestingAuthenticationToken; 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.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContext; @@ -247,6 +249,23 @@ public class CurrentSecurityContextArgumentResolverTests { .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() { return getMethodParameter("showSecurityContextNoAnnotation", String.class); } @@ -307,6 +326,14 @@ public class CurrentSecurityContextArgumentResolverTests { return getMethodParameter("showCurrentAuthentication", Authentication.class); } + public MethodParameter showUserCustomMetaAnnotation() { + return getMethodParameter("showUserCustomMetaAnnotation", String.class); + } + + public MethodParameter showUserCustomMetaAnnotationTpl() { + return getMethodParameter("showUserCustomMetaAnnotationTpl", String.class); + } + public MethodParameter showCurrentSecurityWithErrorOnInvalidType() { return getMethodParameter("showCurrentSecurityWithErrorOnInvalidType", SecurityContext.class); } @@ -394,6 +421,14 @@ public class CurrentSecurityContextArgumentResolverTests { 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( @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 ""; + + } + } diff --git a/web/src/test/java/org/springframework/security/web/reactive/result/method/annotation/CurrentSecurityContextArgumentResolverTests.java b/web/src/test/java/org/springframework/security/web/reactive/result/method/annotation/CurrentSecurityContextArgumentResolverTests.java index 5556a25ed1..a9deaf0714 100644 --- a/web/src/test/java/org/springframework/security/web/reactive/result/method/annotation/CurrentSecurityContextArgumentResolverTests.java +++ b/web/src/test/java/org/springframework/security/web/reactive/result/method/annotation/CurrentSecurityContextArgumentResolverTests.java @@ -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"); * 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.ReactiveAdapterRegistry; +import org.springframework.core.annotation.AliasFor; import org.springframework.expression.BeanResolver; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.security.authentication.TestingAuthenticationToken; 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.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContext; @@ -402,6 +404,42 @@ public class CurrentSecurityContextArgumentResolverTests { 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 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 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 user) { + } + + void showUserCustomMetaAnnotationTpl(@CurrentAuthenticationProperty(property = "principal") Mono user) { + } + void securityContext(@CurrentSecurityContext Mono 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 { private Authentication authentication;