diff --git a/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java b/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java
index 76be0842789..7ee6e58d74f 100644
--- a/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java
+++ b/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -62,6 +62,7 @@
*
* @param the annotation to search for and synthesize
* @author Josh Cummings
+ * @author DingHao
* @since 6.4
*/
final class ExpressionTemplateSecurityAnnotationScanner
@@ -116,27 +117,35 @@ private MergedAnnotation resolvePlaceholders(MergedAnnotation mergedAnnota
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("{", "}", null, null,
this.templateDefaults.isIgnoreUnknown());
Map properties = new HashMap<>(mergedAnnotation.asMap());
- Map metaAnnotationProperties = mergedAnnotation.getMetaSource().asMap();
- Map stringProperties = new HashMap<>();
- for (Map.Entry property : metaAnnotationProperties.entrySet()) {
- String key = property.getKey();
- Object value = property.getValue();
- String asString = (value instanceof String) ? (String) value
- : conversionService.convert(value, String.class);
- stringProperties.put(key, asString);
- }
- Map annotationProperties = mergedAnnotation.asMap();
- for (Map.Entry annotationProperty : annotationProperties.entrySet()) {
+ Map metaAnnotationProperties = extractMetaAnnotationProperties(mergedAnnotation);
+ for (Map.Entry annotationProperty : mergedAnnotation.asMap().entrySet()) {
if (!(annotationProperty.getValue() instanceof String expression)) {
continue;
}
- String value = helper.replacePlaceholders(expression, stringProperties::get);
+ String value = helper.replacePlaceholders(expression, metaAnnotationProperties::get);
properties.put(annotationProperty.getKey(), value);
}
AnnotatedElement annotatedElement = (AnnotatedElement) mergedAnnotation.getSource();
return MergedAnnotation.of(annotatedElement, this.type, properties);
}
+ private Map extractMetaAnnotationProperties(MergedAnnotation mergedAnnotation) {
+ Map stringProperties = new HashMap<>();
+ Map metaAnnotationProperties = new HashMap<>();
+ MergedAnnotation> metaSource = mergedAnnotation.getMetaSource();
+ while (metaSource != null) {
+ metaAnnotationProperties.putAll(metaSource.asMap());
+ metaSource = metaSource.getMetaSource();
+ }
+ for (Map.Entry property : metaAnnotationProperties.entrySet()) {
+ Object value = property.getValue();
+ String valueString = (value instanceof String) ? (String) value
+ : conversionService.convert(value, String.class);
+ stringProperties.put(property.getKey(), valueString);
+ }
+ return stringProperties;
+ }
+
static class ClassToStringConverter implements GenericConverter {
@Override
diff --git a/core/src/test/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScannerTests.java b/core/src/test/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScannerTests.java
new file mode 100644
index 00000000000..c6be904ecbb
--- /dev/null
+++ b/core/src/test/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScannerTests.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.core.annotation;
+
+import java.lang.annotation.Documented;
+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 org.junit.jupiter.api.Test;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link ExpressionTemplateSecurityAnnotationScanner}
+ *
+ * @author DingHao
+ */
+public class ExpressionTemplateSecurityAnnotationScannerTests {
+
+ private ExpressionTemplateSecurityAnnotationScanner scanner = new ExpressionTemplateSecurityAnnotationScanner<>(
+ PreAuthorize.class, new AnnotationTemplateExpressionDefaults());
+
+ @Test
+ void parseMultipleMetaSourceAnnotationParameter() throws Exception {
+ Method method = MessageService.class.getDeclaredMethod("sayHello", String.class);
+ PreAuthorize preAuthorize = this.scanner.scan(method, method.getDeclaringClass());
+ assertThat(preAuthorize.value()).isEqualTo("check(#name)");
+ }
+
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ ElementType.TYPE, ElementType.METHOD })
+ @PreAuthorize("check({object})")
+ @interface HasPermission {
+
+ String object();
+
+ }
+
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ ElementType.TYPE, ElementType.METHOD })
+ @HasPermission(object = "{value}")
+ @interface HasReadPermission {
+
+ String value();
+
+ }
+
+ private interface MessageService {
+
+ @HasReadPermission("#name")
+ String sayHello(String name);
+
+ }
+
+}