diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java
index 5277a34a0b8..3426529d489 100644
--- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java
+++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java
@@ -35,6 +35,8 @@
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
+import org.springframework.security.aot.hint.PrePostAuthorizeHintsRegistrar;
+import org.springframework.security.aot.hint.SecurityHintsRegistrar;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.ObservationAuthorizationManager;
import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
@@ -191,6 +193,12 @@ static MethodInterceptor postFilterAuthorizationMethodInterceptor(
() -> _prePostMethodSecurityConfiguration.getObject().postFilterMethodInterceptor);
}
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ static SecurityHintsRegistrar prePostAuthorizeExpressionHintsRegistrar() {
+ return new PrePostAuthorizeHintsRegistrar();
+ }
+
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
diff --git a/core/src/main/java/org/springframework/security/aot/hint/PrePostAuthorizeExpressionBeanHintsRegistrar.java b/core/src/main/java/org/springframework/security/aot/hint/PrePostAuthorizeExpressionBeanHintsRegistrar.java
new file mode 100644
index 00000000000..b9a5732957f
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/aot/hint/PrePostAuthorizeExpressionBeanHintsRegistrar.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2002-2024 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.aot.hint;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.expression.spel.SpelNode;
+import org.springframework.expression.spel.ast.BeanReference;
+import org.springframework.expression.spel.standard.SpelExpression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authorization.method.AuthorizeReturnObject;
+import org.springframework.security.core.annotation.SecurityAnnotationScanner;
+import org.springframework.security.core.annotation.SecurityAnnotationScanners;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link SecurityHintsRegistrar} that scans all provided classes for methods that use
+ * {@link PreAuthorize} or {@link PostAuthorize} and registers hints for the beans used
+ * within the security expressions.
+ *
+ *
+ * It will also scan return types of methods annotated with {@link AuthorizeReturnObject}.
+ *
+ *
+ * This may be used by an application to register specific Security-adjacent classes that
+ * were otherwise missed by Spring Security's reachability scans.
+ *
+ *
+ * Remember to register this as an infrastructural bean like so:
+ *
+ *
+ * @Bean
+ * @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ * static SecurityHintsRegistrar registerThese() {
+ * return new PrePostAuthorizeExpressionBeanHintsRegistrar(MyClass.class);
+ * }
+ *
+ *
+ * @author Marcus da Coregio
+ * @since 6.4
+ * @see SecurityHintsAotProcessor
+ */
+public final class PrePostAuthorizeExpressionBeanHintsRegistrar implements SecurityHintsRegistrar {
+
+ private final SecurityAnnotationScanner preAuthorizeScanner = SecurityAnnotationScanners
+ .requireUnique(PreAuthorize.class);
+
+ private final SecurityAnnotationScanner postAuthorizeScanner = SecurityAnnotationScanners
+ .requireUnique(PostAuthorize.class);
+
+ private final SecurityAnnotationScanner authorizeReturnObjectScanner = SecurityAnnotationScanners
+ .requireUnique(AuthorizeReturnObject.class);
+
+ private final SpelExpressionParser expressionParser = new SpelExpressionParser();
+
+ private final Set> visitedClasses = new HashSet<>();
+
+ private final List> toVisit;
+
+ public PrePostAuthorizeExpressionBeanHintsRegistrar(Class>... toVisit) {
+ this(Arrays.asList(toVisit));
+ }
+
+ public PrePostAuthorizeExpressionBeanHintsRegistrar(List> toVisit) {
+ Assert.notEmpty(toVisit, "toVisit cannot be empty");
+ Assert.noNullElements(toVisit, "toVisit cannot contain null elements");
+ this.toVisit = toVisit;
+ }
+
+ @Override
+ public void registerHints(RuntimeHints hints, ConfigurableListableBeanFactory beanFactory) {
+ Set expressions = new HashSet<>();
+ for (Class> bean : this.toVisit) {
+ expressions.addAll(extractSecurityExpressions(bean));
+ }
+ Set beanNamesToRegister = new HashSet<>();
+ for (String expression : expressions) {
+ beanNamesToRegister.addAll(extractBeanNames(expression));
+ }
+ for (String toRegister : beanNamesToRegister) {
+ Class> type = beanFactory.getType(toRegister, false);
+ if (type == null) {
+ continue;
+ }
+ hints.reflection().registerType(TypeReference.of(type), MemberCategory.INVOKE_DECLARED_METHODS);
+ }
+ }
+
+ private Set extractSecurityExpressions(Class> clazz) {
+ if (this.visitedClasses.contains(clazz)) {
+ return Collections.emptySet();
+ }
+ this.visitedClasses.add(clazz);
+ Set expressions = new HashSet<>();
+ for (Method method : clazz.getDeclaredMethods()) {
+ PreAuthorize preAuthorize = this.preAuthorizeScanner.scan(method, clazz);
+ PostAuthorize postAuthorize = this.postAuthorizeScanner.scan(method, clazz);
+ if (preAuthorize != null) {
+ expressions.add(preAuthorize.value());
+ }
+ if (postAuthorize != null) {
+ expressions.add(postAuthorize.value());
+ }
+ AuthorizeReturnObject authorizeReturnObject = this.authorizeReturnObjectScanner.scan(method, clazz);
+ if (authorizeReturnObject != null) {
+ expressions.addAll(extractSecurityExpressions(method.getReturnType()));
+ }
+ }
+ return expressions;
+ }
+
+ private Set extractBeanNames(String rawExpression) {
+ SpelExpression expression = this.expressionParser.parseRaw(rawExpression);
+ SpelNode node = expression.getAST();
+ Set beanNames = new HashSet<>();
+ resolveBeanNames(beanNames, node);
+ return beanNames;
+ }
+
+ private void resolveBeanNames(Set beanNames, SpelNode node) {
+ if (node instanceof BeanReference br) {
+ beanNames.add(br.getName());
+ }
+ int childCount = node.getChildCount();
+ if (childCount == 0) {
+ return;
+ }
+ for (int i = 0; i < childCount; i++) {
+ resolveBeanNames(beanNames, node.getChild(i));
+ }
+ }
+
+}
diff --git a/core/src/main/java/org/springframework/security/aot/hint/PrePostAuthorizeHintsRegistrar.java b/core/src/main/java/org/springframework/security/aot/hint/PrePostAuthorizeHintsRegistrar.java
new file mode 100644
index 00000000000..45227ddbc51
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/aot/hint/PrePostAuthorizeHintsRegistrar.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2002-2024 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.aot.hint;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.support.RegisteredBean;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.security.access.prepost.PreAuthorize;
+
+/**
+ * A {@link SecurityHintsRegistrar} that scans all beans for methods that use
+ * {@link PreAuthorize} or {@link PostAuthorize} and registers appropriate hints for the
+ * annotations.
+ *
+ * @author Marcus da Coregio
+ * @since 6.4
+ * @see SecurityHintsAotProcessor
+ * @see PrePostAuthorizeExpressionBeanHintsRegistrar
+ */
+public final class PrePostAuthorizeHintsRegistrar implements SecurityHintsRegistrar {
+
+ @Override
+ public void registerHints(RuntimeHints hints, ConfigurableListableBeanFactory beanFactory) {
+ List> beans = Arrays.stream(beanFactory.getBeanDefinitionNames())
+ .map((beanName) -> RegisteredBean.of(beanFactory, beanName).getBeanClass())
+ .collect(Collectors.toList());
+ new PrePostAuthorizeExpressionBeanHintsRegistrar(beans).registerHints(hints, beanFactory);
+ }
+
+}
diff --git a/core/src/test/java/org/springframework/security/aot/hint/PrePostAuthorizeHintsRegistrarTests.java b/core/src/test/java/org/springframework/security/aot/hint/PrePostAuthorizeHintsRegistrarTests.java
new file mode 100644
index 00000000000..a22e0b73e9e
--- /dev/null
+++ b/core/src/test/java/org/springframework/security/aot/hint/PrePostAuthorizeHintsRegistrarTests.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright 2002-2024 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.aot.hint;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.aot.generate.GenerationContext;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
+import org.springframework.aot.test.generate.TestGenerationContext;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authorization.method.AuthorizeReturnObject;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
+
+class PrePostAuthorizeHintsRegistrarTests {
+
+ private final PrePostAuthorizeHintsRegistrar registrar = new PrePostAuthorizeHintsRegistrar();
+
+ private final GenerationContext generationContext = new TestGenerationContext();
+
+ @Test
+ void registerHintsWhenPreAuthorizeOnTypeThenHintsRegistered() {
+ process(Authz.class, PreAuthorizeOnClass.class);
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(Authz.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ }
+
+ @Test
+ void registerHintsWhenPostAuthorizeOnTypeThenHintsRegistered() {
+ process(Authz.class, PostAuthorizeOnClass.class);
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(Authz.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ }
+
+ @Test
+ void registerHintsWhenPreAuthorizeOnMethodsThenHintsRegistered() {
+ process(Authz.class, Foo.class, PreAuthorizeOnMethods.class);
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(Authz.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(Foo.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ }
+
+ @Test
+ void registerHintsWhenPostAuthorizeOnMethodsThenHintsRegistered() {
+ process(Authz.class, Foo.class, PostAuthorizeOnMethods.class);
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(Authz.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(Foo.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ }
+
+ @Test
+ void registerHintsWhenPreAuthorizeExpressionWithMultipleBeansThenRegisterHintsForAllBeans() {
+ process(Authz.class, Foo.class, PreAuthorizeMultipleBeans.class);
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(Authz.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(Foo.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ }
+
+ @Test
+ void registerHintsWhenPostAuthorizeExpressionWithMultipleBeansThenRegisterHintsForAllBeans() {
+ process(Authz.class, Foo.class, PostAuthorizeMultipleBeans.class);
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(Authz.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(Foo.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ }
+
+ @Test
+ void registerHintsWhenPreAuthorizeOnTypeAndMethodThenRegisterHintsForBoth() {
+ process(Authz.class, Foo.class, PreAuthorizeOnTypeAndMethod.class);
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(Authz.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(Foo.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ }
+
+ @Test
+ void registerHintsWhenPostAuthorizeOnTypeAndMethodThenRegisterHintsForBoth() {
+ process(Authz.class, Foo.class, PostAuthorizeOnTypeAndMethod.class);
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(Authz.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(Foo.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ }
+
+ @Test
+ void registerHintsWhenSecurityAnnotationsInsideAuthorizeReturnObjectOnMethodThenRegisterHints() {
+ process(AccountAuthz.class, Authz.class, PreAuthorizeInsideAuthorizeReturnObjectOnMethod.class);
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(AccountAuthz.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(Authz.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ }
+
+ @Test
+ void registerHintsWhenSecurityAnnotationsInsideAuthorizeReturnObjectOnClassThenRegisterHints() {
+ process(AccountAuthz.class, Authz.class, PreAuthorizeInsideAuthorizeReturnObjectOnClass.class);
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(AccountAuthz.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ assertThat(RuntimeHintsPredicates.reflection()
+ .onType(Authz.class)
+ .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+ .accepts(this.generationContext.getRuntimeHints());
+ }
+
+ @Test
+ void registerHintsWhenCyclicDependencyThenNoStackOverflowException() {
+ assertThatNoException().isThrownBy(() -> process(AService.class));
+ }
+
+ private void process(Class>... beanClasses) {
+ DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
+ for (Class> beanClass : beanClasses) {
+ beanFactory.registerBeanDefinition(beanClass.getSimpleName().toLowerCase(),
+ new RootBeanDefinition(beanClass));
+ }
+ this.registrar.registerHints(this.generationContext.getRuntimeHints(), beanFactory);
+ }
+
+ @PreAuthorize("@authz.check()")
+ static class PreAuthorizeOnClass {
+
+ }
+
+ @PostAuthorize("@authz.check()")
+ static class PostAuthorizeOnClass {
+
+ }
+
+ static class PreAuthorizeOnMethods {
+
+ @PreAuthorize("@authz.check()")
+ void method1() {
+ }
+
+ @PreAuthorize("@foo.bar()")
+ void method2() {
+ }
+
+ }
+
+ static class PostAuthorizeOnMethods {
+
+ @PostAuthorize("@authz.check()")
+ void method1() {
+ }
+
+ @PostAuthorize("@foo.bar()")
+ void method2() {
+ }
+
+ }
+
+ static class PreAuthorizeMultipleBeans {
+
+ @PreAuthorize("@authz.check() ? true : @foo.bar()")
+ void method1() {
+ }
+
+ }
+
+ static class PostAuthorizeMultipleBeans {
+
+ @PostAuthorize("@authz.check() ? true : @foo.bar()")
+ void method1() {
+ }
+
+ }
+
+ @PreAuthorize("@authz.check()")
+ static class PreAuthorizeOnTypeAndMethod {
+
+ @PreAuthorize("@foo.bar()")
+ void method1() {
+ }
+
+ }
+
+ @PostAuthorize("@authz.check()")
+ static class PostAuthorizeOnTypeAndMethod {
+
+ @PostAuthorize("@foo.bar()")
+ void method1() {
+ }
+
+ }
+
+ static class PreAuthorizeInsideAuthorizeReturnObjectOnMethod {
+
+ @AuthorizeReturnObject
+ Account getAccount() {
+ return new Account("1234");
+ }
+
+ }
+
+ @AuthorizeReturnObject
+ static class PreAuthorizeInsideAuthorizeReturnObjectOnClass {
+
+ Account getAccount() {
+ return new Account("1234");
+ }
+
+ }
+
+ static class Authz {
+
+ boolean check() {
+ return true;
+ }
+
+ }
+
+ static class Foo {
+
+ boolean bar() {
+ return true;
+ }
+
+ }
+
+ static class AccountAuthz {
+
+ boolean canViewAccountNumber() {
+ return true;
+ }
+
+ }
+
+ static class Account {
+
+ private final String accountNumber;
+
+ Account(String accountNumber) {
+ this.accountNumber = accountNumber;
+ }
+
+ @PreAuthorize("@accountauthz.canViewAccountNumber()")
+ String getAccountNumber() {
+ return this.accountNumber;
+ }
+
+ @AuthorizeReturnObject
+ User getUser() {
+ return new User("John Doe");
+ }
+
+ }
+
+ static class User {
+
+ private final String fullName;
+
+ User(String fullName) {
+ this.fullName = fullName;
+ }
+
+ @PostAuthorize("@authz.check()")
+ String getFullName() {
+ return this.fullName;
+ }
+
+ }
+
+ static class AService {
+
+ @AuthorizeReturnObject
+ A getA() {
+ return new A();
+ }
+
+ }
+
+ static class A {
+
+ @AuthorizeReturnObject
+ B getB() {
+ return null;
+ }
+
+ }
+
+ static class B {
+
+ @AuthorizeReturnObject
+ A getA() {
+ return null;
+ }
+
+ }
+
+}
diff --git a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc
index 1dc4ff00655..7d827291a19 100644
--- a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc
+++ b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc
@@ -1528,6 +1528,176 @@ We expose `MethodSecurityExpressionHandler` using a `static` method to ensure th
You can also <> to add your own custom authorization expressions beyond the defaults.
+=== Working with AOT
+
+Spring Security will scan all beans in the application context for methods that use `@PreAuthorize` or `@PostAuthorize`.
+When it finds one, it will resolve any beans used inside the security expression and register the appropriate runtime hints for that bean.
+If it finds a method that uses `@AuthorizeReturnObject`, it will recursively search inside the method's return type for `@PreAuthorize` and `@PostAuthorize` annotations and register them accordingly.
+
+For example, consider the following Spring Boot application:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Service
+public class AccountService { <1>
+
+ @PreAuthorize("@authz.decide()") <2>
+ @AuthorizeReturnObject <3>
+ public Account getAccountById(String accountId) {
+ // ...
+ }
+
+}
+
+public class Account {
+
+ private final String accountNumber;
+
+ // ...
+
+ @PreAuthorize("@accountAuthz.canViewAccountNumber()") <4>
+ public String getAccountNumber() {
+ return this.accountNumber;
+ }
+
+ @AuthorizeReturnObject <5>
+ public User getUser() {
+ return new User("John Doe");
+ }
+
+}
+
+public class User {
+
+ private final String fullName;
+
+ // ...
+
+ @PostAuthorize("@myOtherAuthz.decide()") <6>
+ public String getFullName() {
+ return this.fullName;
+ }
+
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Service
+class AccountService { <1>
+
+ @PreAuthorize("@authz.decide()") <2>
+ @AuthorizeReturnObject <3>
+ fun getAccountById(accountId: String): Account {
+ // ...
+ }
+
+}
+
+class Account(private val accountNumber: String) {
+
+ @PreAuthorize("@accountAuthz.canViewAccountNumber()") <4>
+ fun getAccountNumber(): String {
+ return this.accountNumber
+ }
+
+ @AuthorizeReturnObject <5>
+ fun getUser(): User {
+ return User("John Doe")
+ }
+
+}
+
+class User(private val fullName: String) {
+
+ @PostAuthorize("@myOtherAuthz.decide()") <6>
+ fun getFullName(): String {
+ return this.fullName
+ }
+
+}
+----
+======
+
+<1> Spring Security finds the `AccountService` bean
+<2> Finding a method that uses `@PreAuthorize`, it will resolve any bean names used inside the expression, `authz` in that case, and register runtime hints for the bean class
+<3> Finding a method that uses `@AuthorizeReturnObject`, it will look into the method's return type for any `@PreAuthorize` or `@PostAuthorize`
+<4> Then, it finds a `@PreAuthorize` with another bean name: `accountAuthz`; the runtime hints are registered for the bean class as well
+<5> Finding another `@AuthorizeReturnObject` it will look again into the method's return type
+<6> Now, a `@PostAuthorize` is found with yet another bean name used: `myOtherAuthz`; the runtime hints are registered for the bean class as well
+
+There will be many times when Spring Security cannot determine the actual return type of the method ahead of time since it may be hidden in an erased generic type.
+
+Consider the following service:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Service
+public class AccountService {
+
+ @AuthorizeReturnObject
+ public List getAllAccounts() {
+ // ...
+ }
+
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Service
+class AccountService {
+
+ @AuthorizeReturnObject
+ fun getAllAccounts(): List {
+ // ...
+ }
+
+}
+----
+======
+
+In this case, the generic type is erased and so it isn’t apparent to Spring Security ahead-of-time that `Account` needs to be visited in order to check for `@PreAuthorize` and `@PostAuthorize`.
+
+To address this, you can publish a javadoc:org.springframework.security.aot.hint.PrePostAuthorizeExpressionBeanHintsRegistrar[`PrePostAuthorizeExpressionBeanHintsRegistrar`] like so:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Bean
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+static SecurityHintsRegistrar registerTheseToo() {
+ return new PrePostAuthorizeExpressionBeanHintsRegistrar(Account.class);
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Bean
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+fun registerTheseToo(): SecurityHintsRegistrar {
+ return PrePostAuthorizeExpressionBeanHintsRegistrar(Account::class.java)
+}
+----
+======
+
[[use-aspectj]]
== Authorizing with AspectJ