Skip to content

Commit 0d0f8ec

Browse files
committed
Improve @AuthenticationPrincipal meta-annotations
Closes gh-15286
1 parent 6bd2f1c commit 0d0f8ec

File tree

3 files changed

+176
-8
lines changed

3 files changed

+176
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.authorization.method;
18+
19+
/**
20+
* A component for configuring the expression attribute template of the parsed
21+
* AuthenticationPrincipal annotation
22+
*
23+
* @author DingHao
24+
* @since 6.4
25+
* @see org.springframework.security.core.annotation.AuthenticationPrincipal
26+
*/
27+
public final class AuthenticationPrincipalTemplateDefaults {
28+
29+
private boolean ignoreUnknown = true;
30+
31+
/**
32+
* Whether template resolution should ignore placeholders it doesn't recognize.
33+
* <p>
34+
* By default, this value is <code>true</code>.
35+
*/
36+
public boolean isIgnoreUnknown() {
37+
return this.ignoreUnknown;
38+
}
39+
40+
/**
41+
* Configure template resolution to ignore unknown placeholders. When set to
42+
* <code>false</code>, template resolution will throw an exception for unknown
43+
* placeholders.
44+
* <p>
45+
* By default, this value is <code>true</code>.
46+
* @param ignoreUnknown - whether to ignore unknown placeholders parameters
47+
*/
48+
public void setIgnoreUnknown(boolean ignoreUnknown) {
49+
this.ignoreUnknown = ignoreUnknown;
50+
}
51+
52+
}

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

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,33 @@
1717
package org.springframework.security.messaging.context;
1818

1919
import java.lang.annotation.Annotation;
20+
import java.lang.reflect.AnnotatedElement;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import java.util.concurrent.ConcurrentHashMap;
24+
import java.util.function.Function;
2025

2126
import org.springframework.core.MethodParameter;
22-
import org.springframework.core.annotation.AnnotationUtils;
27+
import org.springframework.core.annotation.MergedAnnotation;
28+
import org.springframework.core.annotation.MergedAnnotations;
29+
import org.springframework.core.annotation.RepeatableContainers;
30+
import org.springframework.core.convert.support.DefaultConversionService;
2331
import org.springframework.expression.Expression;
2432
import org.springframework.expression.ExpressionParser;
2533
import org.springframework.expression.spel.standard.SpelExpressionParser;
2634
import org.springframework.expression.spel.support.StandardEvaluationContext;
35+
import org.springframework.lang.NonNull;
2736
import org.springframework.messaging.Message;
2837
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
38+
import org.springframework.security.authorization.method.AuthenticationPrincipalTemplateDefaults;
2939
import org.springframework.security.core.Authentication;
3040
import org.springframework.security.core.annotation.AuthenticationPrincipal;
3141
import org.springframework.security.core.context.SecurityContextHolder;
3242
import org.springframework.security.core.context.SecurityContextHolderStrategy;
3343
import org.springframework.stereotype.Controller;
3444
import org.springframework.util.Assert;
3545
import org.springframework.util.ClassUtils;
46+
import org.springframework.util.PropertyPlaceholderHelper;
3647
import org.springframework.util.StringUtils;
3748

3849
/**
@@ -83,15 +94,20 @@
8394
* </pre>
8495
*
8596
* @author Rob Winch
97+
* @author DingHao
8698
* @since 4.0
8799
*/
88100
public final class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
89101

90102
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
91103
.getContextHolderStrategy();
92104

105+
private final Map<MethodParameter, Annotation> cachedAttributes = new ConcurrentHashMap<>();
106+
93107
private ExpressionParser parser = new SpelExpressionParser();
94108

109+
private AuthenticationPrincipalTemplateDefaults principalTemplateDefaults = new AuthenticationPrincipalTemplateDefaults();
110+
95111
@Override
96112
public boolean supportsParameter(MethodParameter parameter) {
97113
return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null;
@@ -133,26 +149,74 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy secur
133149
this.securityContextHolderStrategy = securityContextHolderStrategy;
134150
}
135151

152+
/**
153+
* Configure AuthenticationPrincipal template resolution
154+
* <p>
155+
* By default, this value is <code>null</code>, which indicates that templates should
156+
* not be resolved.
157+
* @param principalTemplateDefaults - whether to resolve AuthenticationPrincipal
158+
* templates parameters
159+
* @since 6.4
160+
*/
161+
public void setTemplateDefaults(@NonNull AuthenticationPrincipalTemplateDefaults principalTemplateDefaults) {
162+
Assert.notNull(principalTemplateDefaults, "principalTemplateDefaults cannot be null");
163+
this.principalTemplateDefaults = principalTemplateDefaults;
164+
}
165+
136166
/**
137167
* Obtains the specified {@link Annotation} on the specified {@link MethodParameter}.
138168
* @param annotationClass the class of the {@link Annotation} to find on the
139169
* {@link MethodParameter}
140170
* @param parameter the {@link MethodParameter} to search for an {@link Annotation}
141171
* @return the {@link Annotation} that was found or null.
142172
*/
173+
@SuppressWarnings("unchecked")
143174
private <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass, MethodParameter parameter) {
175+
return (T) this.cachedAttributes.computeIfAbsent(parameter,
176+
methodParameter -> findMethodAnnotation(annotationClass, methodParameter,
177+
this.principalTemplateDefaults));
178+
}
179+
180+
private static <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass, MethodParameter parameter,
181+
AuthenticationPrincipalTemplateDefaults principalTemplateDefaults) {
144182
T annotation = parameter.getParameterAnnotation(annotationClass);
145183
if (annotation != null) {
146184
return annotation;
147185
}
148-
Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
149-
for (Annotation toSearch : annotationsToSearch) {
150-
annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass);
151-
if (annotation != null) {
152-
return annotation;
186+
return MergedAnnotations
187+
.from(parameter.getParameter(), MergedAnnotations.SearchStrategy.TYPE_HIERARCHY,
188+
RepeatableContainers.none())
189+
.stream(annotationClass)
190+
.map(mapper(annotationClass, principalTemplateDefaults.isIgnoreUnknown(), "expression"))
191+
.findFirst()
192+
.orElse(null);
193+
}
194+
195+
private static <T extends Annotation> Function<MergedAnnotation<T>, T> mapper(Class<T> annotationClass,
196+
boolean ignoreUnresolvablePlaceholders, String... attrs) {
197+
return (mergedAnnotation) -> {
198+
MergedAnnotation<?> metaSource = mergedAnnotation.getMetaSource();
199+
if (metaSource == null) {
200+
return mergedAnnotation.synthesize();
153201
}
154-
}
155-
return null;
202+
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("{", "}", null, null,
203+
ignoreUnresolvablePlaceholders);
204+
Map<String, String> stringProperties = new HashMap<>();
205+
for (Map.Entry<String, Object> property : metaSource.asMap().entrySet()) {
206+
String key = property.getKey();
207+
Object value = property.getValue();
208+
String asString = (value instanceof String) ? (String) value
209+
: DefaultConversionService.getSharedInstance().convert(value, String.class);
210+
stringProperties.put(key, asString);
211+
}
212+
Map<String, Object> attrMap = mergedAnnotation.asMap();
213+
Map<String, Object> properties = new HashMap<>(attrMap);
214+
for (String attr : attrs) {
215+
properties.put(attr, helper.replacePlaceholders((String) attrMap.get(attr), stringProperties::get));
216+
}
217+
return MergedAnnotation.of((AnnotatedElement) mergedAnnotation.getSource(), annotationClass, properties)
218+
.synthesize();
219+
};
156220
}
157221

158222
}

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
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;
3132
import org.springframework.security.core.annotation.AuthenticationPrincipal;
3233
import org.springframework.security.core.authority.AuthorityUtils;
@@ -167,6 +168,22 @@ public void resolveArgumentObject() throws Exception {
167168
assertThat(this.resolver.resolveArgument(showUserAnnotationObject(), null)).isEqualTo(this.expectedPrincipal);
168169
}
169170

171+
@Test
172+
public void resolveArgumentCustomMetaAnnotation() throws Exception {
173+
CustomUserPrincipal principal = new CustomUserPrincipal();
174+
setAuthenticationPrincipal(principal);
175+
this.expectedPrincipal = principal.id;
176+
assertThat(this.resolver.resolveArgument(showUserCustomMetaAnnotation(), null)).isEqualTo(principal.id);
177+
}
178+
179+
@Test
180+
public void resolveArgumentCustomMetaAnnotationTpl() throws Exception {
181+
CustomUserPrincipal principal = new CustomUserPrincipal();
182+
setAuthenticationPrincipal(principal);
183+
this.expectedPrincipal = principal.id;
184+
assertThat(this.resolver.resolveArgument(showUserCustomMetaAnnotationTpl(), null)).isEqualTo(principal.id);
185+
}
186+
170187
private MethodParameter showUserNoAnnotation() {
171188
return getMethodParameter("showUserNoAnnotation", String.class);
172189
}
@@ -195,6 +212,14 @@ private MethodParameter showUserCustomAnnotation() {
195212
return getMethodParameter("showUserCustomAnnotation", CustomUserPrincipal.class);
196213
}
197214

215+
private MethodParameter showUserCustomMetaAnnotation() {
216+
return getMethodParameter("showUserCustomMetaAnnotation", int.class);
217+
}
218+
219+
private MethodParameter showUserCustomMetaAnnotationTpl() {
220+
return getMethodParameter("showUserCustomMetaAnnotationTpl", int.class);
221+
}
222+
198223
private MethodParameter showUserSpel() {
199224
return getMethodParameter("showUserSpel", String.class);
200225
}
@@ -236,6 +261,23 @@ private void setAuthenticationPrincipal(Object principal) {
236261

237262
}
238263

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

241283
public void showUserNoAnnotation(String user) {
@@ -260,6 +302,12 @@ public void showUserAnnotation(@AuthenticationPrincipal CustomUserPrincipal user
260302
public void showUserCustomAnnotation(@CurrentUser CustomUserPrincipal user) {
261303
}
262304

305+
public void showUserCustomMetaAnnotation(@CurrentUser2(expression = "id") int userId) {
306+
}
307+
308+
public void showUserCustomMetaAnnotationTpl(@CurrentUser3(property = "id") int userId) {
309+
}
310+
263311
public void showUserAnnotation(@AuthenticationPrincipal Object user) {
264312
}
265313

@@ -281,6 +329,10 @@ static class CustomUserPrincipal {
281329

282330
public final int id = 1;
283331

332+
public Object getPrincipal() {
333+
return this;
334+
}
335+
284336
}
285337

286338
public static class CopyUserPrincipal {

0 commit comments

Comments
 (0)