Browse Source

Add Authorization Proxy Support

Closes gh-14596
Josh Cummings 1 năm trước cách đây
mục cha
commit
52dfbfb5b3
16 tập tin đã thay đổi với 1360 bổ sung27 xóa
  1. 44 0
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java
  2. 2 1
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java
  3. 1 0
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java
  4. 4 5
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java
  5. 2 1
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java
  6. 92 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfigurationTests.java
  7. 308 0
      core/src/main/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactory.java
  8. 40 0
      core/src/main/java/org/springframework/security/authorization/AuthorizationProxyFactory.java
  9. 37 0
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisor.java
  10. 1 5
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java
  11. 1 5
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java
  12. 1 5
      core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java
  13. 1 5
      core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java
  14. 431 0
      core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java
  15. 391 0
      docs/modules/ROOT/pages/servlet/authorization/method-security.adoc
  16. 4 0
      docs/modules/ROOT/pages/whats-new.adoc

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

@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.aop.framework.AopInfrastructureBean;
+import org.springframework.beans.factory.ObjectProvider;
+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.core.annotation.AnnotationAwareOrderComparator;
+import org.springframework.security.authorization.AuthorizationAdvisorProxyFactory;
+import org.springframework.security.authorization.method.AuthorizationAdvisor;
+
+@Configuration(proxyBeanMethods = false)
+final class AuthorizationProxyConfiguration implements AopInfrastructureBean {
+
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	static AuthorizationAdvisorProxyFactory authorizationProxyFactory(ObjectProvider<AuthorizationAdvisor> provider) {
+		List<AuthorizationAdvisor> advisors = new ArrayList<>();
+		provider.forEach(advisors::add);
+		AnnotationAwareOrderComparator.sort(advisors);
+		return new AuthorizationAdvisorProxyFactory(advisors);
+	}
+
+}

+ 2 - 1
config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java

@@ -20,6 +20,7 @@ import io.micrometer.observation.ObservationRegistry;
 import org.aopalliance.intercept.MethodInterceptor;
 import org.aopalliance.intercept.MethodInvocation;
 
+import org.springframework.aop.framework.AopInfrastructureBean;
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.context.annotation.Bean;
@@ -48,7 +49,7 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
  */
 @Configuration(proxyBeanMethods = false)
 @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
-final class Jsr250MethodSecurityConfiguration implements ImportAware {
+final class Jsr250MethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
 
 	private int interceptorOrderOffset;
 

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

@@ -56,6 +56,7 @@ final class MethodSecuritySelector implements ImportSelector {
 		if (annotation.jsr250Enabled()) {
 			imports.add(Jsr250MethodSecurityConfiguration.class.getName());
 		}
+		imports.add(AuthorizationProxyConfiguration.class.getName());
 		return imports.toArray(new String[0]);
 	}
 

+ 4 - 5
config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java

@@ -27,7 +27,6 @@ import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import org.springframework.aop.Pointcut;
-import org.springframework.aop.PointcutAdvisor;
 import org.springframework.aop.framework.AopInfrastructureBean;
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.config.BeanDefinition;
@@ -36,7 +35,6 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.ImportAware;
 import org.springframework.context.annotation.Role;
-import org.springframework.core.Ordered;
 import org.springframework.core.type.AnnotationMetadata;
 import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
 import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
@@ -44,6 +42,7 @@ import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
 import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
 import org.springframework.security.authorization.AuthorizationEventPublisher;
 import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.authorization.method.AuthorizationAdvisor;
 import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
 import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
 import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager;
@@ -65,7 +64,7 @@ import org.springframework.util.function.SingletonSupplier;
  */
 @Configuration(proxyBeanMethods = false)
 @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
-final class PrePostMethodSecurityConfiguration implements ImportAware {
+final class PrePostMethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
 
 	private int interceptorOrderOffset;
 
@@ -175,8 +174,8 @@ final class PrePostMethodSecurityConfiguration implements ImportAware {
 		this.interceptorOrderOffset = annotation.offset();
 	}
 
-	private static final class DeferringMethodInterceptor<M extends Ordered & MethodInterceptor & PointcutAdvisor>
-			implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+	private static final class DeferringMethodInterceptor<M extends AuthorizationAdvisor>
+			implements AuthorizationAdvisor {
 
 		private final Pointcut pointcut;
 

+ 2 - 1
config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java

@@ -20,6 +20,7 @@ import io.micrometer.observation.ObservationRegistry;
 import org.aopalliance.intercept.MethodInterceptor;
 import org.aopalliance.intercept.MethodInvocation;
 
+import org.springframework.aop.framework.AopInfrastructureBean;
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.context.annotation.Bean;
@@ -48,7 +49,7 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
  */
 @Configuration(proxyBeanMethods = false)
 @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
-final class SecuredMethodSecurityConfiguration implements ImportAware {
+final class SecuredMethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
 
 	private int interceptorOrderOffset;
 

+ 92 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfigurationTests.java

@@ -0,0 +1,92 @@
+/*
+ * 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;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authorization.AuthorizationProxyFactory;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+/**
+ * Tests for {@link PrePostMethodSecurityConfiguration}.
+ *
+ * @author Evgeniy Cheban
+ * @author Josh Cummings
+ */
+@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
+@SecurityTestExecutionListeners
+public class AuthorizationProxyConfigurationTests {
+
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired
+	AuthorizationProxyFactory proxyFactory;
+
+	@WithMockUser
+	@Test
+	public void proxyWhenNotPreAuthorizedThenDenies() {
+		this.spring.register(DefaultsConfig.class).autowire();
+		Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
+		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(toaster::makeToast)
+			.withMessage("Access Denied");
+		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(toaster::extractBread)
+			.withMessage("Access Denied");
+	}
+
+	@WithMockUser(roles = "ADMIN")
+	@Test
+	public void proxyWhenPreAuthorizedThenAllows() {
+		this.spring.register(DefaultsConfig.class).autowire();
+		Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
+		toaster.makeToast();
+		assertThat(toaster.extractBread()).isEqualTo("yummy");
+	}
+
+	@EnableMethodSecurity
+	@Configuration
+	static class DefaultsConfig {
+
+	}
+
+	static class Toaster {
+
+		@PreAuthorize("hasRole('ADMIN')")
+		void makeToast() {
+
+		}
+
+		@PostAuthorize("hasRole('ADMIN')")
+		String extractBread() {
+			return "yummy";
+		}
+
+	}
+
+}

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

@@ -0,0 +1,308 @@
+/*
+ * 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;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Queue;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.Stream;
+
+import org.springframework.aop.Advisor;
+import org.springframework.aop.framework.ProxyFactory;
+import org.springframework.core.annotation.AnnotationAwareOrderComparator;
+import org.springframework.security.authorization.method.AuthorizationAdvisor;
+import org.springframework.util.ClassUtils;
+
+/**
+ * A proxy factory for applying authorization advice to an arbitrary object.
+ *
+ * <p>
+ * For example, consider a non-Spring-managed object {@code Foo}: <pre>
+ *     class Foo {
+ *         &#064;PreAuthorize("hasAuthority('bar:read')")
+ *         String bar() { ... }
+ *     }
+ * </pre>
+ *
+ * Use {@link AuthorizationAdvisorProxyFactory} to wrap the instance in Spring Security's
+ * {@link org.springframework.security.access.prepost.PreAuthorize} method interceptor
+ * like so:
+ *
+ * <pre>
+ *     AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
+ *     AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize);
+ *     Foo foo = new Foo();
+ *     foo.bar(); // passes
+ *     Foo securedFoo = proxyFactory.proxy(foo);
+ *     securedFoo.bar(); // access denied!
+ * </pre>
+ *
+ * @author Josh Cummings
+ * @since 6.3
+ */
+public final class AuthorizationAdvisorProxyFactory implements AuthorizationProxyFactory {
+
+	private final Collection<AuthorizationAdvisor> advisors;
+
+	public AuthorizationAdvisorProxyFactory(AuthorizationAdvisor... advisors) {
+		this.advisors = List.of(advisors);
+	}
+
+	public AuthorizationAdvisorProxyFactory(Collection<AuthorizationAdvisor> advisors) {
+		this.advisors = List.copyOf(advisors);
+	}
+
+	/**
+	 * Create a new {@link AuthorizationAdvisorProxyFactory} that includes the given
+	 * advisors in addition to any advisors {@code this} instance already has.
+	 *
+	 * <p>
+	 * All advisors are re-sorted by their advisor order.
+	 * @param advisors the advisors to add
+	 * @return a new {@link AuthorizationAdvisorProxyFactory} instance
+	 */
+	public AuthorizationAdvisorProxyFactory withAdvisors(AuthorizationAdvisor... advisors) {
+		List<AuthorizationAdvisor> merged = new ArrayList<>(this.advisors.size() + advisors.length);
+		merged.addAll(this.advisors);
+		merged.addAll(List.of(advisors));
+		AnnotationAwareOrderComparator.sort(merged);
+		return new AuthorizationAdvisorProxyFactory(merged);
+	}
+
+	/**
+	 * Proxy an object to enforce authorization advice.
+	 *
+	 * <p>
+	 * Proxies any instance of a non-final class or a class that implements more than one
+	 * interface.
+	 *
+	 * <p>
+	 * If {@code target} is an {@link Iterator}, {@link Collection}, {@link Array},
+	 * {@link Map}, {@link Stream}, or {@link Optional}, then the element or value type is
+	 * proxied.
+	 *
+	 * <p>
+	 * If {@code target} is a {@link Class}, then {@link ProxyFactory#getProxyClass} is
+	 * invoked instead.
+	 * @param target the instance to proxy
+	 * @return the proxied instance
+	 */
+	@Override
+	public Object proxy(Object target) {
+		if (target == null) {
+			return null;
+		}
+		if (target instanceof Class<?> targetClass) {
+			return proxyClass(targetClass);
+		}
+		if (target instanceof Iterator<?> iterator) {
+			return proxyIterator(iterator);
+		}
+		if (target instanceof Queue<?> queue) {
+			return proxyQueue(queue);
+		}
+		if (target instanceof List<?> list) {
+			return proxyList(list);
+		}
+		if (target instanceof SortedSet<?> set) {
+			return proxySortedSet(set);
+		}
+		if (target instanceof Set<?> set) {
+			return proxySet(set);
+		}
+		if (target.getClass().isArray()) {
+			return proxyArray((Object[]) target);
+		}
+		if (target instanceof SortedMap<?, ?> map) {
+			return proxySortedMap(map);
+		}
+		if (target instanceof Iterable<?> iterable) {
+			return proxyIterable(iterable);
+		}
+		if (target instanceof Map<?, ?> map) {
+			return proxyMap(map);
+		}
+		if (target instanceof Stream<?> stream) {
+			return proxyStream(stream);
+		}
+		if (target instanceof Optional<?> optional) {
+			return proxyOptional(optional);
+		}
+		ProxyFactory factory = new ProxyFactory(target);
+		for (Advisor advisor : this.advisors) {
+			factory.addAdvisors(advisor);
+		}
+		factory.setProxyTargetClass(!Modifier.isFinal(target.getClass().getModifiers()));
+		return factory.getProxy();
+	}
+
+	@SuppressWarnings("unchecked")
+	private <T> T proxyCast(T target) {
+		return (T) proxy(target);
+	}
+
+	private Class<?> proxyClass(Class<?> targetClass) {
+		ProxyFactory factory = new ProxyFactory();
+		factory.setTargetClass(targetClass);
+		factory.setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass));
+		factory.setProxyTargetClass(!Modifier.isFinal(targetClass.getModifiers()));
+		for (Advisor advisor : this.advisors) {
+			factory.addAdvisors(advisor);
+		}
+		return factory.getProxyClass(getClass().getClassLoader());
+	}
+
+	private <T> Iterable<T> proxyIterable(Iterable<T> iterable) {
+		return () -> proxyIterator(iterable.iterator());
+	}
+
+	private <T> Iterator<T> proxyIterator(Iterator<T> iterator) {
+		return new Iterator<>() {
+			@Override
+			public boolean hasNext() {
+				return iterator.hasNext();
+			}
+
+			@Override
+			public T next() {
+				return proxyCast(iterator.next());
+			}
+		};
+	}
+
+	private <T> SortedSet<T> proxySortedSet(SortedSet<T> set) {
+		SortedSet<T> proxies = new TreeSet<>(set.comparator());
+		for (T toProxy : set) {
+			proxies.add(proxyCast(toProxy));
+		}
+		try {
+			set.clear();
+			set.addAll(proxies);
+			return proxies;
+		}
+		catch (UnsupportedOperationException ex) {
+			return Collections.unmodifiableSortedSet(proxies);
+		}
+	}
+
+	private <T> Set<T> proxySet(Set<T> set) {
+		Set<T> proxies = new LinkedHashSet<>(set.size());
+		for (T toProxy : set) {
+			proxies.add(proxyCast(toProxy));
+		}
+		try {
+			set.clear();
+			set.addAll(proxies);
+			return proxies;
+		}
+		catch (UnsupportedOperationException ex) {
+			return Collections.unmodifiableSet(proxies);
+		}
+	}
+
+	private <T> Queue<T> proxyQueue(Queue<T> queue) {
+		Queue<T> proxies = new LinkedList<>();
+		for (T toProxy : queue) {
+			proxies.add(proxyCast(toProxy));
+		}
+		queue.clear();
+		queue.addAll(proxies);
+		return proxies;
+	}
+
+	private <T> List<T> proxyList(List<T> list) {
+		List<T> proxies = new ArrayList<>(list.size());
+		for (T toProxy : list) {
+			proxies.add(proxyCast(toProxy));
+		}
+		try {
+			list.clear();
+			list.addAll(proxies);
+			return proxies;
+		}
+		catch (UnsupportedOperationException ex) {
+			return Collections.unmodifiableList(proxies);
+		}
+	}
+
+	private Object[] proxyArray(Object[] objects) {
+		List<Object> retain = new ArrayList<>(objects.length);
+		for (Object object : objects) {
+			retain.add(proxy(object));
+		}
+		Object[] proxies = (Object[]) Array.newInstance(objects.getClass().getComponentType(), retain.size());
+		for (int i = 0; i < retain.size(); i++) {
+			proxies[i] = retain.get(i);
+		}
+		return proxies;
+	}
+
+	private <K, V> SortedMap<K, V> proxySortedMap(SortedMap<K, V> entries) {
+		SortedMap<K, V> proxies = new TreeMap<>(entries.comparator());
+		for (Map.Entry<K, V> entry : entries.entrySet()) {
+			proxies.put(entry.getKey(), proxyCast(entry.getValue()));
+		}
+		try {
+			entries.clear();
+			entries.putAll(proxies);
+			return entries;
+		}
+		catch (UnsupportedOperationException ex) {
+			return Collections.unmodifiableSortedMap(proxies);
+		}
+	}
+
+	private <K, V> Map<K, V> proxyMap(Map<K, V> entries) {
+		Map<K, V> proxies = new LinkedHashMap<>(entries.size());
+		for (Map.Entry<K, V> entry : entries.entrySet()) {
+			proxies.put(entry.getKey(), proxyCast(entry.getValue()));
+		}
+		try {
+			entries.clear();
+			entries.putAll(proxies);
+			return entries;
+		}
+		catch (UnsupportedOperationException ex) {
+			return Collections.unmodifiableMap(proxies);
+		}
+	}
+
+	private Stream<?> proxyStream(Stream<?> stream) {
+		return stream.map(this::proxy).onClose(stream::close);
+	}
+
+	@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+	private Optional<?> proxyOptional(Optional<?> optional) {
+		return optional.map(this::proxy);
+	}
+
+}

+ 40 - 0
core/src/main/java/org/springframework/security/authorization/AuthorizationProxyFactory.java

@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+/**
+ * A factory for wrapping arbitrary objects in authorization-related advice
+ *
+ * @author Josh Cummings
+ * @since 6.3
+ * @see AuthorizationAdvisorProxyFactory
+ */
+public interface AuthorizationProxyFactory {
+
+	/**
+	 * Wrap the given {@code object} in authorization-related advice.
+	 *
+	 * <p>
+	 * Please check the implementation for which kinds of objects it supports.
+	 * @param object the object to proxy
+	 * @return the proxied object
+	 * @throws org.springframework.aop.framework.AopConfigException if a proxy cannot be
+	 * created
+	 */
+	Object proxy(Object object);
+
+}

+ 37 - 0
core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisor.java

@@ -0,0 +1,37 @@
+/*
+ * 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 org.aopalliance.intercept.MethodInterceptor;
+
+import org.springframework.aop.PointcutAdvisor;
+import org.springframework.aop.framework.AopInfrastructureBean;
+import org.springframework.core.Ordered;
+
+/**
+ * An interface that indicates method security advice
+ *
+ * @author Josh Cummings
+ * @since 6.3
+ * @see AuthorizationManagerBeforeMethodInterceptor
+ * @see AuthorizationManagerAfterMethodInterceptor
+ * @see PreFilterAuthorizationMethodInterceptor
+ * @see PostFilterAuthorizationMethodInterceptor
+ */
+public interface AuthorizationAdvisor extends Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+
+}

+ 1 - 5
core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java

@@ -25,9 +25,6 @@ import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
 import org.springframework.aop.Pointcut;
-import org.springframework.aop.PointcutAdvisor;
-import org.springframework.aop.framework.AopInfrastructureBean;
-import org.springframework.core.Ordered;
 import org.springframework.core.log.LogMessage;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.prepost.PostAuthorize;
@@ -48,8 +45,7 @@ import org.springframework.util.Assert;
  * @author Josh Cummings
  * @since 5.6
  */
-public final class AuthorizationManagerAfterMethodInterceptor
-		implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+public final class AuthorizationManagerAfterMethodInterceptor implements AuthorizationAdvisor {
 
 	private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;
 

+ 1 - 5
core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java

@@ -28,9 +28,6 @@ import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
 import org.springframework.aop.Pointcut;
-import org.springframework.aop.PointcutAdvisor;
-import org.springframework.aop.framework.AopInfrastructureBean;
-import org.springframework.core.Ordered;
 import org.springframework.core.log.LogMessage;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.annotation.Secured;
@@ -52,8 +49,7 @@ import org.springframework.util.Assert;
  * @author Josh Cummings
  * @since 5.6
  */
-public final class AuthorizationManagerBeforeMethodInterceptor
-		implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+public final class AuthorizationManagerBeforeMethodInterceptor implements AuthorizationAdvisor {
 
 	private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;
 

+ 1 - 5
core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java

@@ -23,9 +23,6 @@ import org.aopalliance.intercept.MethodInterceptor;
 import org.aopalliance.intercept.MethodInvocation;
 
 import org.springframework.aop.Pointcut;
-import org.springframework.aop.PointcutAdvisor;
-import org.springframework.aop.framework.AopInfrastructureBean;
-import org.springframework.core.Ordered;
 import org.springframework.expression.EvaluationContext;
 import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
 import org.springframework.security.access.prepost.PostFilter;
@@ -43,8 +40,7 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
  * @author Josh Cummings
  * @since 5.6
  */
-public final class PostFilterAuthorizationMethodInterceptor
-		implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+public final class PostFilterAuthorizationMethodInterceptor implements AuthorizationAdvisor {
 
 	private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;
 

+ 1 - 5
core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java

@@ -23,9 +23,6 @@ import org.aopalliance.intercept.MethodInterceptor;
 import org.aopalliance.intercept.MethodInvocation;
 
 import org.springframework.aop.Pointcut;
-import org.springframework.aop.PointcutAdvisor;
-import org.springframework.aop.framework.AopInfrastructureBean;
-import org.springframework.core.Ordered;
 import org.springframework.expression.EvaluationContext;
 import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
 import org.springframework.security.access.prepost.PreFilter;
@@ -44,8 +41,7 @@ import org.springframework.util.StringUtils;
  * @author Josh Cummings
  * @since 5.6
  */
-public final class PreFilterAuthorizationMethodInterceptor
-		implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+public final class PreFilterAuthorizationMethodInterceptor implements AuthorizationAdvisor {
 
 	private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;
 

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

@@ -0,0 +1,431 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Queue;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.Stream;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.aop.Pointcut;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authentication.TestAuthentication;
+import org.springframework.security.authorization.method.AuthorizationAdvisor;
+import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class AuthorizationAdvisorProxyFactoryTests {
+
+	private final Authentication user = TestAuthentication.authenticatedUser();
+
+	private final Authentication admin = TestAuthentication.authenticatedAdmin();
+
+	private final Flight flight = new Flight();
+
+	private final User alan = new User("alan", "alan", "turing");
+
+	@Test
+	public void proxyWhenPreAuthorizeThenHonors() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		Flight flight = new Flight();
+		assertThat(flight.getAltitude()).isEqualTo(35000d);
+		Flight secured = proxy(factory, flight);
+		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured::getAltitude);
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenPreAuthorizeOnInterfaceThenHonors() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		assertThat(this.alan.getFirstName()).isEqualTo("alan");
+		User secured = proxy(factory, this.alan);
+		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured::getFirstName);
+		SecurityContextHolder.getContext().setAuthentication(authenticated("alan"));
+		assertThat(secured.getFirstName()).isEqualTo("alan");
+		SecurityContextHolder.getContext().setAuthentication(this.admin);
+		assertThat(secured.getFirstName()).isEqualTo("alan");
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenPreAuthorizeOnRecordThenHonors() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		HasSecret repo = new Repository("secret");
+		assertThat(repo.secret()).isEqualTo("secret");
+		HasSecret secured = proxy(factory, repo);
+		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured::secret);
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		assertThat(repo.secret()).isEqualTo("secret");
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenImmutableListThenReturnsSecuredImmutableList() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		List<Flight> flights = List.of(this.flight);
+		List<Flight> secured = proxy(factory, flights);
+		secured.forEach(
+				(flight) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude));
+		assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(secured::clear);
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenImmutableSetThenReturnsSecuredImmutableSet() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		Set<Flight> flights = Set.of(this.flight);
+		Set<Flight> secured = proxy(factory, flights);
+		secured.forEach(
+				(flight) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude));
+		assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(secured::clear);
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenQueueThenReturnsSecuredQueue() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		Queue<Flight> flights = new LinkedList<>(List.of(this.flight));
+		Queue<Flight> secured = proxy(factory, flights);
+		assertThat(flights.size()).isEqualTo(secured.size());
+		secured.forEach(
+				(flight) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude));
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenImmutableSortedSetThenReturnsSecuredImmutableSortedSet() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		SortedSet<User> users = Collections.unmodifiableSortedSet(new TreeSet<>(Set.of(this.alan)));
+		SortedSet<User> secured = proxy(factory, users);
+		secured
+			.forEach((user) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(user::getFirstName));
+		assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(secured::clear);
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenImmutableSortedMapThenReturnsSecuredImmutableSortedMap() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		SortedMap<String, User> users = Collections
+			.unmodifiableSortedMap(new TreeMap<>(Map.of(this.alan.getId(), this.alan)));
+		SortedMap<String, User> secured = proxy(factory, users);
+		secured.forEach(
+				(id, user) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(user::getFirstName));
+		assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(secured::clear);
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenImmutableMapThenReturnsSecuredImmutableMap() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		Map<String, User> users = Map.of(this.alan.getId(), this.alan);
+		Map<String, User> secured = proxy(factory, users);
+		secured.forEach(
+				(id, user) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(user::getFirstName));
+		assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(secured::clear);
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenMutableListThenReturnsSecuredMutableList() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		List<Flight> flights = new ArrayList<>(List.of(this.flight));
+		List<Flight> secured = proxy(factory, flights);
+		secured.forEach(
+				(flight) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude));
+		secured.clear();
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenMutableSetThenReturnsSecuredMutableSet() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		Set<Flight> flights = new HashSet<>(Set.of(this.flight));
+		Set<Flight> secured = proxy(factory, flights);
+		secured.forEach(
+				(flight) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude));
+		secured.clear();
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenMutableSortedSetThenReturnsSecuredMutableSortedSet() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		SortedSet<User> users = new TreeSet<>(Set.of(this.alan));
+		SortedSet<User> secured = proxy(factory, users);
+		secured.forEach((u) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(u::getFirstName));
+		secured.clear();
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenMutableSortedMapThenReturnsSecuredMutableSortedMap() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		SortedMap<String, User> users = new TreeMap<>(Map.of(this.alan.getId(), this.alan));
+		SortedMap<String, User> secured = proxy(factory, users);
+		secured.forEach((id, u) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(u::getFirstName));
+		secured.clear();
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenMutableMapThenReturnsSecuredMutableMap() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		Map<String, User> users = new HashMap<>(Map.of(this.alan.getId(), this.alan));
+		Map<String, User> secured = proxy(factory, users);
+		secured.forEach((id, u) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(u::getFirstName));
+		secured.clear();
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenPreAuthorizeForOptionalThenHonors() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		Optional<Flight> flights = Optional.of(this.flight);
+		assertThat(flights.get().getAltitude()).isEqualTo(35000d);
+		Optional<Flight> secured = proxy(factory, flights);
+		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.ifPresent(Flight::getAltitude));
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenPreAuthorizeForStreamThenHonors() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		Stream<Flight> flights = Stream.of(this.flight);
+		Stream<Flight> secured = proxy(factory, flights);
+		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.forEach(Flight::getAltitude));
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenPreAuthorizeForArrayThenHonors() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		Flight[] flights = { this.flight };
+		Flight[] secured = proxy(factory, flights);
+		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured[0]::getAltitude);
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenPreAuthorizeForIteratorThenHonors() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		Iterator<Flight> flights = List.of(this.flight).iterator();
+		Iterator<Flight> secured = proxy(factory, flights);
+		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.next().getAltitude());
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenPreAuthorizeForIterableThenHonors() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		Iterable<User> users = new UserRepository();
+		Iterable<User> secured = proxy(factory, users);
+		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.forEach(User::getFirstName));
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void proxyWhenPreAuthorizeForClassThenHonors() {
+		AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
+			.preAuthorize();
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
+		Class<Flight> clazz = proxy(factory, Flight.class);
+		assertThat(clazz.getSimpleName()).contains("SpringCGLIB$$0");
+		Flight secured = proxy(factory, this.flight);
+		assertThat(secured.getClass()).isSameAs(clazz);
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured::getAltitude);
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void withAdvisorsWhenProxyThenVisits() {
+		AuthorizationAdvisor advisor = mock(AuthorizationAdvisor.class);
+		given(advisor.getAdvice()).willReturn(advisor);
+		given(advisor.getPointcut()).willReturn(Pointcut.TRUE);
+		AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory();
+		factory = factory.withAdvisors(advisor);
+		Flight flight = proxy(factory, this.flight);
+		flight.getAltitude();
+		verify(advisor, atLeastOnce()).getPointcut();
+	}
+
+	private Authentication authenticated(String user, String... authorities) {
+		return TestAuthentication.authenticated(TestAuthentication.withUsername(user).authorities(authorities).build());
+	}
+
+	private <T> T proxy(AuthorizationProxyFactory factory, Object target) {
+		return (T) factory.proxy(target);
+	}
+
+	static class Flight {
+
+		@PreAuthorize("hasRole('PILOT')")
+		Double getAltitude() {
+			return 35000d;
+		}
+
+	}
+
+	interface Identifiable {
+
+		@PreAuthorize("authentication.name == this.id || hasRole('ADMIN')")
+		String getFirstName();
+
+		@PreAuthorize("authentication.name == this.id || hasRole('ADMIN')")
+		String getLastName();
+
+	}
+
+	public static class User implements Identifiable, Comparable<User> {
+
+		private final String id;
+
+		private final String firstName;
+
+		private final String lastName;
+
+		User(String id, String firstName, String lastName) {
+			this.id = id;
+			this.firstName = firstName;
+			this.lastName = lastName;
+		}
+
+		public String getId() {
+			return this.id;
+		}
+
+		@Override
+		public String getFirstName() {
+			return this.firstName;
+		}
+
+		@Override
+		public String getLastName() {
+			return this.lastName;
+		}
+
+		@Override
+		public int compareTo(@NotNull User that) {
+			return this.id.compareTo(that.getId());
+		}
+
+	}
+
+	static class UserRepository implements Iterable<User> {
+
+		List<User> users = List.of(new User("1", "first", "last"));
+
+		@NotNull
+		@Override
+		public Iterator<User> iterator() {
+			return this.users.iterator();
+		}
+
+	}
+
+	interface HasSecret {
+
+		String secret();
+
+	}
+
+	record Repository(@PreAuthorize("hasRole('ADMIN')") String secret) implements HasSecret {
+	}
+
+}

+ 391 - 0
docs/modules/ROOT/pages/servlet/authorization/method-security.adoc

@@ -1702,6 +1702,397 @@ This works on both classes and interfaces.
 This does not work for interfaces, since they do not have debug information about the parameter names.
 For interfaces, either annotations or the `-parameters` approach must be used.
 
+[[authorize-object]]
+== Authorizing Arbitrary Objects
+
+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`.
+
+For example, consider the following `User` class:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+public class User {
+	private String name;
+	private String email;
+
+	public User(String name, String email) {
+		this.name = name;
+		this.email = email;
+	}
+
+	public String getName() {
+		return this.name;
+	}
+
+    @PreAuthorize("hasAuthority('user:read')")
+    public String getEmail() {
+		return this.email;
+    }
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+class User (val name:String, @get:PreAuthorize("hasAuthority('user:read')") val email:String)
+----
+======
+
+You can proxy an instance of user in the following way:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Autowired
+AuthorizationProxyFactory proxyFactory;
+
+@Test
+void getEmailWhenProxiedThenAuthorizes() {
+    User user = new User("name", "email");
+    assertThat(user.getEmail()).isNotNull();
+    User securedUser = proxyFactory.proxy(user);
+    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail);
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Autowired
+var proxyFactory:AuthorizationProxyFactory? = null
+
+@Test
+fun getEmailWhenProxiedThenAuthorizes() {
+    val user: User = User("name", "email")
+    assertThat(user.getEmail()).isNotNull()
+    val securedUser: User = proxyFactory.proxy(user)
+    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail)
+}
+----
+======
+
+=== Manual Construction
+
+You can also define your own instance if you need something different from the Spring Security default.
+
+For example, if you define an `AuthorizationProxyFactory` instance like so:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+import static org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize;
+
+// ...
+
+AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize());
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize
+
+// ...
+
+val proxyFactory: AuthorizationProxyFactory = AuthorizationProxyFactory(preAuthorize())
+----
+======
+
+Then you can wrap any instance of `User` as follows:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Test
+void getEmailWhenProxiedThenAuthorizes() {
+	AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize());
+    User user = new User("name", "email");
+    assertThat(user.getEmail()).isNotNull();
+    User securedUser = proxyFactory.proxy(user);
+    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail);
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Test
+fun getEmailWhenProxiedThenAuthorizes() {
+    val proxyFactory: AuthorizationProxyFactory = AuthorizationProxyFactory(preAuthorize())
+    val user: User = User("name", "email")
+    assertThat(user.getEmail()).isNotNull()
+    val securedUser: User = proxyFactory.proxy(user)
+    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail)
+}
+----
+======
+
+[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.
+
+This means that when proxying a `List` of objects, the following also works:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Test
+void getEmailWhenProxiedThenAuthorizes() {
+	AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize());
+    List<User> users = List.of(ada, albert, marie);
+    List<User> securedUsers = proxyFactory.proxy(users);
+	securedUsers.forEach((securedUser) ->
+        assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail));
+}
+----
+======
+
+=== Proxying Classes
+
+In limited circumstances, it may be valuable to proxy a `Class` itself, and `AuthorizationProxyFactory` also supports this.
+This is roughly the equivalent of calling `ProxyFactory#getProxyClass` in Spring Framework's support for creating proxies.
+
+One place where this is handy is when you need to construct the proxy class ahead-of-time, like with Spring AOT.
+
+=== Support for All Method Security Annotations
+
+`AuthorizationProxyFactory` supports whichever method security annotations are enabled in your application.
+It is based off of whatever `AuthorizationAdvisor` classes are published as a bean.
+
+Since `@EnableMethodSecurity` publishes `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter`  advisors by default, you will typically need to do nothing to activate the ability.
+
+[NOTE]
+====
+SpEL expressions that use `returnObject` or `filterObject` sit behind the proxy and so have full access to the object.
+====
+
+[#custom_advice]
+=== Custom Advice
+
+If you have security advice that you also want applied, you can publish your own `AuthorizationAdvisor` like so:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@EnableMethodSecurity
+class SecurityConfig {
+    @Bean
+    static AuthorizationAdvisor myAuthorizationAdvisor() {
+        return new AuthorizationAdvisor();
+    }
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@EnableMethodSecurity
+internal class SecurityConfig {
+    @Bean
+    fun myAuthorizationAdvisor(): AuthorizationAdvisor {
+        return AuthorizationAdvisor()
+    }
+]
+----
+======
+
+And Spring Security will add that advisor into the set of advice that `AuthorizationProxyFactory` adds when proxying an object.
+
+=== Working with Jackson
+
+One powerful use of this feature is to return a secured value from a controller like so:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@RestController
+public class UserController {
+	@Autowired
+    AuthorizationProxyFactory proxyFactory;
+
+	@GetMapping
+    User currentUser(@AuthenticationPrincipal User user) {
+        return this.proxyFactory.proxy(user);
+    }
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@RestController
+class UserController  {
+    @Autowired
+    var proxyFactory: AuthorizationProxyFactory? = null
+
+    @GetMapping
+    fun currentUser(@AuthenticationPrincipal user:User?): User {
+        return proxyFactory.proxy(user)
+    }
+}
+----
+======
+
+If you are using Jackson, though, this may result in a serialization error like the following:
+
+[source,bash]
+====
+com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle
+====
+
+This is due to how Jackson works with CGLIB proxies.
+To address this, add the following annotation to the top of the `User` class:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@JsonSerialize(as = User.class)
+public class User {
+
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@JsonSerialize(`as` = User::class)
+class User
+----
+======
+
+Finally, you will need to publish a <<custom_advice, custom interceptor>> to catch the `AccessDeniedException` thrown for each field, which you can do like so:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Component
+public class AccessDeniedExceptionInterceptor implements AuthorizationAdvisor {
+    private final AuthorizationAdvisor advisor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
+
+	@Override
+	public Object invoke(MethodInvocation invocation) throws Throwable {
+		try {
+			return invocation.proceed();
+		} catch (AccessDeniedException ex) {
+			return null;
+		}
+	}
+
+	@Override
+	public Pointcut getPointcut() {
+		return this.advisor.getPointcut();
+	}
+
+	@Override
+	public Advice getAdvice() {
+		return this;
+	}
+
+	@Override
+	public int getOrder() {
+		return this.advisor.getOrder() - 1;
+	}
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Component
+class AccessDeniedExceptionInterceptor: AuthorizationAdvisor {
+    var advisor: AuthorizationAdvisor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize()
+
+    @Throws(Throwable::class)
+    fun invoke(invocation: MethodInvocation): Any? {
+        return try  {
+            invocation.proceed()
+        } catch (ex:AccessDeniedException) {
+            null
+        }
+    }
+
+     val pointcut: Pointcut
+     get() = advisor.getPointcut()
+
+     val advice: Advice
+     get() = this
+
+     val order: Int
+     get() = advisor.getOrder() - 1
+}
+----
+======
+
+Then, you'll see a different JSON serialization based on the authorization level of the user.
+If they don't have the `user:read` authority, then they'll see:
+
+[source,json]
+----
+{
+    "name" : "name",
+    "email" : null
+}
+----
+
+And if they do have that authority, they'll see:
+
+[source,json]
+----
+{
+    "name" : "name",
+    "email" : "email"
+}
+----
+
+[TIP]
+====
+You can also add the Spring Boot property `spring.jackson.default-property-inclusion=non_null` to exclude the null value, if you also don't want to reveal the JSON key to an unauthorized user.
+====
+
 [[migration-enableglobalmethodsecurity]]
 == Migrating from `@EnableGlobalMethodSecurity`
 

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

@@ -8,6 +8,10 @@ Below are the highlights of the release.
 
 - https://spring.io/blog/2024/01/19/spring-security-6-3-adds-passive-jdk-serialization-deserialization-for[blog post] - Added Passive JDK Serialization/Deserialization for Seamless Upgrades
 
+== 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
+
 == Configuration
 
 - https://github.com/spring-projects/spring-security/issues/6192[gh-6192] - xref:reactive/authentication/concurrent-sessions-control.adoc[(docs)] - Add Concurrent Sessions Control on WebFlux