Skip to content

Commit 760ec9c

Browse files
committed
Support @AuthenticationPrincipal on interfaces
Closes gh-16177
1 parent dc82a6e commit 760ec9c

File tree

3 files changed

+123
-7
lines changed

3 files changed

+123
-7
lines changed

core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818

1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.AnnotatedElement;
21+
import java.lang.reflect.Executable;
2122
import java.lang.reflect.Method;
2223
import java.lang.reflect.Parameter;
2324
import java.util.ArrayList;
25+
import java.util.Arrays;
2426
import java.util.Collections;
2527
import java.util.HashSet;
2628
import java.util.List;
@@ -29,6 +31,7 @@
2931
import java.util.concurrent.ConcurrentHashMap;
3032

3133
import org.springframework.core.MethodClassKey;
34+
import org.springframework.core.ResolvableType;
3235
import org.springframework.core.annotation.AnnotationConfigurationException;
3336
import org.springframework.core.annotation.MergedAnnotation;
3437
import org.springframework.core.annotation.MergedAnnotations;
@@ -107,7 +110,7 @@ final class UniqueSecurityAnnotationScanner<A extends Annotation> extends Abstra
107110
MergedAnnotation<A> merge(AnnotatedElement element, Class<?> targetClass) {
108111
if (element instanceof Parameter parameter) {
109112
return this.uniqueParameterAnnotationCache.computeIfAbsent(parameter, (p) -> {
110-
List<MergedAnnotation<A>> annotations = findDirectAnnotations(p);
113+
List<MergedAnnotation<A>> annotations = findParameterAnnotations(p);
111114
return requireUnique(p, annotations);
112115
});
113116
}
@@ -137,6 +140,64 @@ private MergedAnnotation<A> requireUnique(AnnotatedElement element, List<MergedA
137140
};
138141
}
139142

143+
private List<MergedAnnotation<A>> findParameterAnnotations(Method method, Class<?> superOrIfc, Parameter current) {
144+
List<MergedAnnotation<A>> directAnnotations = Collections.emptyList();
145+
for (Method candidate : superOrIfc.getMethods()) {
146+
if (isOverrideFor(method, candidate)) {
147+
for (Parameter parameter : candidate.getParameters()) {
148+
if (parameter.getName().equals(current.getName())) {
149+
directAnnotations = findDirectAnnotations(parameter);
150+
if (!directAnnotations.isEmpty()) {
151+
return directAnnotations;
152+
}
153+
}
154+
}
155+
}
156+
}
157+
return directAnnotations;
158+
}
159+
160+
private List<MergedAnnotation<A>> findParameterAnnotations(Parameter current) {
161+
List<MergedAnnotation<A>> directAnnotations = new ArrayList<>(findDirectAnnotations(current));
162+
if (directAnnotations.isEmpty()) {
163+
Executable executable = current.getDeclaringExecutable();
164+
if (executable instanceof Method method) {
165+
Class<?> clazz = method.getDeclaringClass();
166+
while (clazz != null) {
167+
for (Class<?> ifc : clazz.getInterfaces()) {
168+
directAnnotations.addAll(findParameterAnnotations(method, ifc, current));
169+
}
170+
clazz = clazz.getSuperclass();
171+
if (clazz == Object.class) {
172+
clazz = null;
173+
}
174+
if (clazz != null) {
175+
directAnnotations.addAll(findParameterAnnotations(method, clazz, current));
176+
}
177+
}
178+
}
179+
}
180+
return directAnnotations;
181+
}
182+
183+
private boolean isOverrideFor(Method method, Method candidate) {
184+
if (!candidate.getName().equals(method.getName())
185+
|| candidate.getParameterCount() != method.getParameterCount()) {
186+
return false;
187+
}
188+
Class<?>[] paramTypes = method.getParameterTypes();
189+
if (Arrays.equals(candidate.getParameterTypes(), paramTypes)) {
190+
return true;
191+
}
192+
for (int i = 0; i < paramTypes.length; i++) {
193+
if (paramTypes[i] != ResolvableType.forMethodParameter(candidate, i, method.getDeclaringClass())
194+
.resolve()) {
195+
return false;
196+
}
197+
}
198+
return true;
199+
}
200+
140201
private List<MergedAnnotation<A>> findMethodAnnotations(Method method, Class<?> targetClass) {
141202
// The method may be on an interface, but we need attributes from the target
142203
// class.

web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.lang.annotation.Annotation;
2020

2121
import org.springframework.core.MethodParameter;
22+
import org.springframework.core.annotation.AnnotationUtils;
2223
import org.springframework.expression.BeanResolver;
2324
import org.springframework.expression.Expression;
2425
import org.springframework.expression.ExpressionParser;
@@ -98,8 +99,12 @@ public final class AuthenticationPrincipalArgumentResolver implements HandlerMet
9899

99100
private ExpressionParser parser = new SpelExpressionParser();
100101

102+
private final Class<AuthenticationPrincipal> annotationType = AuthenticationPrincipal.class;
103+
101104
private SecurityAnnotationScanner<AuthenticationPrincipal> scanner = SecurityAnnotationScanners
102-
.requireUnique(AuthenticationPrincipal.class);
105+
.requireUnique(this.annotationType);
106+
107+
private boolean useAnnotationTemplate = false;
103108

104109
private BeanResolver beanResolver;
105110

@@ -164,7 +169,8 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy secur
164169
* @since 6.4
165170
*/
166171
public void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) {
167-
this.scanner = SecurityAnnotationScanners.requireUnique(AuthenticationPrincipal.class, templateDefaults);
172+
this.useAnnotationTemplate = templateDefaults != null;
173+
this.scanner = SecurityAnnotationScanners.requireUnique(this.annotationType, templateDefaults);
168174
}
169175

170176
/**
@@ -173,9 +179,22 @@ public void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDef
173179
* @param parameter the {@link MethodParameter} to search for an {@link Annotation}
174180
* @return the {@link Annotation} that was found or null.
175181
*/
176-
@SuppressWarnings("unchecked")
177-
private <T extends Annotation> T findMethodAnnotation(MethodParameter parameter) {
178-
return (T) this.scanner.scan(parameter.getParameter());
182+
private AuthenticationPrincipal findMethodAnnotation(MethodParameter parameter) {
183+
if (this.useAnnotationTemplate) {
184+
return this.scanner.scan(parameter.getParameter());
185+
}
186+
AuthenticationPrincipal annotation = parameter.getParameterAnnotation(this.annotationType);
187+
if (annotation != null) {
188+
return annotation;
189+
}
190+
Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
191+
for (Annotation toSearch : annotationsToSearch) {
192+
annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), this.annotationType);
193+
if (annotation != null) {
194+
return annotation;
195+
}
196+
}
197+
return null;
179198
}
180199

181200
}

web/src/test/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolverTests.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import org.springframework.core.MethodParameter;
3030
import org.springframework.core.annotation.AliasFor;
31+
import org.springframework.core.annotation.AnnotationConfigurationException;
3132
import org.springframework.expression.BeanResolver;
3233
import org.springframework.security.authentication.TestingAuthenticationToken;
3334
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
@@ -214,6 +215,17 @@ public void resolveArgumentCustomMetaAnnotationTpl() throws Exception {
214215
.isEqualTo(this.expectedPrincipal);
215216
}
216217

218+
@Test
219+
public void resolveArgumentAnnotationFromInterface() {
220+
CustomUserPrincipal principal = new CustomUserPrincipal();
221+
setAuthenticationPrincipal(principal);
222+
this.resolver.setTemplateDefaults(new AnnotationTemplateExpressionDefaults());
223+
assertThat(this.resolver.supportsParameter(getMethodParameter("getUserByInterface", CustomUserPrincipal.class)))
224+
.isTrue();
225+
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> this.resolver
226+
.resolveArgument(getMethodParameter("username", CustomUserPrincipal.class), null, null, null));
227+
}
228+
217229
private MethodParameter showUserNoAnnotation() {
218230
return getMethodParameter("showUserNoAnnotation", String.class);
219231
}
@@ -312,7 +324,31 @@ private void setAuthenticationPrincipal(Object principal) {
312324

313325
}
314326

315-
public static class TestController {
327+
interface UserApi {
328+
329+
String getUserByInterface(@AuthenticationPrincipal CustomUserPrincipal user);
330+
331+
Object username(@AuthenticationPrincipal CustomUserPrincipal user);
332+
333+
}
334+
335+
interface UserPublicApi {
336+
337+
Object username(@AuthenticationPrincipal CustomUserPrincipal user);
338+
339+
}
340+
341+
public static class TestController implements UserApi, UserPublicApi {
342+
343+
@Override
344+
public String getUserByInterface(CustomUserPrincipal user) {
345+
return "";
346+
}
347+
348+
@Override
349+
public Object username(CustomUserPrincipal user) {
350+
return user.getPrincipal();
351+
}
316352

317353
public void showUserNoAnnotation(String user) {
318354
}

0 commit comments

Comments
 (0)