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 4c4617964c6..3f03140a461 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 @@ public void addArgumentResolvers(List argumentRes 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 74c832ae427..40bbac985d7 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 @@ void setCompromisedPasswordChecker(ReactiveCompromisedPasswordChecker compromise @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 @@ AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver( } @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 2d1249abc3d..ea1c70d0501 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.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 void metaAnnotationWhenTemplateDefaultsBeanThenResolvesExpression() throw 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 @@ private ResultMatcher assertResult(Object expected) { } + @Target({ ElementType.PARAMETER }) + @Retention(RetentionPolicy.RUNTIME) + @CurrentSecurityContext(expression = "authentication.{property}") + @interface CurrentAuthenticationProperty { + + String property(); + + } + @Controller static class TestController { @@ -158,6 +177,13 @@ String ifUser(@IsUser("harold") boolean isHarold) { } } + @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 b45085edfd7..006b3f190a9 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.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 void metaAnnotationWhenTemplateDefaultsBeanThenResolvesExpression() throw .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 Mono check(String password) { } + @Target({ ElementType.PARAMETER }) + @Retention(RetentionPolicy.RUNTIME) + @CurrentSecurityContext(expression = "authentication.{property}") + @interface CurrentAuthenticationProperty { + + String property(); + + } + @RestController static class TestController { @@ -296,6 +327,12 @@ String ifUser(@IsUser("harold") boolean isHarold) { } } + @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 ec69e1d389f..303b351e55f 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.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.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 @@ * * * @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 void setAdapterRegistry(ReactiveAdapterRegistry adapterRegistry) { @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 Mono resolveArgument(MethodParameter parameter, Message messag } 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 @@ private boolean isInvalidType(MethodParameter parameter, Object value) { 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 22876bde63e..e9bdbd96659 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 void resolveArgumentWhenMonoCustomSecurityContextNoAnnotationThenFound() 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 @@ private MethodParameter arg0(String methodName) { } + @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 d1a6ba12a7d..475d650c9a5 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 @@ *

* * @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 Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m 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 void setBeanResolver(BeanResolver beanResolver) { 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 @@ private Object resolveSecurityContextFromAnnotation(MethodParameter parameter, C /** * 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 fd51d8ac533..432743d2d94 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.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 @@ * 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 void setBeanResolver(BeanResolver beanResolver) { 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 Mono resolveArgument(MethodParameter parameter, BindingContext bi * @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 @@ private boolean isInvalidType(MethodParameter parameter, Object reactiveSecurity /** * 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 80c33ac9eca..fa35eff2b49 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.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 void metaAnnotationWhenCurrentSecurityWithErrorOnInvalidTypeThenMisMatch( .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 MethodParameter showCurrentAuthentication() { 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 void showCurrentCustomSecurityContext(@CurrentCustomSecurityContext Secur 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 void setAuthentication(Authentication authentication) { } + @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 5556a25ed1b..a9deaf0714c 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 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 void metaAnnotationWhenCurrentSecurityWithErrorOnInvalidTypeThenMisMatch( 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 @@ private Authentication buildAuthenticationWithPrincipal(Object principal) { } + @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;