Przeglądaj źródła

Add AuthorizeReturnObject

Closes gh-14597
Josh Cummings 1 rok temu
rodzic
commit
d169d5a835
19 zmienionych plików z 778 dodań i 12 usunięć
  1. 21 1
      aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PostAuthorizeAspectTests.java
  2. 19 1
      aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PostFilterAspectTests.java
  3. 21 1
      aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PreAuthorizeAspectTests.java
  4. 19 1
      aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PreFilterAspectTests.java
  5. 1 1
      aspects/src/test/java/org/springframework/security/authorization/method/aspectj/SecuredAspectTests.java
  6. 16 0
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java
  7. 1 0
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityAdvisorRegistrar.java
  8. 16 0
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationProxyConfiguration.java
  9. 187 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java
  10. 231 1
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java
  11. 2 0
      core/src/main/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactory.java
  12. 2 0
      core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactory.java
  13. 4 2
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationInterceptorsOrder.java
  14. 41 0
      core/src/main/java/org/springframework/security/authorization/method/AuthorizeReturnObject.java
  15. 110 0
      core/src/main/java/org/springframework/security/authorization/method/AuthorizeReturnObjectMethodInterceptor.java
  16. 1 1
      core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java
  17. 1 1
      core/src/test/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactoryTests.java
  18. 84 2
      docs/modules/ROOT/pages/servlet/authorization/method-security.adoc
  19. 1 0
      docs/modules/ROOT/pages/whats-new.adoc

+ 21 - 1
aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PostAuthorizeAspectTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -103,6 +103,13 @@ public class PostAuthorizeAspectTests {
 		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.prePostSecured::denyAllMethod);
 		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.prePostSecured::denyAllMethod);
 	}
 	}
 
 
+	@Test
+	public void nestedDenyAllPostAuthorizeDeniesAccess() {
+		SecurityContextHolder.getContext().setAuthentication(this.anne);
+		assertThatExceptionOfType(AccessDeniedException.class)
+			.isThrownBy(() -> this.secured.myObject().denyAllMethod());
+	}
+
 	interface SecuredInterface {
 	interface SecuredInterface {
 
 
 		@PostAuthorize("hasRole('X')")
 		@PostAuthorize("hasRole('X')")
@@ -134,6 +141,10 @@ public class PostAuthorizeAspectTests {
 			privateMethod();
 			privateMethod();
 		}
 		}
 
 
+		NestedObject myObject() {
+			return new NestedObject();
+		}
+
 	}
 	}
 
 
 	static class SecuredImplSubclass extends SecuredImpl {
 	static class SecuredImplSubclass extends SecuredImpl {
@@ -157,4 +168,13 @@ public class PostAuthorizeAspectTests {
 
 
 	}
 	}
 
 
+	static class NestedObject {
+
+		@PostAuthorize("denyAll")
+		void denyAllMethod() {
+
+		}
+
+	}
+
 }
 }

+ 19 - 1
aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PostFilterAspectTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -54,6 +54,11 @@ public class PostFilterAspectTests {
 		assertThat(this.prePostSecured.postFilterMethod(objects)).containsExactly("apple", "aubergine");
 		assertThat(this.prePostSecured.postFilterMethod(objects)).containsExactly("apple", "aubergine");
 	}
 	}
 
 
+	@Test
+	public void nestedDenyAllPostFilterDeniesAccess() {
+		assertThat(this.prePostSecured.myObject().denyAllMethod()).isEmpty();
+	}
+
 	static class PrePostSecured {
 	static class PrePostSecured {
 
 
 		@PostFilter("filterObject.startsWith('a')")
 		@PostFilter("filterObject.startsWith('a')")
@@ -61,6 +66,19 @@ public class PostFilterAspectTests {
 			return objects;
 			return objects;
 		}
 		}
 
 
+		NestedObject myObject() {
+			return new NestedObject();
+		}
+
+	}
+
+	static class NestedObject {
+
+		@PostFilter("filterObject == null")
+		List<String> denyAllMethod() {
+			return new ArrayList<>(List.of("deny"));
+		}
+
 	}
 	}
 
 
 }
 }

+ 21 - 1
aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PreAuthorizeAspectTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -103,6 +103,13 @@ public class PreAuthorizeAspectTests {
 		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.prePostSecured::denyAllMethod);
 		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.prePostSecured::denyAllMethod);
 	}
 	}
 
 
+	@Test
+	public void nestedDenyAllPreAuthorizeDeniesAccess() {
+		SecurityContextHolder.getContext().setAuthentication(this.anne);
+		assertThatExceptionOfType(AccessDeniedException.class)
+			.isThrownBy(() -> this.secured.myObject().denyAllMethod());
+	}
+
 	interface SecuredInterface {
 	interface SecuredInterface {
 
 
 		@PreAuthorize("hasRole('X')")
 		@PreAuthorize("hasRole('X')")
@@ -134,6 +141,10 @@ public class PreAuthorizeAspectTests {
 			privateMethod();
 			privateMethod();
 		}
 		}
 
 
+		NestedObject myObject() {
+			return new NestedObject();
+		}
+
 	}
 	}
 
 
 	static class SecuredImplSubclass extends SecuredImpl {
 	static class SecuredImplSubclass extends SecuredImpl {
@@ -157,4 +168,13 @@ public class PreAuthorizeAspectTests {
 
 
 	}
 	}
 
 
+	static class NestedObject {
+
+		@PreAuthorize("denyAll")
+		void denyAllMethod() {
+
+		}
+
+	}
+
 }
 }

+ 19 - 1
aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PreFilterAspectTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -54,6 +54,11 @@ public class PreFilterAspectTests {
 		assertThat(this.prePostSecured.preFilterMethod(objects)).containsExactly("apple", "aubergine");
 		assertThat(this.prePostSecured.preFilterMethod(objects)).containsExactly("apple", "aubergine");
 	}
 	}
 
 
+	@Test
+	public void nestedDenyAllPreFilterDeniesAccess() {
+		assertThat(this.prePostSecured.myObject().denyAllMethod(new ArrayList<>(List.of("deny")))).isEmpty();
+	}
+
 	static class PrePostSecured {
 	static class PrePostSecured {
 
 
 		@PreFilter("filterObject.startsWith('a')")
 		@PreFilter("filterObject.startsWith('a')")
@@ -61,6 +66,19 @@ public class PreFilterAspectTests {
 			return objects;
 			return objects;
 		}
 		}
 
 
+		NestedObject myObject() {
+			return new NestedObject();
+		}
+
+	}
+
+	static class NestedObject {
+
+		@PreFilter("filterObject == null")
+		List<String> denyAllMethod(List<String> list) {
+			return list;
+		}
+
 	}
 	}
 
 
 }
 }

+ 1 - 1
aspects/src/test/java/org/springframework/security/authorization/method/aspectj/SecuredAspectTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.

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

@@ -19,6 +19,8 @@ package org.springframework.security.config.annotation.method.configuration;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
 
 
+import org.aopalliance.intercept.MethodInterceptor;
+
 import org.springframework.aop.framework.AopInfrastructureBean;
 import org.springframework.aop.framework.AopInfrastructureBean;
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.config.BeanDefinition;
@@ -27,6 +29,7 @@ import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Role;
 import org.springframework.context.annotation.Role;
 import org.springframework.security.authorization.AuthorizationAdvisorProxyFactory;
 import org.springframework.security.authorization.AuthorizationAdvisorProxyFactory;
 import org.springframework.security.authorization.method.AuthorizationAdvisor;
 import org.springframework.security.authorization.method.AuthorizationAdvisor;
+import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor;
 
 
 @Configuration(proxyBeanMethods = false)
 @Configuration(proxyBeanMethods = false)
 final class AuthorizationProxyConfiguration implements AopInfrastructureBean {
 final class AuthorizationProxyConfiguration implements AopInfrastructureBean {
@@ -41,4 +44,17 @@ final class AuthorizationProxyConfiguration implements AopInfrastructureBean {
 		return factory;
 		return factory;
 	}
 	}
 
 
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	static MethodInterceptor authorizeReturnObjectMethodInterceptor(ObjectProvider<AuthorizationAdvisor> provider,
+			AuthorizationAdvisorProxyFactory authorizationProxyFactory) {
+		AuthorizeReturnObjectMethodInterceptor interceptor = new AuthorizeReturnObjectMethodInterceptor(
+				authorizationProxyFactory);
+		List<AuthorizationAdvisor> advisors = new ArrayList<>();
+		provider.forEach(advisors::add);
+		advisors.add(interceptor);
+		authorizationProxyFactory.setAdvisors(advisors);
+		return interceptor;
+	}
+
 }
 }

+ 1 - 0
config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityAdvisorRegistrar.java

@@ -33,6 +33,7 @@ class MethodSecurityAdvisorRegistrar implements ImportBeanDefinitionRegistrar {
 		registerAsAdvisor("postAuthorizeAuthorization", registry);
 		registerAsAdvisor("postAuthorizeAuthorization", registry);
 		registerAsAdvisor("securedAuthorization", registry);
 		registerAsAdvisor("securedAuthorization", registry);
 		registerAsAdvisor("jsr250Authorization", registry);
 		registerAsAdvisor("jsr250Authorization", registry);
+		registerAsAdvisor("authorizeReturnObject", registry);
 	}
 	}
 
 
 	private void registerAsAdvisor(String prefix, BeanDefinitionRegistry registry) {
 	private void registerAsAdvisor(String prefix, BeanDefinitionRegistry registry) {

+ 16 - 0
config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationProxyConfiguration.java

@@ -19,6 +19,8 @@ package org.springframework.security.config.annotation.method.configuration;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
 
 
+import org.aopalliance.intercept.MethodInterceptor;
+
 import org.springframework.aop.framework.AopInfrastructureBean;
 import org.springframework.aop.framework.AopInfrastructureBean;
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.config.BeanDefinition;
@@ -27,6 +29,7 @@ import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Role;
 import org.springframework.context.annotation.Role;
 import org.springframework.security.authorization.ReactiveAuthorizationAdvisorProxyFactory;
 import org.springframework.security.authorization.ReactiveAuthorizationAdvisorProxyFactory;
 import org.springframework.security.authorization.method.AuthorizationAdvisor;
 import org.springframework.security.authorization.method.AuthorizationAdvisor;
+import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor;
 
 
 @Configuration(proxyBeanMethods = false)
 @Configuration(proxyBeanMethods = false)
 final class ReactiveAuthorizationProxyConfiguration implements AopInfrastructureBean {
 final class ReactiveAuthorizationProxyConfiguration implements AopInfrastructureBean {
@@ -42,4 +45,17 @@ final class ReactiveAuthorizationProxyConfiguration implements AopInfrastructure
 		return factory;
 		return factory;
 	}
 	}
 
 
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	static MethodInterceptor authorizeReturnObjectMethodInterceptor(ObjectProvider<AuthorizationAdvisor> provider,
+			ReactiveAuthorizationAdvisorProxyFactory authorizationProxyFactory) {
+		AuthorizeReturnObjectMethodInterceptor interceptor = new AuthorizeReturnObjectMethodInterceptor(
+				authorizationProxyFactory);
+		List<AuthorizationAdvisor> advisors = new ArrayList<>();
+		provider.forEach(advisors::add);
+		advisors.add(interceptor);
+		authorizationProxyFactory.setAdvisors(advisors);
+		return interceptor;
+	}
+
 }
 }

+ 187 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java

@@ -21,7 +21,10 @@ import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Arrays;
+import java.util.Iterator;
 import java.util.List;
 import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Consumer;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 import java.util.function.Supplier;
 
 
@@ -60,6 +63,7 @@ import org.springframework.security.authorization.AuthorizationEventPublisher;
 import org.springframework.security.authorization.AuthorizationManager;
 import org.springframework.security.authorization.AuthorizationManager;
 import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
 import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
 import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
 import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
+import org.springframework.security.authorization.method.AuthorizeReturnObject;
 import org.springframework.security.authorization.method.MethodInvocationResult;
 import org.springframework.security.authorization.method.MethodInvocationResult;
 import org.springframework.security.authorization.method.PrePostTemplateDefaults;
 import org.springframework.security.authorization.method.PrePostTemplateDefaults;
 import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
 import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
@@ -80,6 +84,7 @@ import org.springframework.web.context.support.AnnotationConfigWebApplicationCon
 
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatNoException;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mock;
@@ -662,6 +667,79 @@ public class PrePostMethodSecurityConfigurationTests {
 			.containsExactly("dave");
 			.containsExactly("dave");
 	}
 	}
 
 
+	@Test
+	@WithMockUser(authorities = "airplane:read")
+	public void findByIdWhenAuthorizedResultThenAuthorizes() {
+		this.spring.register(AuthorizeResultConfig.class).autowire();
+		FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
+		Flight flight = flights.findById("1");
+		assertThatNoException().isThrownBy(flight::getAltitude);
+		assertThatNoException().isThrownBy(flight::getSeats);
+	}
+
+	@Test
+	@WithMockUser(authorities = "seating:read")
+	public void findByIdWhenUnauthorizedResultThenDenies() {
+		this.spring.register(AuthorizeResultConfig.class).autowire();
+		FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
+		Flight flight = flights.findById("1");
+		assertThatNoException().isThrownBy(flight::getSeats);
+		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
+	}
+
+	@Test
+	@WithMockUser(authorities = "seating:read")
+	public void findAllWhenUnauthorizedResultThenDenies() {
+		this.spring.register(AuthorizeResultConfig.class).autowire();
+		FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
+		flights.findAll().forEachRemaining((flight) -> {
+			assertThatNoException().isThrownBy(flight::getSeats);
+			assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
+		});
+	}
+
+	@Test
+	public void removeWhenAuthorizedResultThenRemoves() {
+		this.spring.register(AuthorizeResultConfig.class).autowire();
+		FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
+		flights.remove("1");
+	}
+
+	@Test
+	@WithMockUser(authorities = "airplane:read")
+	public void findAllWhenPostFilterThenFilters() {
+		this.spring.register(AuthorizeResultConfig.class).autowire();
+		FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
+		flights.findAll()
+			.forEachRemaining((flight) -> assertThat(flight.getPassengers()).extracting(Passenger::getName)
+				.doesNotContain("Kevin Mitnick"));
+	}
+
+	@Test
+	@WithMockUser(authorities = "airplane:read")
+	public void findAllWhenPreFilterThenFilters() {
+		this.spring.register(AuthorizeResultConfig.class).autowire();
+		FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
+		flights.findAll().forEachRemaining((flight) -> {
+			flight.board(new ArrayList<>(List.of("John")));
+			assertThat(flight.getPassengers()).extracting(Passenger::getName).doesNotContain("John");
+			flight.board(new ArrayList<>(List.of("John Doe")));
+			assertThat(flight.getPassengers()).extracting(Passenger::getName).contains("John Doe");
+		});
+	}
+
+	@Test
+	@WithMockUser(authorities = "seating:read")
+	public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
+		this.spring.register(AuthorizeResultConfig.class).autowire();
+		FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
+		flights.findAll().forEachRemaining((flight) -> {
+			List<Passenger> passengers = flight.getPassengers();
+			passengers.forEach((passenger) -> assertThatExceptionOfType(AccessDeniedException.class)
+				.isThrownBy(passenger::getName));
+		});
+	}
+
 	private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
 	private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
 		return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
 		return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
 	}
 	}
@@ -1061,4 +1139,113 @@ public class PrePostMethodSecurityConfigurationTests {
 
 
 	}
 	}
 
 
+	@EnableMethodSecurity
+	@Configuration
+	static class AuthorizeResultConfig {
+
+		@Bean
+		FlightRepository flights() {
+			FlightRepository flights = new FlightRepository();
+			Flight one = new Flight("1", 35000d, 35);
+			one.board(new ArrayList<>(List.of("Marie Curie", "Kevin Mitnick", "Ada Lovelace")));
+			flights.save(one);
+			Flight two = new Flight("2", 32000d, 72);
+			two.board(new ArrayList<>(List.of("Albert Einstein")));
+			flights.save(two);
+			return flights;
+		}
+
+		@Bean
+		RoleHierarchy roleHierarchy() {
+			return RoleHierarchyImpl.withRolePrefix("").role("airplane:read").implies("seating:read").build();
+		}
+
+	}
+
+	@AuthorizeReturnObject
+	static class FlightRepository {
+
+		private final Map<String, Flight> flights = new ConcurrentHashMap<>();
+
+		Iterator<Flight> findAll() {
+			return this.flights.values().iterator();
+		}
+
+		Flight findById(String id) {
+			return this.flights.get(id);
+		}
+
+		Flight save(Flight flight) {
+			this.flights.put(flight.getId(), flight);
+			return flight;
+		}
+
+		void remove(String id) {
+			this.flights.remove(id);
+		}
+
+	}
+
+	static class Flight {
+
+		private final String id;
+
+		private final Double altitude;
+
+		private final Integer seats;
+
+		private final List<Passenger> passengers = new ArrayList<>();
+
+		Flight(String id, Double altitude, Integer seats) {
+			this.id = id;
+			this.altitude = altitude;
+			this.seats = seats;
+		}
+
+		String getId() {
+			return this.id;
+		}
+
+		@PreAuthorize("hasAuthority('airplane:read')")
+		Double getAltitude() {
+			return this.altitude;
+		}
+
+		@PreAuthorize("hasAuthority('seating:read')")
+		Integer getSeats() {
+			return this.seats;
+		}
+
+		@AuthorizeReturnObject
+		@PostAuthorize("hasAuthority('seating:read')")
+		@PostFilter("filterObject.name != 'Kevin Mitnick'")
+		List<Passenger> getPassengers() {
+			return this.passengers;
+		}
+
+		@PreAuthorize("hasAuthority('seating:read')")
+		@PreFilter("filterObject.contains(' ')")
+		void board(List<String> passengers) {
+			for (String passenger : passengers) {
+				this.passengers.add(new Passenger(passenger));
+			}
+		}
+
+	}
+
+	public static class Passenger {
+
+		String name;
+
+		public Passenger(String name) {
+			this.name = name;
+		}
+
+		@PreAuthorize("hasAuthority('airplane:read')")
+		public String getName() {
+			return this.name;
+		}
+
+	}
+
 }
 }

+ 231 - 1
config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -16,20 +16,36 @@
 
 
 package org.springframework.security.config.annotation.method.configuration;
 package org.springframework.security.config.annotation.method.configuration;
 
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.extension.ExtendWith;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
 
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.expression.EvaluationContext;
 import org.springframework.expression.EvaluationContext;
+import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.expression.SecurityExpressionRoot;
 import org.springframework.security.access.expression.SecurityExpressionRoot;
 import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
 import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
 import org.springframework.security.access.intercept.method.MockMethodInvocation;
 import org.springframework.security.access.intercept.method.MockMethodInvocation;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.security.access.prepost.PostFilter;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.access.prepost.PreFilter;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.authorization.method.AuthorizeReturnObject;
 import org.springframework.security.config.core.GrantedAuthorityDefaults;
 import org.springframework.security.config.core.GrantedAuthorityDefaults;
 import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
 import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.core.context.ReactiveSecurityContextHolder;
 
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThat;
 
 
@@ -85,6 +101,112 @@ public class ReactiveMethodSecurityConfigurationTests {
 		assertThat(root.hasRole("ABC")).isTrue();
 		assertThat(root.hasRole("ABC")).isTrue();
 	}
 	}
 
 
+	@Test
+	public void findByIdWhenAuthorizedResultThenAuthorizes() {
+		this.spring.register(AuthorizeResultConfig.class).autowire();
+		FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
+		TestingAuthenticationToken pilot = new TestingAuthenticationToken("user", "pass", "airplane:read");
+		StepVerifier
+			.create(flights.findById("1")
+				.flatMap(Flight::getAltitude)
+				.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
+			.expectNextCount(1)
+			.verifyComplete();
+		StepVerifier
+			.create(flights.findById("1")
+				.flatMap(Flight::getSeats)
+				.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
+			.expectNextCount(1)
+			.verifyComplete();
+	}
+
+	@Test
+	public void findByIdWhenUnauthorizedResultThenDenies() {
+		this.spring.register(AuthorizeResultConfig.class).autowire();
+		FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
+		TestingAuthenticationToken pilot = new TestingAuthenticationToken("user", "pass", "seating:read");
+		StepVerifier
+			.create(flights.findById("1")
+				.flatMap(Flight::getSeats)
+				.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
+			.expectNextCount(1)
+			.verifyComplete();
+		StepVerifier
+			.create(flights.findById("1")
+				.flatMap(Flight::getAltitude)
+				.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
+			.verifyError(AccessDeniedException.class);
+	}
+
+	@Test
+	public void findAllWhenUnauthorizedResultThenDenies() {
+		this.spring.register(AuthorizeResultConfig.class).autowire();
+		FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
+		TestingAuthenticationToken pilot = new TestingAuthenticationToken("user", "pass", "seating:read");
+		StepVerifier
+			.create(flights.findAll()
+				.flatMap(Flight::getSeats)
+				.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
+			.expectNextCount(2)
+			.verifyComplete();
+		StepVerifier
+			.create(flights.findAll()
+				.flatMap(Flight::getAltitude)
+				.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
+			.verifyError(AccessDeniedException.class);
+	}
+
+	@Test
+	public void removeWhenAuthorizedResultThenRemoves() {
+		this.spring.register(AuthorizeResultConfig.class).autowire();
+		FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
+		TestingAuthenticationToken pilot = new TestingAuthenticationToken("user", "pass", "seating:read");
+		StepVerifier.create(flights.remove("1").contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
+			.verifyComplete();
+	}
+
+	@Test
+	public void findAllWhenPostFilterThenFilters() {
+		this.spring.register(AuthorizeResultConfig.class).autowire();
+		FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
+		TestingAuthenticationToken pilot = new TestingAuthenticationToken("user", "pass", "airplane:read");
+		StepVerifier
+			.create(flights.findAll()
+				.flatMap(Flight::getPassengers)
+				.flatMap(Passenger::getName)
+				.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
+			.expectNext("Marie Curie", "Ada Lovelace", "Albert Einstein")
+			.verifyComplete();
+	}
+
+	@Test
+	public void findAllWhenPreFilterThenFilters() {
+		this.spring.register(AuthorizeResultConfig.class).autowire();
+		FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
+		TestingAuthenticationToken pilot = new TestingAuthenticationToken("user", "pass", "airplane:read");
+		StepVerifier
+			.create(flights.findAll()
+				.flatMap((flight) -> flight.board(Flux.just("John Doe", "John")).then(Mono.just(flight)))
+				.flatMap(Flight::getPassengers)
+				.flatMap(Passenger::getName)
+				.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
+			.expectNext("Marie Curie", "Ada Lovelace", "John Doe", "Albert Einstein", "John Doe")
+			.verifyComplete();
+	}
+
+	@Test
+	public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
+		this.spring.register(AuthorizeResultConfig.class).autowire();
+		FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
+		TestingAuthenticationToken pilot = new TestingAuthenticationToken("user", "pass", "seating:read");
+		StepVerifier
+			.create(flights.findAll()
+				.flatMap(Flight::getPassengers)
+				.flatMap(Passenger::getName)
+				.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
+			.verifyError(AccessDeniedException.class);
+	}
+
 	@Configuration
 	@Configuration
 	@EnableReactiveMethodSecurity // this imports ReactiveMethodSecurityConfiguration
 	@EnableReactiveMethodSecurity // this imports ReactiveMethodSecurityConfiguration
 	static class WithRolePrefixConfiguration {
 	static class WithRolePrefixConfiguration {
@@ -108,4 +230,112 @@ public class ReactiveMethodSecurityConfigurationTests {
 
 
 	}
 	}
 
 
+	@EnableReactiveMethodSecurity
+	@Configuration
+	static class AuthorizeResultConfig {
+
+		@Bean
+		FlightRepository flights() {
+			FlightRepository flights = new FlightRepository();
+			Flight one = new Flight("1", 35000d, 35);
+			one.board(Flux.just("Marie Curie", "Kevin Mitnick", "Ada Lovelace")).block();
+			flights.save(one).block();
+			Flight two = new Flight("2", 32000d, 72);
+			two.board(Flux.just("Albert Einstein")).block();
+			flights.save(two).block();
+			return flights;
+		}
+
+		@Bean
+		Function<Passenger, Mono<Boolean>> isNotKevin() {
+			return (passenger) -> passenger.getName().map((name) -> !name.equals("Kevin Mitnick"));
+		}
+
+	}
+
+	@AuthorizeReturnObject
+	static class FlightRepository {
+
+		private final Map<String, Flight> flights = new ConcurrentHashMap<>();
+
+		Flux<Flight> findAll() {
+			return Flux.fromIterable(this.flights.values());
+		}
+
+		Mono<Flight> findById(String id) {
+			return Mono.just(this.flights.get(id));
+		}
+
+		Mono<Flight> save(Flight flight) {
+			this.flights.put(flight.getId(), flight);
+			return Mono.just(flight);
+		}
+
+		Mono<Void> remove(String id) {
+			this.flights.remove(id);
+			return Mono.empty();
+		}
+
+	}
+
+	static class Flight {
+
+		private final String id;
+
+		private final Double altitude;
+
+		private final Integer seats;
+
+		private final List<Passenger> passengers = new ArrayList<>();
+
+		Flight(String id, Double altitude, Integer seats) {
+			this.id = id;
+			this.altitude = altitude;
+			this.seats = seats;
+		}
+
+		String getId() {
+			return this.id;
+		}
+
+		@PreAuthorize("hasAuthority('airplane:read')")
+		Mono<Double> getAltitude() {
+			return Mono.just(this.altitude);
+		}
+
+		@PreAuthorize("hasAnyAuthority('seating:read', 'airplane:read')")
+		Mono<Integer> getSeats() {
+			return Mono.just(this.seats);
+		}
+
+		@AuthorizeReturnObject
+		@PostAuthorize("hasAnyAuthority('seating:read', 'airplane:read')")
+		@PostFilter("@isNotKevin.apply(filterObject)")
+		Flux<Passenger> getPassengers() {
+			return Flux.fromIterable(this.passengers);
+		}
+
+		@PreAuthorize("hasAnyAuthority('seating:read', 'airplane:read')")
+		@PreFilter("filterObject.contains(' ')")
+		Mono<Void> board(Flux<String> passengers) {
+			return passengers.doOnNext((passenger) -> this.passengers.add(new Passenger(passenger))).then();
+		}
+
+	}
+
+	public static class Passenger {
+
+		String name;
+
+		public Passenger(String name) {
+			this.name = name;
+		}
+
+		@PreAuthorize("hasAuthority('airplane:read')")
+		public Mono<String> getName() {
+			return Mono.just(this.name);
+		}
+
+	}
+
 }
 }

+ 2 - 0
core/src/main/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactory.java

@@ -42,6 +42,7 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator;
 import org.springframework.security.authorization.method.AuthorizationAdvisor;
 import org.springframework.security.authorization.method.AuthorizationAdvisor;
 import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
 import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
 import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
 import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
+import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor;
 import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor;
 import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor;
 import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor;
 import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ClassUtils;
@@ -83,6 +84,7 @@ public final class AuthorizationAdvisorProxyFactory implements AuthorizationProx
 		advisors.add(AuthorizationManagerAfterMethodInterceptor.postAuthorize());
 		advisors.add(AuthorizationManagerAfterMethodInterceptor.postAuthorize());
 		advisors.add(new PreFilterAuthorizationMethodInterceptor());
 		advisors.add(new PreFilterAuthorizationMethodInterceptor());
 		advisors.add(new PostFilterAuthorizationMethodInterceptor());
 		advisors.add(new PostFilterAuthorizationMethodInterceptor());
+		advisors.add(new AuthorizeReturnObjectMethodInterceptor(this));
 		setAdvisors(advisors);
 		setAdvisors(advisors);
 	}
 	}
 
 

+ 2 - 0
core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactory.java

@@ -32,6 +32,7 @@ import org.springframework.aop.framework.ProxyFactory;
 import org.springframework.security.authorization.method.AuthorizationAdvisor;
 import org.springframework.security.authorization.method.AuthorizationAdvisor;
 import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor;
 import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor;
 import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor;
 import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor;
+import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor;
 import org.springframework.security.authorization.method.PostFilterAuthorizationReactiveMethodInterceptor;
 import org.springframework.security.authorization.method.PostFilterAuthorizationReactiveMethodInterceptor;
 import org.springframework.security.authorization.method.PreFilterAuthorizationReactiveMethodInterceptor;
 import org.springframework.security.authorization.method.PreFilterAuthorizationReactiveMethodInterceptor;
 
 
@@ -72,6 +73,7 @@ public final class ReactiveAuthorizationAdvisorProxyFactory implements Authoriza
 		advisors.add(AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize());
 		advisors.add(AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize());
 		advisors.add(new PreFilterAuthorizationReactiveMethodInterceptor());
 		advisors.add(new PreFilterAuthorizationReactiveMethodInterceptor());
 		advisors.add(new PostFilterAuthorizationReactiveMethodInterceptor());
 		advisors.add(new PostFilterAuthorizationReactiveMethodInterceptor());
+		advisors.add(new AuthorizeReturnObjectMethodInterceptor(this));
 		this.defaults.setAdvisors(advisors);
 		this.defaults.setAdvisors(advisors);
 	}
 	}
 
 

+ 4 - 2
core/src/main/java/org/springframework/security/authorization/method/AuthorizationInterceptorsOrder.java

@@ -43,12 +43,14 @@ public enum AuthorizationInterceptorsOrder {
 
 
 	JSR250,
 	JSR250,
 
 
-	POST_AUTHORIZE,
+	SECURE_RESULT(450),
+
+	POST_AUTHORIZE(500),
 
 
 	/**
 	/**
 	 * {@link PostFilterAuthorizationMethodInterceptor}
 	 * {@link PostFilterAuthorizationMethodInterceptor}
 	 */
 	 */
-	POST_FILTER,
+	POST_FILTER(600),
 
 
 	LAST(Integer.MAX_VALUE);
 	LAST(Integer.MAX_VALUE);
 
 

+ 41 - 0
core/src/main/java/org/springframework/security/authorization/method/AuthorizeReturnObject.java

@@ -0,0 +1,41 @@
+/*
+ * 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.authorization.method;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Wraps Spring Security method authorization advice around the return object of any
+ * method this annotation is applied to.
+ *
+ * <p>
+ * Placing this at the class level is semantically identical to placing it on each method
+ * in that class.
+ * </p>
+ *
+ * @author Josh Cummings
+ * @since 6.3
+ * @see AuthorizeReturnObjectMethodInterceptor
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE, ElementType.METHOD })
+public @interface AuthorizeReturnObject {
+
+}

+ 110 - 0
core/src/main/java/org/springframework/security/authorization/method/AuthorizeReturnObjectMethodInterceptor.java

@@ -0,0 +1,110 @@
+/*
+ * 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.authorization.method;
+
+import java.lang.reflect.Method;
+import java.util.function.Predicate;
+
+import org.aopalliance.aop.Advice;
+import org.aopalliance.intercept.MethodInvocation;
+
+import org.springframework.aop.Pointcut;
+import org.springframework.aop.support.Pointcuts;
+import org.springframework.aop.support.StaticMethodMatcherPointcut;
+import org.springframework.security.authorization.AuthorizationProxyFactory;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+
+/**
+ * A method interceptor that applies the given {@link AuthorizationProxyFactory} to any
+ * return value annotated with {@link AuthorizeReturnObject}
+ *
+ * @author Josh Cummings
+ * @since 6.3
+ * @see org.springframework.security.authorization.AuthorizationAdvisorProxyFactory
+ */
+public final class AuthorizeReturnObjectMethodInterceptor implements AuthorizationAdvisor {
+
+	private final AuthorizationProxyFactory authorizationProxyFactory;
+
+	private Pointcut pointcut = Pointcuts.intersection(
+			new MethodReturnTypePointcut(Predicate.not(ClassUtils::isVoidType)),
+			AuthorizationMethodPointcuts.forAnnotations(AuthorizeReturnObject.class));
+
+	private int order = AuthorizationInterceptorsOrder.SECURE_RESULT.getOrder();
+
+	public AuthorizeReturnObjectMethodInterceptor(AuthorizationProxyFactory authorizationProxyFactory) {
+		Assert.notNull(authorizationProxyFactory, "authorizationManager cannot be null");
+		this.authorizationProxyFactory = authorizationProxyFactory;
+	}
+
+	@Override
+	public Object invoke(MethodInvocation mi) throws Throwable {
+		Object result = mi.proceed();
+		if (result == null) {
+			return null;
+		}
+		return this.authorizationProxyFactory.proxy(result);
+	}
+
+	@Override
+	public int getOrder() {
+		return this.order;
+	}
+
+	public void setOrder(int order) {
+		this.order = order;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Pointcut getPointcut() {
+		return this.pointcut;
+	}
+
+	public void setPointcut(Pointcut pointcut) {
+		this.pointcut = pointcut;
+	}
+
+	@Override
+	public Advice getAdvice() {
+		return this;
+	}
+
+	@Override
+	public boolean isPerInstance() {
+		return true;
+	}
+
+	static final class MethodReturnTypePointcut extends StaticMethodMatcherPointcut {
+
+		private final Predicate<Class<?>> returnTypeMatches;
+
+		MethodReturnTypePointcut(Predicate<Class<?>> returnTypeMatches) {
+			this.returnTypeMatches = returnTypeMatches;
+		}
+
+		@Override
+		public boolean matches(Method method, Class<?> targetClass) {
+			return this.returnTypeMatches.test(method.getReturnType());
+		}
+
+	}
+
+}

+ 1 - 1
core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java

@@ -284,7 +284,7 @@ public class AuthorizationAdvisorProxyFactoryTests {
 	public void proxyWhenPreAuthorizeForClassThenHonors() {
 	public void proxyWhenPreAuthorizeForClassThenHonors() {
 		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory();
 		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory();
 		Class<Flight> clazz = proxy(factory, Flight.class);
 		Class<Flight> clazz = proxy(factory, Flight.class);
-		assertThat(clazz.getSimpleName()).contains("SpringCGLIB$$0");
+		assertThat(clazz.getSimpleName()).contains("SpringCGLIB$$");
 		Flight secured = proxy(factory, this.flight);
 		Flight secured = proxy(factory, this.flight);
 		assertThat(secured.getClass()).isSameAs(clazz);
 		assertThat(secured.getClass()).isSameAs(clazz);
 		SecurityContextHolder.getContext().setAuthentication(this.user);
 		SecurityContextHolder.getContext().setAuthentication(this.user);

+ 1 - 1
core/src/test/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactoryTests.java

@@ -117,7 +117,7 @@ public class ReactiveAuthorizationAdvisorProxyFactoryTests {
 	public void proxyWhenPreAuthorizeForClassThenHonors() {
 	public void proxyWhenPreAuthorizeForClassThenHonors() {
 		ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory();
 		ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory();
 		Class<Flight> clazz = proxy(factory, Flight.class);
 		Class<Flight> clazz = proxy(factory, Flight.class);
-		assertThat(clazz.getSimpleName()).contains("SpringCGLIB$$0");
+		assertThat(clazz.getSimpleName()).contains("SpringCGLIB$$");
 		Flight secured = proxy(factory, this.flight);
 		Flight secured = proxy(factory, this.flight);
 		StepVerifier
 		StepVerifier
 			.create(secured.getAltitude().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user)))
 			.create(secured.getAltitude().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user)))

+ 84 - 2
docs/modules/ROOT/pages/servlet/authorization/method-security.adoc

@@ -1707,8 +1707,7 @@ For interfaces, either annotations or the `-parameters` approach must be used.
 
 
 Spring Security also supports wrapping any object that is annotated its method security annotations.
 Spring Security also supports wrapping any object that is annotated its method security annotations.
 
 
-To achieve this, you can autowire the provided `AuthorizationProxyFactory` instance, which is based on which method security interceptors you have configured.
-If you are using `@EnableMethodSecurity`, then this means that it will by default have the interceptors for `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter`.
+The simplest way to achieve this is to mark any method that returns the object you wish to authorize with the `@AuthorizeReturnObject` annotation.
 
 
 For example, consider the following `User` class:
 For example, consider the following `User` class:
 
 
@@ -1746,6 +1745,89 @@ class User (val name:String, @get:PreAuthorize("hasAuthority('user:read')") val
 ----
 ----
 ======
 ======
 
 
+Given an interface like this one:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+public class UserRepository {
+	@AuthorizeReturnObject
+    Optional<User> findByName(String name) {
+		// ...
+    }
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+class UserRepository {
+    @AuthorizeReturnObject
+    fun findByName(name:String?): Optional<User?>? {
+        // ...
+    }
+}
+----
+======
+
+Then any `User` that is returned from `findById` will be secured like other Spring Security-protected components:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Autowired
+UserRepository users;
+
+@Test
+void getEmailWhenProxiedThenAuthorizes() {
+    Optional<User> securedUser = users.findByName("name");
+    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> securedUser.get().getEmail());
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+
+import jdk.incubator.vector.VectorOperators.Test
+import java.nio.file.AccessDeniedException
+import java.util.*
+
+@Autowired
+var users:UserRepository? = null
+
+@Test
+fun getEmailWhenProxiedThenAuthorizes() {
+    val securedUser: Optional<User> = users.findByName("name")
+    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy{securedUser.get().getEmail()}
+}
+----
+======
+
+[NOTE]
+====
+`@AuthorizeReturnObject` can be placed at the class level. Note, though, that this means Spring Security will proxy any return object, including ``String``, ``Integer`` and other types.
+This is often not what you want to do.
+
+In most cases, you will want to annotate the individual methods.
+====
+
+=== Programmatically Proxying
+
+You can also programmatically proxy a given object.
+
+To achieve this, you can autowire the provided `AuthorizationProxyFactory` instance, which is based on which method security interceptors you have configured.
+If you are using `@EnableMethodSecurity`, then this means that it will by default have the interceptors for `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter`.
+
+
 You can proxy an instance of user in the following way:
 You can proxy an instance of user in the following way:
 
 
 [tabs]
 [tabs]

+ 1 - 0
docs/modules/ROOT/pages/whats-new.adoc

@@ -11,6 +11,7 @@ Below are the highlights of the release.
 == Authorization
 == Authorization
 
 
 - https://github.com/spring-projects/spring-security/issues/14596[gh-14596] - xref:servlet/authorization/method-security.adoc[docs] - Add Programmatic Proxy Support for Method Security
 - https://github.com/spring-projects/spring-security/issues/14596[gh-14596] - xref:servlet/authorization/method-security.adoc[docs] - Add Programmatic Proxy Support for Method Security
+- https://github.com/spring-projects/spring-security/issues/14597[gh-14597] - xref:servlet/authorization/method-security.adoc[docs] - Add Securing of Return Values
 
 
 == Configuration
 == Configuration