Skip to content

Commit 747299c

Browse files
committed
Improve @AuthenticationPrincipal meta-annotations
Closes gh-15286
1 parent e2f98db commit 747299c

File tree

8 files changed

+341
-68
lines changed

8 files changed

+341
-68
lines changed

messaging/src/main/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolver.java

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,20 @@
1717
package org.springframework.security.messaging.context;
1818

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

2123
import org.springframework.core.MethodParameter;
22-
import org.springframework.core.annotation.AnnotationUtils;
2324
import org.springframework.expression.Expression;
2425
import org.springframework.expression.ExpressionParser;
2526
import org.springframework.expression.spel.standard.SpelExpressionParser;
2627
import org.springframework.expression.spel.support.StandardEvaluationContext;
2728
import org.springframework.messaging.Message;
2829
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
2930
import org.springframework.security.core.Authentication;
31+
import org.springframework.security.core.annotation.AnnotationSynthesizer;
32+
import org.springframework.security.core.annotation.AnnotationSynthesizers;
33+
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
3034
import org.springframework.security.core.annotation.AuthenticationPrincipal;
3135
import org.springframework.security.core.context.SecurityContextHolder;
3236
import org.springframework.security.core.context.SecurityContextHolderStrategy;
@@ -83,18 +87,24 @@
8387
* </pre>
8488
*
8589
* @author Rob Winch
90+
* @author DingHao
8691
* @since 4.0
8792
*/
8893
public final class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
8994

9095
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
9196
.getContextHolderStrategy();
9297

98+
private final Map<MethodParameter, Annotation> cachedAttributes = new ConcurrentHashMap<>();
99+
93100
private ExpressionParser parser = new SpelExpressionParser();
94101

102+
private AnnotationSynthesizer<AuthenticationPrincipal> synthesizer = AnnotationSynthesizers
103+
.requireUnique(AuthenticationPrincipal.class);
104+
95105
@Override
96106
public boolean supportsParameter(MethodParameter parameter) {
97-
return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null;
107+
return findMethodAnnotation(parameter) != null;
98108
}
99109

100110
@Override
@@ -104,7 +114,7 @@ public Object resolveArgument(MethodParameter parameter, Message<?> message) {
104114
return null;
105115
}
106116
Object principal = authentication.getPrincipal();
107-
AuthenticationPrincipal authPrincipal = findMethodAnnotation(AuthenticationPrincipal.class, parameter);
117+
AuthenticationPrincipal authPrincipal = findMethodAnnotation(parameter);
108118
String expressionToParse = authPrincipal.expression();
109119
if (StringUtils.hasLength(expressionToParse)) {
110120
StandardEvaluationContext context = new StandardEvaluationContext();
@@ -133,26 +143,29 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy secur
133143
this.securityContextHolderStrategy = securityContextHolderStrategy;
134144
}
135145

146+
/**
147+
* Configure AuthenticationPrincipal template resolution
148+
* <p>
149+
* By default, this value is <code>null</code>, which indicates that templates should
150+
* not be resolved.
151+
* @param templateDefaults - whether to resolve AuthenticationPrincipal templates
152+
* parameters
153+
* @since 6.4
154+
*/
155+
public void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) {
156+
this.synthesizer = AnnotationSynthesizers.requireUnique(AuthenticationPrincipal.class, templateDefaults);
157+
}
158+
136159
/**
137160
* Obtains the specified {@link Annotation} on the specified {@link MethodParameter}.
138-
* @param annotationClass the class of the {@link Annotation} to find on the
139161
* {@link MethodParameter}
140162
* @param parameter the {@link MethodParameter} to search for an {@link Annotation}
141163
* @return the {@link Annotation} that was found or null.
142164
*/
143-
private <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass, MethodParameter parameter) {
144-
T annotation = parameter.getParameterAnnotation(annotationClass);
145-
if (annotation != null) {
146-
return annotation;
147-
}
148-
Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
149-
for (Annotation toSearch : annotationsToSearch) {
150-
annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass);
151-
if (annotation != null) {
152-
return annotation;
153-
}
154-
}
155-
return null;
165+
@SuppressWarnings("unchecked")
166+
private <T extends Annotation> T findMethodAnnotation(MethodParameter parameter) {
167+
return (T) this.cachedAttributes.computeIfAbsent(parameter,
168+
(methodParameter) -> this.synthesizer.synthesize(methodParameter.getParameter()));
156169
}
157170

158171
}

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

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

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

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

102+
private final Map<MethodParameter, Annotation> cachedAttributes = new ConcurrentHashMap<>();
103+
97104
private ExpressionParser parser = new SpelExpressionParser();
98105

106+
private AnnotationSynthesizer<AuthenticationPrincipal> synthesizer = AnnotationSynthesizers
107+
.requireUnique(AuthenticationPrincipal.class);
108+
99109
private BeanResolver beanResolver;
100110

101111
private ReactiveAdapterRegistry adapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
@@ -120,7 +130,7 @@ public void setAdapterRegistry(ReactiveAdapterRegistry adapterRegistry) {
120130

121131
@Override
122132
public boolean supportsParameter(MethodParameter parameter) {
123-
return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null;
133+
return findMethodAnnotation(parameter) != null;
124134
}
125135

126136
@Override
@@ -138,7 +148,7 @@ public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> messag
138148
}
139149

140150
private Object resolvePrincipal(MethodParameter parameter, Object principal) {
141-
AuthenticationPrincipal authPrincipal = findMethodAnnotation(AuthenticationPrincipal.class, parameter);
151+
AuthenticationPrincipal authPrincipal = findMethodAnnotation(parameter);
142152
String expressionToParse = authPrincipal.expression();
143153
if (StringUtils.hasLength(expressionToParse)) {
144154
StandardEvaluationContext context = new StandardEvaluationContext();
@@ -174,26 +184,29 @@ private boolean isInvalidType(MethodParameter parameter, Object principal) {
174184
return !ClassUtils.isAssignable(typeToCheck, principal.getClass());
175185
}
176186

187+
/**
188+
* Configure AuthenticationPrincipal template resolution
189+
* <p>
190+
* By default, this value is <code>null</code>, which indicates that templates should
191+
* not be resolved.
192+
* @param templateDefaults - whether to resolve AuthenticationPrincipal templates
193+
* parameters
194+
* @since 6.4
195+
*/
196+
public void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) {
197+
this.synthesizer = AnnotationSynthesizers.requireUnique(AuthenticationPrincipal.class, templateDefaults);
198+
}
199+
177200
/**
178201
* Obtains the specified {@link Annotation} on the specified {@link MethodParameter}.
179-
* @param annotationClass the class of the {@link Annotation} to find on the
180202
* {@link MethodParameter}
181203
* @param parameter the {@link MethodParameter} to search for an {@link Annotation}
182204
* @return the {@link Annotation} that was found or null.
183205
*/
184-
private <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass, MethodParameter parameter) {
185-
T annotation = parameter.getParameterAnnotation(annotationClass);
186-
if (annotation != null) {
187-
return annotation;
188-
}
189-
Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
190-
for (Annotation toSearch : annotationsToSearch) {
191-
annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass);
192-
if (annotation != null) {
193-
return annotation;
194-
}
195-
}
196-
return null;
206+
@SuppressWarnings("unchecked")
207+
private <T extends Annotation> T findMethodAnnotation(MethodParameter parameter) {
208+
return (T) this.cachedAttributes.computeIfAbsent(parameter,
209+
(methodParameter) -> this.synthesizer.synthesize(methodParameter.getParameter()));
197210
}
198211

199212
}

messaging/src/test/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolverTests.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
import org.junit.jupiter.api.Test;
2828

2929
import org.springframework.core.MethodParameter;
30+
import org.springframework.core.annotation.AliasFor;
3031
import org.springframework.security.authentication.TestingAuthenticationToken;
32+
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
3133
import org.springframework.security.core.annotation.AuthenticationPrincipal;
3234
import org.springframework.security.core.authority.AuthorityUtils;
3335
import org.springframework.security.core.context.SecurityContextHolder;
@@ -167,6 +169,23 @@ public void resolveArgumentObject() throws Exception {
167169
assertThat(this.resolver.resolveArgument(showUserAnnotationObject(), null)).isEqualTo(this.expectedPrincipal);
168170
}
169171

172+
@Test
173+
public void resolveArgumentCustomMetaAnnotation() throws Exception {
174+
CustomUserPrincipal principal = new CustomUserPrincipal();
175+
setAuthenticationPrincipal(principal);
176+
this.expectedPrincipal = principal.id;
177+
assertThat(this.resolver.resolveArgument(showUserCustomMetaAnnotation(), null)).isEqualTo(principal.id);
178+
}
179+
180+
@Test
181+
public void resolveArgumentCustomMetaAnnotationTpl() throws Exception {
182+
CustomUserPrincipal principal = new CustomUserPrincipal();
183+
setAuthenticationPrincipal(principal);
184+
this.resolver.setTemplateDefaults(new AnnotationTemplateExpressionDefaults());
185+
this.expectedPrincipal = principal.id;
186+
assertThat(this.resolver.resolveArgument(showUserCustomMetaAnnotationTpl(), null)).isEqualTo(principal.id);
187+
}
188+
170189
private MethodParameter showUserNoAnnotation() {
171190
return getMethodParameter("showUserNoAnnotation", String.class);
172191
}
@@ -195,6 +214,14 @@ private MethodParameter showUserCustomAnnotation() {
195214
return getMethodParameter("showUserCustomAnnotation", CustomUserPrincipal.class);
196215
}
197216

217+
private MethodParameter showUserCustomMetaAnnotation() {
218+
return getMethodParameter("showUserCustomMetaAnnotation", int.class);
219+
}
220+
221+
private MethodParameter showUserCustomMetaAnnotationTpl() {
222+
return getMethodParameter("showUserCustomMetaAnnotationTpl", int.class);
223+
}
224+
198225
private MethodParameter showUserSpel() {
199226
return getMethodParameter("showUserSpel", String.class);
200227
}
@@ -236,6 +263,23 @@ private void setAuthenticationPrincipal(Object principal) {
236263

237264
}
238265

266+
@Retention(RetentionPolicy.RUNTIME)
267+
@AuthenticationPrincipal
268+
public @interface CurrentUser2 {
269+
270+
@AliasFor(annotation = AuthenticationPrincipal.class)
271+
String expression() default "";
272+
273+
}
274+
275+
@Retention(RetentionPolicy.RUNTIME)
276+
@AuthenticationPrincipal(expression = "principal.{property}")
277+
public @interface CurrentUser3 {
278+
279+
String property() default "";
280+
281+
}
282+
239283
public static class TestController {
240284

241285
public void showUserNoAnnotation(String user) {
@@ -260,6 +304,12 @@ public void showUserAnnotation(@AuthenticationPrincipal CustomUserPrincipal user
260304
public void showUserCustomAnnotation(@CurrentUser CustomUserPrincipal user) {
261305
}
262306

307+
public void showUserCustomMetaAnnotation(@CurrentUser2(expression = "principal.id") int userId) {
308+
}
309+
310+
public void showUserCustomMetaAnnotationTpl(@CurrentUser3(property = "id") int userId) {
311+
}
312+
263313
public void showUserAnnotation(@AuthenticationPrincipal Object user) {
264314
}
265315

@@ -281,6 +331,10 @@ static class CustomUserPrincipal {
281331

282332
public final int id = 1;
283333

334+
public Object getPrincipal() {
335+
return this;
336+
}
337+
284338
}
285339

286340
public static class CopyUserPrincipal {

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323
import reactor.core.publisher.Mono;
2424

2525
import org.springframework.core.MethodParameter;
26+
import org.springframework.core.annotation.AliasFor;
2627
import org.springframework.core.annotation.SynthesizingMethodParameter;
2728
import org.springframework.security.authentication.TestAuthentication;
2829
import org.springframework.security.authentication.TestingAuthenticationToken;
2930
import org.springframework.security.core.Authentication;
31+
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
3032
import org.springframework.security.core.annotation.AuthenticationPrincipal;
3133
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
3234
import org.springframework.security.core.userdetails.UserDetails;
@@ -141,10 +143,56 @@ private MethodParameter arg0(String methodName) {
141143

142144
}
143145

146+
@Test
147+
public void resolveArgumentCustomMetaAnnotation() {
148+
CustomUserPrincipal principal = new CustomUserPrincipal();
149+
Mono<Object> result = this.resolver.resolveArgument(arg0("showUserCustomMetaAnnotation"), null)
150+
.contextWrite(ReactiveSecurityContextHolder
151+
.withAuthentication(new TestingAuthenticationToken(principal, "password", "ROLE_USER")));
152+
assertThat(result.block()).isEqualTo(principal.id);
153+
}
154+
155+
@Test
156+
public void resolveArgumentCustomMetaAnnotationTpl() {
157+
CustomUserPrincipal principal = new CustomUserPrincipal();
158+
this.resolver.setTemplateDefaults(new AnnotationTemplateExpressionDefaults());
159+
Mono<Object> result = this.resolver.resolveArgument(arg0("showUserCustomMetaAnnotationTpl"), null)
160+
.contextWrite(ReactiveSecurityContextHolder
161+
.withAuthentication(new TestingAuthenticationToken(principal, "password", "ROLE_USER")));
162+
assertThat(result.block()).isEqualTo(principal.id);
163+
}
164+
165+
public void showUserCustomMetaAnnotation(@CurrentUser2(expression = "principal.id") int userId) {
166+
}
167+
168+
public void showUserCustomMetaAnnotationTpl(@CurrentUser3(property = "id") int userId) {
169+
}
170+
144171
static class CustomUserPrincipal {
145172

173+
146174
public final int id = 1;
147175

176+
public Object getPrincipal() {
177+
return this;
178+
}
179+
180+
}
181+
@Retention(RetentionPolicy.RUNTIME)
182+
@AuthenticationPrincipal
183+
public @interface CurrentUser2 {
184+
185+
186+
@AliasFor(annotation = AuthenticationPrincipal.class)
187+
String expression() default "";
188+
189+
}
190+
@Retention(RetentionPolicy.RUNTIME)
191+
@AuthenticationPrincipal(expression = "principal.{property}")
192+
public @interface CurrentUser3 {
193+
194+
String property() default "";
195+
148196
}
149197

150198
}

0 commit comments

Comments
 (0)