diff --git a/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java b/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java
index 5093e1bd1d1..e3f5c9297c6 100644
--- a/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java
+++ b/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java
@@ -18,6 +18,7 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
@@ -83,6 +84,7 @@
*
* @param the annotation to search for and synthesize
* @author Josh Cummings
+ * @author DingHao
* @since 6.4
*/
final class UniqueSecurityAnnotationScanner extends AbstractSecurityAnnotationScanner {
@@ -107,7 +109,7 @@ final class UniqueSecurityAnnotationScanner extends Abstra
MergedAnnotation merge(AnnotatedElement element, Class> targetClass) {
if (element instanceof Parameter parameter) {
return this.uniqueParameterAnnotationCache.computeIfAbsent(parameter, (p) -> {
- List> annotations = findDirectAnnotations(p);
+ List> annotations = findParameterAnnotations(p);
return requireUnique(p, annotations);
});
}
@@ -137,6 +139,56 @@ private MergedAnnotation requireUnique(AnnotatedElement element, List> findParameterAnnotations(Parameter current) {
+ List> directAnnotations = findDirectAnnotations(current);
+ if (!directAnnotations.isEmpty()) {
+ return directAnnotations;
+ }
+ Executable executable = current.getDeclaringExecutable();
+ if (executable instanceof Method method) {
+ Class> clazz = method.getDeclaringClass();
+ Set> visited = new HashSet<>();
+ while (clazz != null && clazz != Object.class) {
+ directAnnotations = findClosestParameterAnnotations(method, clazz, current, visited);
+ if (!directAnnotations.isEmpty()) {
+ return directAnnotations;
+ }
+ clazz = clazz.getSuperclass();
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ private List> findClosestParameterAnnotations(Method method, Class> clazz, Parameter current,
+ Set> visited) {
+ if (!visited.add(clazz)) {
+ return Collections.emptyList();
+ }
+ List> annotations = new ArrayList<>(findDirectParameterAnnotations(method, clazz, current));
+ for (Class> ifc : clazz.getInterfaces()) {
+ annotations.addAll(findClosestParameterAnnotations(method, ifc, current, visited));
+ }
+ return annotations;
+ }
+
+ private List> findDirectParameterAnnotations(Method method, Class> clazz, Parameter current) {
+ try {
+ Method methodToUse = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes());
+ for (Parameter parameter : methodToUse.getParameters()) {
+ if (parameter.getName().equals(current.getName())) {
+ List> directAnnotations = findDirectAnnotations(parameter);
+ if (!directAnnotations.isEmpty()) {
+ return directAnnotations;
+ }
+ }
+ }
+ }
+ catch (NoSuchMethodException ex) {
+ // move on
+ }
+ return Collections.emptyList();
+ }
+
private List> findMethodAnnotations(Method method, Class> targetClass) {
// The method may be on an interface, but we need attributes from the target
// class.
diff --git a/core/src/test/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScannerTests.java b/core/src/test/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScannerTests.java
index b1a7a779aac..976e0879ab1 100644
--- a/core/src/test/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScannerTests.java
+++ b/core/src/test/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScannerTests.java
@@ -16,7 +16,13 @@
package org.springframework.security.core.annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.List;
import org.junit.jupiter.api.Test;
@@ -34,6 +40,9 @@ public class UniqueSecurityAnnotationScannerTests {
private UniqueSecurityAnnotationScanner scanner = new UniqueSecurityAnnotationScanner<>(
PreAuthorize.class);
+ private UniqueSecurityAnnotationScanner parameterScanner = new UniqueSecurityAnnotationScanner<>(
+ CustomParameterAnnotation.class);
+
@Test
void scanWhenAnnotationOnInterfaceThenResolves() throws Exception {
Method method = AnnotationOnInterface.class.getDeclaredMethod("method");
@@ -251,6 +260,101 @@ void scanWhenClassInheritingAbstractClassNoAnnotationsThenNoAnnotation() throws
assertThat(preAuthorize).isNull();
}
+ @Test
+ void scanParameterAnnotationWhenAnnotationOnInterface() throws Exception {
+ Parameter parameter = UserService.class.getDeclaredMethod("add", String.class).getParameters()[0];
+ CustomParameterAnnotation customParameterAnnotation = this.parameterScanner.scan(parameter);
+ assertThat(customParameterAnnotation.value()).isEqualTo("one");
+ }
+
+ @Test
+ void scanParameterAnnotationWhenClassInheritingInterfaceAnnotation() throws Exception {
+ Parameter parameter = UserServiceImpl.class.getDeclaredMethod("add", String.class).getParameters()[0];
+ CustomParameterAnnotation customParameterAnnotation = this.parameterScanner.scan(parameter);
+ assertThat(customParameterAnnotation.value()).isEqualTo("one");
+ }
+
+ @Test
+ void scanParameterAnnotationWhenClassOverridingMethodOverridingInterface() throws Exception {
+ Parameter parameter = UserServiceImpl.class.getDeclaredMethod("get", String.class).getParameters()[0];
+ CustomParameterAnnotation customParameterAnnotation = this.parameterScanner.scan(parameter);
+ assertThat(customParameterAnnotation.value()).isEqualTo("five");
+ }
+
+ @Test
+ void scanParameterAnnotationWhenMultipleMethodInheritanceThenException() throws Exception {
+ Parameter parameter = UserServiceImpl.class.getDeclaredMethod("list", String.class).getParameters()[0];
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> this.parameterScanner.scan(parameter));
+ }
+
+ @Test
+ void scanParameterAnnotationWhenInterfaceNoAnnotationsThenException() throws Exception {
+ Parameter parameter = UserServiceImpl.class.getDeclaredMethod("delete", String.class).getParameters()[0];
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> this.parameterScanner.scan(parameter));
+ }
+
+ interface UserService {
+
+ void add(@CustomParameterAnnotation("one") String user);
+
+ List list(@CustomParameterAnnotation("two") String user);
+
+ String get(@CustomParameterAnnotation("three") String user);
+
+ void delete(@CustomParameterAnnotation("five") String user);
+
+ }
+
+ interface OtherUserService {
+
+ List list(@CustomParameterAnnotation("four") String user);
+
+ }
+
+ interface ThirdPartyUserService {
+
+ void delete(@CustomParameterAnnotation("five") String user);
+
+ }
+
+ interface RemoteUserService extends ThirdPartyUserService {
+
+ }
+
+ static class UserServiceImpl implements UserService, OtherUserService, RemoteUserService {
+
+ @Override
+ public void add(String user) {
+
+ }
+
+ @Override
+ public List list(String user) {
+ return List.of(user);
+ }
+
+ @Override
+ public String get(@CustomParameterAnnotation("five") String user) {
+ return user;
+ }
+
+ @Override
+ public void delete(String user) {
+
+ }
+
+ }
+
+ @Target({ ElementType.PARAMETER })
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface CustomParameterAnnotation {
+
+ String value();
+
+ }
+
@PreAuthorize("one")
private interface AnnotationOnInterface {