浏览代码

Handle SpEL AuthorizationDeniedExceptions

Closes gh-14600
Josh Cummings 1 年之前
父节点
当前提交
50b85aea0d
共有 13 个文件被更改,包括 262 次插入26 次删除
  1. 15 1
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java
  2. 27 1
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java
  3. 17 2
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java
  4. 39 8
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java
  5. 15 0
      core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedHandler.java
  6. 18 0
      core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedPostProcessor.java
  7. 5 0
      core/src/main/java/org/springframework/security/authorization/method/ThrowingMethodAuthorizationDeniedHandler.java
  8. 6 0
      core/src/main/java/org/springframework/security/authorization/method/ThrowingMethodAuthorizationDeniedPostProcessor.java
  9. 23 0
      core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptorTests.java
  10. 39 11
      core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptorTests.java
  11. 21 0
      core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptorTests.java
  12. 27 3
      core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptorTests.java
  13. 10 0
      docs/modules/ROOT/pages/servlet/authorization/method-security.adoc

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

@@ -30,6 +30,7 @@ import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.prepost.PostAuthorize;
 import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
 import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationDeniedException;
 import org.springframework.security.authorization.AuthorizationEventPublisher;
 import org.springframework.security.authorization.AuthorizationManager;
 import org.springframework.security.core.Authentication;
@@ -172,7 +173,13 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori
 	private Object attemptAuthorization(MethodInvocation mi, Object result) {
 		this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
 		MethodInvocationResult object = new MethodInvocationResult(mi, result);
-		AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, object);
+		AuthorizationDecision decision;
+		try {
+			decision = this.authorizationManager.check(this::getAuthentication, object);
+		}
+		catch (AuthorizationDeniedException denied) {
+			return postProcess(object, denied);
+		}
 		this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, object, decision);
 		if (decision != null && !decision.isGranted()) {
 			this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
@@ -183,6 +190,13 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori
 		return result;
 	}
 
+	private Object postProcess(MethodInvocationResult mi, AuthorizationDeniedException denied) {
+		if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
+			return postProcessableDecision.postProcessResult(mi, denied);
+		}
+		return this.defaultPostProcessor.postProcessResult(mi, denied);
+	}
+
 	private Object postProcess(MethodInvocationResult mi, AuthorizationDecision decision) {
 		if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
 			return postProcessableDecision.postProcessResult(mi, decision);

+ 27 - 1
core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java

@@ -34,6 +34,7 @@ import org.springframework.core.ReactiveAdapter;
 import org.springframework.core.ReactiveAdapterRegistry;
 import org.springframework.security.access.prepost.PostAuthorize;
 import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationDeniedException;
 import org.springframework.security.authorization.ReactiveAuthorizationManager;
 import org.springframework.security.core.Authentication;
 import org.springframework.util.Assert;
@@ -151,7 +152,32 @@ public final class AuthorizationManagerAfterReactiveMethodInterceptor implements
 		MethodInvocationResult invocationResult = new MethodInvocationResult(mi, result);
 		return this.authorizationManager.check(authentication, invocationResult)
 			.switchIfEmpty(Mono.just(new AuthorizationDecision(false)))
-			.flatMap((decision) -> postProcess(decision, invocationResult));
+			.materialize()
+			.flatMap((signal) -> {
+				if (!signal.hasError()) {
+					AuthorizationDecision decision = signal.get();
+					return postProcess(decision, invocationResult);
+				}
+				if (signal.getThrowable() instanceof AuthorizationDeniedException denied) {
+					return postProcess(denied, invocationResult);
+				}
+				return Mono.error(signal.getThrowable());
+			});
+	}
+
+	private Mono<Object> postProcess(AuthorizationDeniedException denied,
+			MethodInvocationResult methodInvocationResult) {
+		return Mono.fromSupplier(() -> {
+			if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
+				return postProcessableDecision.postProcessResult(methodInvocationResult, denied);
+			}
+			return this.defaultPostProcessor.postProcessResult(methodInvocationResult, denied);
+		}).flatMap((processedResult) -> {
+			if (Mono.class.isAssignableFrom(processedResult.getClass())) {
+				return (Mono<?>) processedResult;
+			}
+			return Mono.justOrEmpty(processedResult);
+		});
 	}
 
 	private Mono<Object> postProcess(AuthorizationDecision decision, MethodInvocationResult methodInvocationResult) {

+ 17 - 2
core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java

@@ -34,8 +34,10 @@ import org.springframework.security.access.annotation.Secured;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
 import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationDeniedException;
 import org.springframework.security.authorization.AuthorizationEventPublisher;
 import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.authorization.AuthorizationResult;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextHolderStrategy;
@@ -245,7 +247,13 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author
 
 	private Object attemptAuthorization(MethodInvocation mi) throws Throwable {
 		this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
-		AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, mi);
+		AuthorizationDecision decision;
+		try {
+			decision = this.authorizationManager.check(this::getAuthentication, mi);
+		}
+		catch (AuthorizationDeniedException denied) {
+			return handle(mi, denied);
+		}
 		this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, decision);
 		if (decision != null && !decision.isGranted()) {
 			this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
@@ -256,7 +264,14 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author
 		return mi.proceed();
 	}
 
-	private Object handle(MethodInvocation mi, AuthorizationDecision decision) {
+	private Object handle(MethodInvocation mi, AuthorizationDeniedException denied) {
+		if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
+			return handler.handle(mi, denied);
+		}
+		return this.defaultHandler.handle(mi, denied);
+	}
+
+	private Object handle(MethodInvocation mi, AuthorizationResult decision) {
 		if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
 			return handler.handle(mi, decision);
 		}

+ 39 - 8
core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java

@@ -33,6 +33,7 @@ import org.springframework.core.ReactiveAdapter;
 import org.springframework.core.ReactiveAdapterRegistry;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationDeniedException;
 import org.springframework.security.authorization.ReactiveAuthorizationManager;
 import org.springframework.security.core.Authentication;
 import org.springframework.util.Assert;
@@ -140,11 +141,19 @@ public final class AuthorizationManagerBeforeReactiveMethodInterceptor implement
 		Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
 		return this.authorizationManager.check(authentication, mi)
 			.switchIfEmpty(Mono.just(new AuthorizationDecision(false)))
-			.flatMapMany((decision) -> {
-				if (decision.isGranted()) {
-					return mapping;
+			.materialize()
+			.flatMapMany((signal) -> {
+				if (!signal.hasError()) {
+					AuthorizationDecision decision = signal.get();
+					if (decision.isGranted()) {
+						return mapping;
+					}
+					return postProcess(decision, mi);
 				}
-				return postProcess(decision, mi);
+				if (signal.getThrowable() instanceof AuthorizationDeniedException denied) {
+					return postProcess(denied, mi);
+				}
+				return Mono.error(signal.getThrowable());
 			});
 	}
 
@@ -152,14 +161,36 @@ public final class AuthorizationManagerBeforeReactiveMethodInterceptor implement
 		Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
 		return this.authorizationManager.check(authentication, mi)
 			.switchIfEmpty(Mono.just(new AuthorizationDecision(false)))
-			.flatMap((decision) -> {
-				if (decision.isGranted()) {
-					return mapping;
+			.materialize()
+			.flatMap((signal) -> {
+				if (!signal.hasError()) {
+					AuthorizationDecision decision = signal.get();
+					if (decision.isGranted()) {
+						return mapping;
+					}
+					return postProcess(decision, mi);
+				}
+				if (signal.getThrowable() instanceof AuthorizationDeniedException denied) {
+					return postProcess(denied, mi);
 				}
-				return postProcess(decision, mi);
+				return Mono.error(signal.getThrowable());
 			});
 	}
 
+	private Mono<Object> postProcess(AuthorizationDeniedException denied, MethodInvocation mi) {
+		return Mono.fromSupplier(() -> {
+			if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
+				return handler.handle(mi, denied);
+			}
+			return this.defaultHandler.handle(mi, denied);
+		}).flatMap((processedResult) -> {
+			if (Mono.class.isAssignableFrom(processedResult.getClass())) {
+				return (Mono<?>) processedResult;
+			}
+			return Mono.justOrEmpty(processedResult);
+		});
+	}
+
 	private Mono<Object> postProcess(AuthorizationDecision decision, MethodInvocation mi) {
 		return Mono.fromSupplier(() -> {
 			if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {

+ 15 - 0
core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedHandler.java

@@ -19,6 +19,7 @@ package org.springframework.security.authorization.method;
 import org.aopalliance.intercept.MethodInvocation;
 
 import org.springframework.lang.Nullable;
+import org.springframework.security.authorization.AuthorizationDeniedException;
 import org.springframework.security.authorization.AuthorizationResult;
 
 /**
@@ -43,4 +44,18 @@ public interface MethodAuthorizationDeniedHandler {
 	@Nullable
 	Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult);
 
+	/**
+	 * Handle denied method invocations, implementations might either throw an
+	 * {@link org.springframework.security.access.AccessDeniedException} or a replacement
+	 * result instead of invoking the method, e.g. a masked value.
+	 * @param methodInvocation the {@link MethodInvocation} related to the authorization
+	 * denied
+	 * @param authorizationDenied the authorization denied exception
+	 * @return a replacement result for the denied method invocation, or null, or a
+	 * {@link reactor.core.publisher.Mono} for reactive applications
+	 */
+	default Object handle(MethodInvocation methodInvocation, AuthorizationDeniedException authorizationDenied) {
+		return handle(methodInvocation, authorizationDenied.getAuthorizationResult());
+	}
+
 }

+ 18 - 0
core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedPostProcessor.java

@@ -17,6 +17,7 @@
 package org.springframework.security.authorization.method;
 
 import org.springframework.lang.Nullable;
+import org.springframework.security.authorization.AuthorizationDeniedException;
 import org.springframework.security.authorization.AuthorizationResult;
 
 /**
@@ -43,4 +44,21 @@ public interface MethodAuthorizationDeniedPostProcessor {
 	@Nullable
 	Object postProcessResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult);
 
+	/**
+	 * Post-process the denied result produced by a method invocation, implementations
+	 * might either throw an
+	 * {@link org.springframework.security.access.AccessDeniedException} or return a
+	 * replacement result instead of the denied result, e.g. a masked value.
+	 * @param methodInvocationResult the object containing the method invocation and the
+	 * result produced
+	 * @param authorizationDenied the {@link AuthorizationDeniedException} containing the
+	 * authorization denied details
+	 * @return a replacement result for the denied result, or null, or a
+	 * {@link reactor.core.publisher.Mono} for reactive applications
+	 */
+	default Object postProcessResult(MethodInvocationResult methodInvocationResult,
+			AuthorizationDeniedException authorizationDenied) {
+		return postProcessResult(methodInvocationResult, authorizationDenied.getAuthorizationResult());
+	}
+
 }

+ 5 - 0
core/src/main/java/org/springframework/security/authorization/method/ThrowingMethodAuthorizationDeniedHandler.java

@@ -35,4 +35,9 @@ public final class ThrowingMethodAuthorizationDeniedHandler implements MethodAut
 		throw new AuthorizationDeniedException("Access Denied", result);
 	}
 
+	@Override
+	public Object handle(MethodInvocation methodInvocation, AuthorizationDeniedException authorizationDenied) {
+		throw authorizationDenied;
+	}
+
 }

+ 6 - 0
core/src/main/java/org/springframework/security/authorization/method/ThrowingMethodAuthorizationDeniedPostProcessor.java

@@ -33,4 +33,10 @@ public final class ThrowingMethodAuthorizationDeniedPostProcessor implements Met
 		throw new AuthorizationDeniedException("Access Denied", result);
 	}
 
+	@Override
+	public Object postProcessResult(MethodInvocationResult methodInvocationResult,
+			AuthorizationDeniedException authorizationDenied) {
+		throw authorizationDenied;
+	}
+
 }

+ 23 - 0
core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptorTests.java

@@ -26,8 +26,10 @@ import org.springframework.security.authentication.TestAuthentication;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
 import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationDeniedException;
 import org.springframework.security.authorization.AuthorizationEventPublisher;
 import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.authorization.AuthorizationResult;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.context.SecurityContext;
@@ -36,6 +38,7 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
 import org.springframework.security.core.context.SecurityContextImpl;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.BDDMockito.given;
@@ -139,4 +142,24 @@ public class AuthorizationManagerAfterMethodInterceptorTests {
 				any(AuthorizationDecision.class));
 	}
 
+	@Test
+	public void invokeWhenCustomAuthorizationDeniedExceptionThenThrows() throws Throwable {
+		MethodInvocation mi = mock(MethodInvocation.class);
+		given(mi.proceed()).willReturn("ok");
+		AuthorizationManager<MethodInvocationResult> manager = mock(AuthorizationManager.class);
+		given(manager.check(any(), any()))
+			.willThrow(new MyAuthzDeniedException("denied", new AuthorizationDecision(false)));
+		AuthorizationManagerAfterMethodInterceptor advice = new AuthorizationManagerAfterMethodInterceptor(
+				Pointcut.TRUE, manager);
+		assertThatExceptionOfType(MyAuthzDeniedException.class).isThrownBy(() -> advice.invoke(mi));
+	}
+
+	static class MyAuthzDeniedException extends AuthorizationDeniedException {
+
+		MyAuthzDeniedException(String msg, AuthorizationResult authorizationResult) {
+			super(msg, authorizationResult);
+		}
+
+	}
+
 }

+ 39 - 11
core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptorTests.java

@@ -28,6 +28,7 @@ import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.intercept.method.MockMethodInvocation;
 import org.springframework.security.authorization.AuthorizationDecision;
 import org.springframework.security.authorization.AuthorizationDeniedException;
+import org.springframework.security.authorization.AuthorizationResult;
 import org.springframework.security.authorization.ReactiveAuthorizationManager;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -126,7 +127,8 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
 		given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
 		HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
 				HandlingReactiveAuthorizationManager.class);
-		given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::masking);
+		given(mockReactiveAuthorizationManager.postProcessResult(any(), any(AuthorizationResult.class)))
+			.willAnswer(this::masking);
 		given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
 		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
 				Pointcut.TRUE, mockReactiveAuthorizationManager);
@@ -145,13 +147,14 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
 		given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
 		HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
 				HandlingReactiveAuthorizationManager.class);
-		given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer((invocation) -> {
-			MethodInvocationResult argument = invocation.getArgument(0);
-			if (!"john".equals(argument.getResult())) {
-				return monoMasking(invocation);
-			}
-			return Mono.just(argument.getResult());
-		});
+		given(mockReactiveAuthorizationManager.postProcessResult(any(), any(AuthorizationResult.class)))
+			.willAnswer((invocation) -> {
+				MethodInvocationResult argument = invocation.getArgument(0);
+				if (!"john".equals(argument.getResult())) {
+					return monoMasking(invocation);
+				}
+				return Mono.just(argument.getResult());
+			});
 		given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
 		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
 				Pointcut.TRUE, mockReactiveAuthorizationManager);
@@ -170,7 +173,8 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
 		given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
 		HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
 				HandlingReactiveAuthorizationManager.class);
-		given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::masking);
+		given(mockReactiveAuthorizationManager.postProcessResult(any(), any(AuthorizationResult.class)))
+			.willAnswer(this::masking);
 		given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
 		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
 				Pointcut.TRUE, mockReactiveAuthorizationManager);
@@ -188,7 +192,8 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
 		given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
 		HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
 				HandlingReactiveAuthorizationManager.class);
-		given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::monoMasking);
+		given(mockReactiveAuthorizationManager.postProcessResult(any(), any(AuthorizationResult.class)))
+			.willAnswer(this::monoMasking);
 		given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
 		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
 				Pointcut.TRUE, mockReactiveAuthorizationManager);
@@ -206,7 +211,8 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
 		given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
 		HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
 				HandlingReactiveAuthorizationManager.class);
-		given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willReturn(null);
+		given(mockReactiveAuthorizationManager.postProcessResult(any(), any(AuthorizationResult.class)))
+			.willReturn(null);
 		given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
 		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
 				Pointcut.TRUE, mockReactiveAuthorizationManager);
@@ -235,6 +241,20 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
 		verify(mockReactiveAuthorizationManager).check(any(), any());
 	}
 
+	@Test
+	public void invokeWhenCustomAuthorizationDeniedExceptionThenThrows() throws Throwable {
+		MethodInvocation mockMethodInvocation = spy(
+				new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
+		given(mockMethodInvocation.proceed()).willReturn(Mono.just("ok"));
+		ReactiveAuthorizationManager<MethodInvocationResult> manager = mock(ReactiveAuthorizationManager.class);
+		given(manager.check(any(), any()))
+			.willReturn(Mono.error(new MyAuthzDeniedException("denied", new AuthorizationDecision(false))));
+		AuthorizationManagerAfterReactiveMethodInterceptor advice = new AuthorizationManagerAfterReactiveMethodInterceptor(
+				Pointcut.TRUE, manager);
+		assertThatExceptionOfType(MyAuthzDeniedException.class)
+			.isThrownBy(() -> ((Mono<?>) advice.invoke(mockMethodInvocation)).block());
+	}
+
 	private Object masking(InvocationOnMock invocation) {
 		MethodInvocationResult result = invocation.getArgument(0);
 		return result.getResult() + "-masked";
@@ -262,4 +282,12 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
 
 	}
 
+	static class MyAuthzDeniedException extends AuthorizationDeniedException {
+
+		MyAuthzDeniedException(String msg, AuthorizationResult authorizationResult) {
+			super(msg, authorizationResult);
+		}
+
+	}
+
 }

+ 21 - 0
core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptorTests.java

@@ -25,8 +25,10 @@ import org.springframework.aop.Pointcut;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
 import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationDeniedException;
 import org.springframework.security.authorization.AuthorizationEventPublisher;
 import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.authorization.AuthorizationResult;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.context.SecurityContext;
@@ -34,6 +36,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextHolderStrategy;
 import org.springframework.security.core.context.SecurityContextImpl;
 
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
@@ -133,4 +136,22 @@ public class AuthorizationManagerBeforeMethodInterceptorTests {
 				any(AuthorizationDecision.class));
 	}
 
+	@Test
+	public void invokeWhenCustomAuthorizationDeniedExceptionThenThrows() {
+		AuthorizationManager<MethodInvocation> manager = mock(AuthorizationManager.class);
+		given(manager.check(any(), any()))
+			.willThrow(new MyAuthzDeniedException("denied", new AuthorizationDecision(false)));
+		AuthorizationManagerBeforeMethodInterceptor advice = new AuthorizationManagerBeforeMethodInterceptor(
+				Pointcut.TRUE, manager);
+		assertThatExceptionOfType(MyAuthzDeniedException.class).isThrownBy(() -> advice.invoke(null));
+	}
+
+	static class MyAuthzDeniedException extends AuthorizationDeniedException {
+
+		MyAuthzDeniedException(String msg, AuthorizationResult authorizationResult) {
+			super(msg, authorizationResult);
+		}
+
+	}
+
 }

+ 27 - 3
core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptorTests.java

@@ -27,6 +27,7 @@ import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.intercept.method.MockMethodInvocation;
 import org.springframework.security.authorization.AuthorizationDecision;
 import org.springframework.security.authorization.AuthorizationDeniedException;
+import org.springframework.security.authorization.AuthorizationResult;
 import org.springframework.security.authorization.ReactiveAuthorizationManager;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -126,7 +127,7 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
 		HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
 				HandlingReactiveAuthorizationManager.class);
 		given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.empty());
-		given(mockReactiveAuthorizationManager.handle(any(), any())).willReturn("***");
+		given(mockReactiveAuthorizationManager.handle(any(), any(AuthorizationResult.class))).willReturn("***");
 		AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
 				Pointcut.TRUE, mockReactiveAuthorizationManager);
 		Object result = interceptor.invoke(mockMethodInvocation);
@@ -144,7 +145,8 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
 		HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
 				HandlingReactiveAuthorizationManager.class);
 		given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.empty());
-		given(mockReactiveAuthorizationManager.handle(any(), any())).willReturn(Mono.just("***"));
+		given(mockReactiveAuthorizationManager.handle(any(), any(AuthorizationResult.class)))
+			.willReturn(Mono.just("***"));
 		AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
 				Pointcut.TRUE, mockReactiveAuthorizationManager);
 		Object result = interceptor.invoke(mockMethodInvocation);
@@ -162,7 +164,8 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
 		HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
 				HandlingReactiveAuthorizationManager.class);
 		given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.empty());
-		given(mockReactiveAuthorizationManager.handle(any(), any())).willReturn(Mono.just("***"));
+		given(mockReactiveAuthorizationManager.handle(any(), any(AuthorizationResult.class)))
+			.willReturn(Mono.just("***"));
 		AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
 				Pointcut.TRUE, mockReactiveAuthorizationManager);
 		Object result = interceptor.invoke(mockMethodInvocation);
@@ -209,6 +212,19 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
 		verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation));
 	}
 
+	@Test
+	public void invokeWhenCustomAuthorizationDeniedExceptionThenThrows() throws Throwable {
+		MethodInvocation mockMethodInvocation = spy(
+				new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
+		ReactiveAuthorizationManager<MethodInvocation> manager = mock(ReactiveAuthorizationManager.class);
+		given(manager.check(any(), any()))
+			.willThrow(new MyAuthzDeniedException("denied", new AuthorizationDecision(false)));
+		AuthorizationManagerBeforeReactiveMethodInterceptor advice = new AuthorizationManagerBeforeReactiveMethodInterceptor(
+				Pointcut.TRUE, manager);
+		assertThatExceptionOfType(MyAuthzDeniedException.class)
+			.isThrownBy(() -> ((Mono<?>) advice.invoke(mockMethodInvocation)).block());
+	}
+
 	interface HandlingReactiveAuthorizationManager
 			extends ReactiveAuthorizationManager<MethodInvocation>, MethodAuthorizationDeniedHandler {
 
@@ -226,4 +242,12 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
 
 	}
 
+	static class MyAuthzDeniedException extends AuthorizationDeniedException {
+
+		MyAuthzDeniedException(String msg, AuthorizationResult authorizationResult) {
+			super(msg, authorizationResult);
+		}
+
+	}
+
 }

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

@@ -1249,6 +1249,9 @@ open class AuthorizationLogic {
 ----
 ======
 
+Or throw a custom `AuthorizationDeniedException` instance.
+Note, though, that returning an object is preferred as this doesn't incur the expense of generating a stacktrace.
+
 Then, you can access the custom details when you <<fallback-values-authorization-denied, customize how the authorization result is handled>>.
 
 [[custom-authorization-managers]]
@@ -1654,6 +1657,13 @@ Xml::
 <4> This method may only be invoked by ``Princpal``s with an `aud` claim equal to "my-audience"
 <5> This method may only be invoked if the bean ``authz``'s `check` method returns `true`
 
+[NOTE]
+====
+You can use a bean like `authz` above to add programmatic authorization.
+It can return a `boolean`, and `AuthorizationResult`, or throw an `AuthorizationDeniedException`.
+For exceptions, you can <<fallback-values-authorization-denied, handle them at the method level>>.
+====
+
 [[using_method_parameters]]
 === Using Method Parameters