瀏覽代碼

Add AuthorizeReturnObject Hints

Closes gh-15709
Josh Cummings 11 月之前
父節點
當前提交
fd5d03d384

+ 9 - 0
config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java

@@ -26,6 +26,9 @@ import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Role;
+import org.springframework.security.aot.hint.AuthorizeReturnObjectCoreHintsRegistrar;
+import org.springframework.security.aot.hint.SecurityHintsRegistrar;
+import org.springframework.security.authorization.AuthorizationProxyFactory;
 import org.springframework.security.authorization.method.AuthorizationAdvisor;
 import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
 import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor;
@@ -54,4 +57,10 @@ final class AuthorizationProxyConfiguration implements AopInfrastructureBean {
 		return interceptor;
 	}
 
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	static SecurityHintsRegistrar authorizeReturnObjectHintsRegistrar(AuthorizationProxyFactory proxyFactory) {
+		return new AuthorizeReturnObjectCoreHintsRegistrar(proxyFactory);
+	}
+
 }

+ 97 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/EnableMethodSecurityAotTests.java

@@ -0,0 +1,97 @@
+/*
+ * 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.config.annotation.method.configuration.aot;
+
+import javax.sql.DataSource;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.aot.generate.GenerationContext;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.aot.test.generate.TestGenerationContext;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.aot.ApplicationContextAotGenerator;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * AOT Tests for {@code PrePostMethodSecurityConfiguration}.
+ *
+ * @author Evgeniy Cheban
+ * @author Josh Cummings
+ */
+@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
+public class EnableMethodSecurityAotTests {
+
+	private final ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
+
+	private final GenerationContext context = new TestGenerationContext();
+
+	@Test
+	void whenProcessAheadOfTimeThenCreatesAuthorizationProxies() {
+		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+		context.register(AppConfig.class);
+		this.generator.processAheadOfTime(context, this.context);
+		RuntimeHints hints = this.context.getRuntimeHints();
+		assertThat(hints.reflection().getTypeHint(TypeReference.of(cglibClassName(Message.class)))).isNotNull();
+		assertThat(hints.reflection().getTypeHint(TypeReference.of(cglibClassName(User.class)))).isNotNull();
+		assertThat(hints.proxies()
+			.jdkProxyHints()
+			.anyMatch((hint) -> hint.getProxiedInterfaces().contains(TypeReference.of(UserProjection.class)))).isTrue();
+	}
+
+	private static String cglibClassName(Class<?> clazz) {
+		return clazz.getCanonicalName() + "$$SpringCGLIB$$0";
+	}
+
+	@Configuration
+	@EnableMethodSecurity
+	@EnableJpaRepositories
+	static class AppConfig {
+
+		@Bean
+		DataSource dataSource() {
+			EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
+			return builder.setType(EmbeddedDatabaseType.HSQL).build();
+		}
+
+		@Bean
+		LocalContainerEntityManagerFactoryBean entityManagerFactory() {
+			HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
+			vendorAdapter.setGenerateDdl(true);
+			LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
+			factory.setJpaVendorAdapter(vendorAdapter);
+			factory.setPackagesToScan("org.springframework.security.config.annotation.method.configuration.aot");
+			factory.setDataSource(dataSource());
+			return factory;
+		}
+
+	}
+
+}

+ 89 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/Message.java

@@ -0,0 +1,89 @@
+/*
+ * 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.config.annotation.method.configuration.aot;
+
+import java.time.Instant;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authorization.method.AuthorizeReturnObject;
+
+@Entity
+public class Message {
+
+	@Id
+	@GeneratedValue(strategy = GenerationType.AUTO)
+	private Long id;
+
+	private String text;
+
+	private String summary;
+
+	private Instant created = Instant.now();
+
+	@ManyToOne
+	private User to;
+
+	@AuthorizeReturnObject
+	public User getTo() {
+		return this.to;
+	}
+
+	public void setTo(User to) {
+		this.to = to;
+	}
+
+	public Long getId() {
+		return this.id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public Instant getCreated() {
+		return this.created;
+	}
+
+	public void setCreated(Instant created) {
+		this.created = created;
+	}
+
+	@PreAuthorize("hasAuthority('message:read')")
+	public String getText() {
+		return this.text;
+	}
+
+	public void setText(String text) {
+		this.text = text;
+	}
+
+	@PreAuthorize("hasAuthority('message:read')")
+	public String getSummary() {
+		return this.summary;
+	}
+
+	public void setSummary(String summary) {
+		this.summary = summary;
+	}
+
+}

+ 39 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/MessageRepository.java

@@ -0,0 +1,39 @@
+/*
+ * 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.config.annotation.method.configuration.aot;
+
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.security.authorization.method.AuthorizeReturnObject;
+import org.springframework.stereotype.Repository;
+
+/**
+ * A repository for accessing {@link Message}s.
+ *
+ * @author Rob Winch
+ */
+@Repository
+@AuthorizeReturnObject
+public interface MessageRepository extends CrudRepository<Message, Long> {
+
+	@Query("select m from Message m where m.to.id = ?#{ authentication.name }")
+	Iterable<Message> findAll();
+
+	@Query("from org.springframework.security.config.annotation.method.configuration.aot.User u where u.id = ?#{ authentication.name }")
+	UserProjection findCurrentUser();
+
+}

+ 85 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/User.java

@@ -0,0 +1,85 @@
+/*
+ * 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.config.annotation.method.configuration.aot;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+
+/**
+ * A user.
+ *
+ * @author Rob Winch
+ */
+@Entity(name = "users")
+public class User {
+
+	@Id
+	private String id;
+
+	private String firstName;
+
+	private String lastName;
+
+	private String email;
+
+	private String password;
+
+	public String getId() {
+		return this.id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	@PreAuthorize("hasAuthority('user:read')")
+	public String getFirstName() {
+		return this.firstName;
+	}
+
+	public void setFirstName(String firstName) {
+		this.firstName = firstName;
+	}
+
+	@PreAuthorize("hasAuthority('user:read')")
+	public String getLastName() {
+		return this.lastName;
+	}
+
+	public void setLastName(String lastName) {
+		this.lastName = lastName;
+	}
+
+	public String getEmail() {
+		return this.email;
+	}
+
+	public void setEmail(String email) {
+		this.email = email;
+	}
+
+	public String getPassword() {
+		return this.password;
+	}
+
+	public void setPassword(String password) {
+		this.password = password;
+	}
+
+}

+ 25 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/UserProjection.java

@@ -0,0 +1,25 @@
+/*
+ * 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.config.annotation.method.configuration.aot;
+
+public interface UserProjection {
+
+	String getFirstName();
+
+	String getLastName();
+
+}

+ 95 - 0
core/src/main/java/org/springframework/security/aot/hint/AuthorizeReturnObjectCoreHintsRegistrar.java

@@ -0,0 +1,95 @@
+/*
+ * 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.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.security.authorization.AuthorizationProxyFactory;
+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 beans for methods that use
+ * {@link AuthorizeReturnObject} and registers those return objects as
+ * {@link org.springframework.aot.hint.TypeHint}s.
+ *
+ * <p>
+ * It also traverses those found types for other return values.
+ *
+ * <p>
+ * An instance of this class is published as an infrastructural bean by the
+ * {@code spring-security-config} module. However, in the event you need to publish it
+ * yourself, remember to publish it as an infrastructural bean like so:
+ *
+ * <pre>
+ *	&#064;Bean
+ *	&#064;Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ *	static SecurityHintsRegistrar proxyThese(AuthorizationProxyFactory proxyFactory) {
+ *		return new AuthorizeReturnObjectHintsRegistrar(proxyFactory);
+ *	}
+ * </pre>
+ *
+ * @author Josh Cummings
+ * @since 6.4
+ * @see AuthorizeReturnObjectHintsRegistrar
+ * @see SecurityHintsAotProcessor
+ */
+public final class AuthorizeReturnObjectCoreHintsRegistrar implements SecurityHintsRegistrar {
+
+	private final AuthorizationProxyFactory proxyFactory;
+
+	private final SecurityAnnotationScanner<AuthorizeReturnObject> scanner = SecurityAnnotationScanners
+		.requireUnique(AuthorizeReturnObject.class);
+
+	private final Set<Class<?>> visitedClasses = new HashSet<>();
+
+	public AuthorizeReturnObjectCoreHintsRegistrar(AuthorizationProxyFactory proxyFactory) {
+		Assert.notNull(proxyFactory, "proxyFactory cannot be null");
+		this.proxyFactory = proxyFactory;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void registerHints(RuntimeHints hints, ConfigurableListableBeanFactory beanFactory) {
+		List<Class<?>> toProxy = new ArrayList<>();
+		for (String name : beanFactory.getBeanDefinitionNames()) {
+			Class<?> clazz = beanFactory.getType(name, false);
+			if (clazz == null) {
+				continue;
+			}
+			for (Method method : clazz.getDeclaredMethods()) {
+				AuthorizeReturnObject annotation = this.scanner.scan(method, clazz);
+				if (annotation == null) {
+					continue;
+				}
+				toProxy.add(method.getReturnType());
+			}
+		}
+		new AuthorizeReturnObjectHintsRegistrar(this.proxyFactory, toProxy).registerHints(hints, beanFactory);
+	}
+
+}

+ 143 - 0
core/src/main/java/org/springframework/security/aot/hint/AuthorizeReturnObjectHintsRegistrar.java

@@ -0,0 +1,143 @@
+/*
+ * 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.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.aop.SpringProxy;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.security.authorization.AuthorizationProxyFactory;
+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} implementation that registers only the classes
+ * provided in the constructor.
+ *
+ * <p>
+ * It also traverses those found types for other return values.
+ *
+ * <p>
+ * This may be used by an application to register specific Security-adjacent classes that
+ * were otherwise missed by Spring Security's reachability scans.
+ *
+ * <p>
+ * Remember to register this as an infrastructural bean like so:
+ *
+ * <pre>
+ *	&#064;Bean
+ *	&#064;Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ *	static SecurityHintsRegistrar proxyThese(AuthorizationProxyFactory proxyFactory) {
+ *		return new AuthorizationProxyFactoryHintsRegistrar(proxyFactory, MyClass.class);
+ *	}
+ * </pre>
+ *
+ * <p>
+ * Note that no object graph traversal is performed in this class. As such, any classes
+ * that need an authorization proxy that are missed by Security's default registrars
+ * should be listed exhaustively in the constructor.
+ *
+ * @author Josh Cummings
+ * @since 6.4
+ * @see AuthorizeReturnObjectCoreHintsRegistrar
+ */
+public final class AuthorizeReturnObjectHintsRegistrar implements SecurityHintsRegistrar {
+
+	private final AuthorizationProxyFactory proxyFactory;
+
+	private final SecurityAnnotationScanner<AuthorizeReturnObject> scanner = SecurityAnnotationScanners
+		.requireUnique(AuthorizeReturnObject.class);
+
+	private final Set<Class<?>> visitedClasses = new HashSet<>();
+
+	private final List<Class<?>> classesToProxy;
+
+	public AuthorizeReturnObjectHintsRegistrar(AuthorizationProxyFactory proxyFactory, Class<?>... classes) {
+		Assert.notNull(proxyFactory, "proxyFactory cannot be null");
+		Assert.noNullElements(classes, "classes cannot contain null elements");
+		this.proxyFactory = proxyFactory;
+		this.classesToProxy = new ArrayList(List.of(classes));
+	}
+
+	/**
+	 * Construct this registrar
+	 * @param proxyFactory the proxy factory to use to produce the proxy class
+	 * implementations to be registered
+	 * @param classes the classes to proxy
+	 */
+	public AuthorizeReturnObjectHintsRegistrar(AuthorizationProxyFactory proxyFactory, List<Class<?>> classes) {
+		this.proxyFactory = proxyFactory;
+		this.classesToProxy = new ArrayList<>(classes);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void registerHints(RuntimeHints hints, ConfigurableListableBeanFactory beanFactory) {
+		List<Class<?>> toProxy = new ArrayList<>();
+		for (Class<?> clazz : this.classesToProxy) {
+			toProxy.add(clazz);
+			traverseType(toProxy, clazz);
+		}
+		for (Class<?> clazz : toProxy) {
+			registerProxy(hints, clazz);
+		}
+	}
+
+	private void registerProxy(RuntimeHints hints, Class<?> clazz) {
+		Class<?> proxied = (Class<?>) this.proxyFactory.proxy(clazz);
+		if (proxied == null) {
+			return;
+		}
+		if (Proxy.isProxyClass(proxied)) {
+			hints.proxies().registerJdkProxy(proxied.getInterfaces());
+			return;
+		}
+		if (SpringProxy.class.isAssignableFrom(proxied)) {
+			hints.reflection()
+				.registerType(proxied, MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.PUBLIC_FIELDS,
+						MemberCategory.DECLARED_FIELDS);
+		}
+	}
+
+	private void traverseType(List<Class<?>> toProxy, Class<?> clazz) {
+		if (clazz == Object.class || this.visitedClasses.contains(clazz)) {
+			return;
+		}
+		this.visitedClasses.add(clazz);
+		for (Method m : clazz.getDeclaredMethods()) {
+			AuthorizeReturnObject object = this.scanner.scan(m, clazz);
+			if (object == null) {
+				continue;
+			}
+			Class<?> returnType = m.getReturnType();
+			toProxy.add(returnType);
+			traverseType(toProxy, returnType);
+		}
+	}
+
+}

+ 104 - 0
core/src/test/java/org/springframework/security/aot/hint/AuthorizeReturnObjectCoreHintsRegistrarTests.java

@@ -0,0 +1,104 @@
+/*
+ * 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.hint.RuntimeHints;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.security.authorization.AuthorizationProxyFactory;
+import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
+import org.springframework.security.authorization.method.AuthorizeReturnObject;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.spy;
+
+/**
+ * Tests for {@link AuthorizeReturnObjectCoreHintsRegistrar}
+ */
+public class AuthorizeReturnObjectCoreHintsRegistrarTests {
+
+	private final AuthorizationProxyFactory proxyFactory = spy(AuthorizationAdvisorProxyFactory.withDefaults());
+
+	private final AuthorizeReturnObjectCoreHintsRegistrar registrar = new AuthorizeReturnObjectCoreHintsRegistrar(
+			this.proxyFactory);
+
+	@Test
+	public void registerHintsWhenUsingAuthorizeReturnObjectThenRegisters() {
+		GenericApplicationContext context = new GenericApplicationContext();
+		context.registerBean(MyService.class, MyService::new);
+		context.registerBean(MyInterface.class, MyImplementation::new);
+		context.refresh();
+		RuntimeHints hints = new RuntimeHints();
+		this.registrar.registerHints(hints, context.getBeanFactory());
+		assertThat(hints.reflection().typeHints().map((hint) -> hint.getType().getName()))
+			.containsOnly(cglibClassName(MyObject.class), cglibClassName(MySubObject.class));
+		assertThat(hints.proxies()
+			.jdkProxyHints()
+			.flatMap((hint) -> hint.getProxiedInterfaces().stream())
+			.map(TypeReference::getName)).contains(MyInterface.class.getName());
+	}
+
+	private static String cglibClassName(Class<?> clazz) {
+		return clazz.getName() + "$$SpringCGLIB$$0";
+	}
+
+	public static class MyService {
+
+		@AuthorizeReturnObject
+		MyObject get() {
+			return new MyObject();
+		}
+
+	}
+
+	public interface MyInterface {
+
+		MyObject get();
+
+	}
+
+	@AuthorizeReturnObject
+	public static class MyImplementation implements MyInterface {
+
+		@Override
+		public MyObject get() {
+			return new MyObject();
+		}
+
+	}
+
+	public static class MyObject {
+
+		@AuthorizeReturnObject
+		public MySubObject get() {
+			return new MySubObject();
+		}
+
+		@AuthorizeReturnObject
+		public MyInterface getInterface() {
+			return new MyImplementation();
+		}
+
+	}
+
+	public static class MySubObject {
+
+	}
+
+}

+ 64 - 0
core/src/test/java/org/springframework/security/aot/hint/AuthorizeReturnObjectHintsRegistrarTests.java

@@ -0,0 +1,64 @@
+/*
+ * 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.hint.RuntimeHints;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.security.authorization.AuthorizationProxyFactory;
+import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.spy;
+
+/**
+ * Tests for {@link AuthorizeReturnObjectHintsRegistrar}
+ */
+public class AuthorizeReturnObjectHintsRegistrarTests {
+
+	private final AuthorizationProxyFactory proxyFactory = spy(AuthorizationAdvisorProxyFactory.withDefaults());
+
+	@Test
+	public void registerHintsWhenSpecifiedThenRegisters() {
+		AuthorizeReturnObjectHintsRegistrar registrar = new AuthorizeReturnObjectHintsRegistrar(this.proxyFactory,
+				MyObject.class, MyInterface.class);
+		RuntimeHints hints = new RuntimeHints();
+		registrar.registerHints(hints, null);
+		assertThat(hints.reflection().typeHints().map((hint) -> hint.getType().getName()))
+			.containsOnly(cglibClassName(MyObject.class));
+		assertThat(hints.proxies()
+			.jdkProxyHints()
+			.flatMap((hint) -> hint.getProxiedInterfaces().stream())
+			.map(TypeReference::getName)).contains(MyInterface.class.getName());
+	}
+
+	private static String cglibClassName(Class<?> clazz) {
+		return clazz.getName() + "$$SpringCGLIB$$0";
+	}
+
+	public interface MyInterface {
+
+		MyObject get();
+
+	}
+
+	public static class MyObject {
+
+	}
+
+}

+ 158 - 5
docs/modules/ROOT/pages/servlet/authorization/method-security.adoc

@@ -2105,11 +2105,6 @@ fun getEmailWhenProxiedThenAuthorizes() {
 ----
 ======
 
-[NOTE]
-====
-This feature does not yet support Spring AOT
-====
-
 === Proxying Collections
 
 `AuthorizationProxyFactory` supports Java collections, streams, arrays, optionals, and iterators by proxying the element type and maps by proxying the value type.
@@ -2297,6 +2292,164 @@ And if they do have that authority, they'll see:
 You can also add the Spring Boot property `spring.jackson.default-property-inclusion=non_null` to exclude the null value from serialization, if you also don't want to reveal the JSON key to an unauthorized user.
 ====
 
+=== Working with AOT
+
+Spring Security will scan all beans in the application context for methods that use `@AuthorizeReturnObject`.
+When it finds one, it will create and register the appropriate proxy class ahead of time.
+It will also recursively search for other nested objects that also use `@AuthorizeReturnObject` and register them accordingly.
+
+For example, consider the following Spring Boot application:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@SpringBootApplication
+public class MyApplication {
+	@RestController
+    public static class MyController { <1>
+		@GetMapping
+        @AuthorizeReturnObject
+        Message getMessage() { <2>
+			return new Message(someUser, "hello!");
+        }
+    }
+
+	public static class Message { <3>
+		User to;
+		String text;
+
+		// ...
+
+        @AuthorizeReturnObject
+        public User getTo() { <4>
+			return this.to;
+        }
+
+		// ...
+	}
+
+	public static class User { <5>
+		// ...
+	}
+
+	public static void main(String[] args) {
+		SpringApplication.run(MyApplication.class);
+	}
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@SpringBootApplication
+open class MyApplication {
+	@RestController
+    open class MyController { <1>
+		@GetMapping
+        @AuthorizeReturnObject
+        fun getMessage():Message { <2>
+			return Message(someUser, "hello!")
+        }
+    }
+
+	open class Message { <3>
+		val to: User
+		val test: String
+
+		// ...
+
+        @AuthorizeReturnObject
+        fun getTo(): User { <4>
+			return this.to
+        }
+
+		// ...
+	}
+
+	open class User { <5>
+		// ...
+	}
+
+	fun main(args: Array<String>) {
+		SpringApplication.run(MyApplication.class)
+	}
+}
+----
+======
+<1> - First, Spring Security finds the `MyController` bean
+<2> - Finding a method that uses `@AuthorizeReturnObject`, it proxies `Message`, the return value, and registers that proxy class to `RuntimeHints`
+<3> - Then, it traverses `Message` to see if it uses `@AuthorizeReturnObject`
+<4> - Finding a method that uses `@AuthorizeReturnObject`, it proxies `User`, the return value, and registers that proxy class to `RuntimeHints`
+<5> - Finally, it traverses `User` to see if it uses `@AuthorizeReturnObject`; finding nothing, the algorithm completes
+
+There will be many times when Spring Security cannot determine the proxy class ahead of time since it may be hidden in an erased generic type.
+
+Consider the following change to `MyController`:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@RestController
+public static class MyController {
+    @GetMapping
+    @AuthorizeReturnObject
+    List<Message> getMessages() {
+        return List.of(new Message(someUser, "hello!"));
+    }
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@RestController
+static class MyController {
+    @AuthorizeReturnObject
+    @GetMapping
+    fun getMessages(): Array<Message> = arrayOf(Message(someUser, "hello!"))
+}
+----
+======
+
+In this case, the generic type is erased and so it isn't apparent to Spring Security ahead-of-time that `Message` will need to be proxied at runtime.
+
+To address this, you can publish `AuthorizeProxyFactoryHintsRegistrar` like so:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Bean
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+static SecurityHintsRegsitrar registerTheseToo(AuthorizationProxyFactory proxyFactory) {
+	return new AuthorizeReturnObjectHintsRegistrar(proxyFactory, Message.class);
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Bean
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+fun registerTheseToo(proxyFactory: AuthorizationProxyFactory?): SecurityHintsRegistrar {
+    return AuthorizeReturnObjectHintsRegistrar(proxyFactory, Message::class.java)
+}
+----
+======
+
+Spring Security will register that class and then traverse its type as before.
+
 [[fallback-values-authorization-denied]]
 == Providing Fallback Values When Authorization is Denied