Skip to content

Commit 195eba4

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

File tree

2 files changed

+88
-2
lines changed

2 files changed

+88
-2
lines changed

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

Lines changed: 68 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;
@@ -103,11 +106,75 @@ final class UniqueSecurityAnnotationScanner<A extends Annotation> extends Abstra
103106
this.types = types;
104107
}
105108

109+
private List<MergedAnnotation<A>> findParameterOnInterface(Method method, Class<?> superOrIfc, Parameter current) {
110+
List<MergedAnnotation<A>> directAnnotations = Collections.emptyList();
111+
for (Method candidate : superOrIfc.getMethods()) {
112+
if (isOverrideFor(method, candidate)) {
113+
for (Parameter parameter : candidate.getParameters()) {
114+
if (parameter.getName().equals(current.getName())) {
115+
directAnnotations = findDirectAnnotations(parameter);
116+
if (!directAnnotations.isEmpty()) {
117+
return directAnnotations;
118+
}
119+
}
120+
}
121+
}
122+
}
123+
return directAnnotations;
124+
}
125+
126+
private List<MergedAnnotation<A>> findParameterAnnotations(Parameter current) {
127+
List<MergedAnnotation<A>> directAnnotations = findDirectAnnotations(current);
128+
if (directAnnotations.isEmpty()) {
129+
Executable executable = current.getDeclaringExecutable();
130+
if (executable instanceof Method method) {
131+
Class<?> clazz = method.getDeclaringClass();
132+
while (clazz != null) {
133+
for (Class<?> ifc : clazz.getInterfaces()) {
134+
directAnnotations = findParameterOnInterface(method, ifc, current);
135+
if (!directAnnotations.isEmpty()) {
136+
return directAnnotations;
137+
}
138+
}
139+
clazz = clazz.getSuperclass();
140+
if (clazz == Object.class) {
141+
clazz = null;
142+
}
143+
if (clazz != null) {
144+
directAnnotations = findParameterOnInterface(method, clazz, current);
145+
if (!directAnnotations.isEmpty()) {
146+
return directAnnotations;
147+
}
148+
}
149+
}
150+
}
151+
}
152+
return directAnnotations;
153+
}
154+
155+
private boolean isOverrideFor(Method method, Method candidate) {
156+
if (!candidate.getName().equals(method.getName())
157+
|| candidate.getParameterCount() != method.getParameterCount()) {
158+
return false;
159+
}
160+
Class<?>[] paramTypes = method.getParameterTypes();
161+
if (Arrays.equals(candidate.getParameterTypes(), paramTypes)) {
162+
return true;
163+
}
164+
for (int i = 0; i < paramTypes.length; i++) {
165+
if (paramTypes[i] != ResolvableType.forMethodParameter(candidate, i, method.getDeclaringClass())
166+
.resolve()) {
167+
return false;
168+
}
169+
}
170+
return true;
171+
}
172+
106173
@Override
107174
MergedAnnotation<A> merge(AnnotatedElement element, Class<?> targetClass) {
108175
if (element instanceof Parameter parameter) {
109176
return this.uniqueParameterAnnotationCache.computeIfAbsent(parameter, (p) -> {
110-
List<MergedAnnotation<A>> annotations = findDirectAnnotations(p);
177+
List<MergedAnnotation<A>> annotations = findParameterAnnotations(p);
111178
return requireUnique(p, annotations);
112179
});
113180
}

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,14 @@ public void resolveArgumentCustomMetaAnnotationTpl() throws Exception {
214214
.isEqualTo(this.expectedPrincipal);
215215
}
216216

217+
@Test
218+
public void resolveArgumentAnnotationFromInterface() {
219+
CustomUserPrincipal principal = new CustomUserPrincipal();
220+
setAuthenticationPrincipal(principal);
221+
assertThat(this.resolver.supportsParameter(getMethodParameter("getUserByInterface", CustomUserPrincipal.class)))
222+
.isTrue();
223+
}
224+
217225
private MethodParameter showUserNoAnnotation() {
218226
return getMethodParameter("showUserNoAnnotation", String.class);
219227
}
@@ -312,7 +320,18 @@ private void setAuthenticationPrincipal(Object principal) {
312320

313321
}
314322

315-
public static class TestController {
323+
interface UserApi {
324+
325+
String getUserByInterface(@AuthenticationPrincipal CustomUserPrincipal user);
326+
327+
}
328+
329+
public static class TestController implements UserApi {
330+
331+
@Override
332+
public String getUserByInterface(CustomUserPrincipal user) {
333+
return "";
334+
}
316335

317336
public void showUserNoAnnotation(String user) {
318337
}

0 commit comments

Comments
 (0)