Selaa lähdekoodia

Merge remote-tracking branch 'origin/5.8.x' into main

Josh Cummings 3 vuotta sitten
vanhempi
commit
84f765a89c
38 muutettua tiedostoa jossa 3394 lisäystä ja 191 poistoa
  1. 8 0
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurity.java
  2. 87 0
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java
  3. 35 17
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java
  4. 8 1
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java
  5. 22 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/DelegatingReactiveMessageService.java
  6. 484 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableAuthorizationManagerReactiveMethodSecurityTests.java
  7. 6 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMessageService.java
  8. 5 0
      core/src/main/java/org/springframework/security/access/prepost/PrePostAdviceReactiveMethodInterceptor.java
  9. 153 0
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java
  10. 149 0
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java
  11. 10 1
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationMethodPointcuts.java
  12. 5 39
      core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java
  13. 72 0
      core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java
  14. 78 0
      core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java
  15. 5 37
      core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java
  16. 153 0
      core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java
  17. 71 0
      core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java
  18. 3 36
      core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java
  19. 76 0
      core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java
  20. 73 0
      core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java
  21. 8 52
      core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java
  22. 213 0
      core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java
  23. 89 0
      core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java
  24. 46 0
      core/src/main/java/org/springframework/security/authorization/method/ReactiveAuthenticationUtils.java
  25. 67 0
      core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java
  26. 42 0
      core/src/main/java/org/springframework/security/authorization/method/ReactiveMethodInvocationUtils.java
  27. 6 1
      core/src/test/java/org/springframework/security/access/intercept/method/MockMethodInvocation.java
  28. 124 0
      core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptorTests.java
  29. 125 0
      core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptorTests.java
  30. 2 2
      core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java
  31. 246 0
      core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManagerTests.java
  32. 1 1
      core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java
  33. 191 0
      core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptorTests.java
  34. 2 2
      core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java
  35. 210 0
      core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManagerTests.java
  36. 1 1
      core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java
  37. 236 0
      core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptorTests.java
  38. 282 1
      docs/modules/ROOT/pages/reactive/authorization/method.adoc

+ 8 - 0
config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurity.java

@@ -25,6 +25,7 @@ import java.lang.annotation.Target;
 import org.springframework.context.annotation.AdviceMode;
 import org.springframework.context.annotation.Import;
 import org.springframework.core.Ordered;
+import org.springframework.security.authorization.ReactiveAuthorizationManager;
 
 /**
  *
@@ -67,4 +68,11 @@ public @interface EnableReactiveMethodSecurity {
 	 */
 	int order() default Ordered.LOWEST_PRECEDENCE;
 
+	/**
+	 * Indicate whether {@link ReactiveAuthorizationManager} based Method Security to be
+	 * used.
+	 * @since 5.8
+	 */
+	boolean useAuthorizationManager() default false;
+
 }

+ 87 - 0
config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java

@@ -0,0 +1,87 @@
+/*
+ * Copyright 2002-2022 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.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Role;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor;
+import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor;
+import org.springframework.security.authorization.method.PostAuthorizeReactiveAuthorizationManager;
+import org.springframework.security.authorization.method.PostFilterAuthorizationReactiveMethodInterceptor;
+import org.springframework.security.authorization.method.PreAuthorizeReactiveAuthorizationManager;
+import org.springframework.security.authorization.method.PreFilterAuthorizationReactiveMethodInterceptor;
+import org.springframework.security.config.core.GrantedAuthorityDefaults;
+
+/**
+ * Configuration for a {@link ReactiveAuthenticationManager} based Method Security.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.8
+ */
+@Configuration(proxyBeanMethods = false)
+final class ReactiveAuthorizationManagerMethodSecurityConfiguration {
+
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor(
+			MethodSecurityExpressionHandler expressionHandler) {
+		return new PreFilterAuthorizationReactiveMethodInterceptor(expressionHandler);
+	}
+
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor(
+			MethodSecurityExpressionHandler expressionHandler) {
+		PreAuthorizeReactiveAuthorizationManager authorizationManager = new PreAuthorizeReactiveAuthorizationManager(
+				expressionHandler);
+		return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(authorizationManager);
+	}
+
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor(
+			MethodSecurityExpressionHandler expressionHandler) {
+		return new PostFilterAuthorizationReactiveMethodInterceptor(expressionHandler);
+	}
+
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor(
+			MethodSecurityExpressionHandler expressionHandler) {
+		PostAuthorizeReactiveAuthorizationManager authorizationManager = new PostAuthorizeReactiveAuthorizationManager(
+				expressionHandler);
+		return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(authorizationManager);
+	}
+
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler(
+			@Autowired(required = false) GrantedAuthorityDefaults grantedAuthorityDefaults) {
+		DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
+		if (grantedAuthorityDefaults != null) {
+			handler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix());
+		}
+		return handler;
+	}
+
+}

+ 35 - 17
config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2017 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -17,37 +17,55 @@
 package org.springframework.security.config.annotation.method.configuration;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 import org.springframework.context.annotation.AdviceMode;
 import org.springframework.context.annotation.AdviceModeImportSelector;
 import org.springframework.context.annotation.AutoProxyRegistrar;
+import org.springframework.context.annotation.ImportSelector;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.lang.NonNull;
 
 /**
  * @author Rob Winch
+ * @author Evgeniy Cheban
  * @since 5.0
  */
-class ReactiveMethodSecuritySelector extends AdviceModeImportSelector<EnableReactiveMethodSecurity> {
+class ReactiveMethodSecuritySelector implements ImportSelector {
+
+	private final ImportSelector autoProxy = new AutoProxyRegistrarSelector();
 
 	@Override
-	protected String[] selectImports(AdviceMode adviceMode) {
-		if (adviceMode == AdviceMode.PROXY) {
-			return getProxyImports();
+	public String[] selectImports(AnnotationMetadata importMetadata) {
+		if (!importMetadata.hasAnnotation(EnableReactiveMethodSecurity.class.getName())) {
+			return new String[0];
+		}
+		EnableReactiveMethodSecurity annotation = importMetadata.getAnnotations()
+				.get(EnableReactiveMethodSecurity.class).synthesize();
+		List<String> imports = new ArrayList<>(Arrays.asList(this.autoProxy.selectImports(importMetadata)));
+		if (annotation.useAuthorizationManager()) {
+			imports.add(ReactiveAuthorizationManagerMethodSecurityConfiguration.class.getName());
 		}
-		throw new IllegalStateException("AdviceMode " + adviceMode + " is not supported");
+		else {
+			imports.add(ReactiveMethodSecurityConfiguration.class.getName());
+		}
+		return imports.toArray(new String[0]);
 	}
 
-	/**
-	 * Return the imports to use if the {@link AdviceMode} is set to
-	 * {@link AdviceMode#PROXY}.
-	 * <p>
-	 * Take care of adding the necessary JSR-107 import if it is available.
-	 */
-	private String[] getProxyImports() {
-		List<String> result = new ArrayList<>();
-		result.add(AutoProxyRegistrar.class.getName());
-		result.add(ReactiveMethodSecurityConfiguration.class.getName());
-		return result.toArray(new String[0]);
+	private static final class AutoProxyRegistrarSelector
+			extends AdviceModeImportSelector<EnableReactiveMethodSecurity> {
+
+		private static final String[] IMPORTS = new String[] { AutoProxyRegistrar.class.getName() };
+
+		@Override
+		protected String[] selectImports(@NonNull AdviceMode adviceMode) {
+			if (adviceMode == AdviceMode.PROXY) {
+				return IMPORTS;
+			}
+			throw new IllegalStateException("AdviceMode " + adviceMode + " is not supported");
+		}
+
 	}
 
 }

+ 8 - 1
config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2017 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -16,11 +16,14 @@
 
 package org.springframework.security.config.annotation.method.configuration;
 
+import reactor.core.publisher.Mono;
+
 import org.springframework.security.core.Authentication;
 import org.springframework.stereotype.Component;
 
 /**
  * @author Rob Winch
+ * @author Evgeniy Cheban
  * @since 5.0
  */
 @Component
@@ -34,6 +37,10 @@ public class Authz {
 		return id % 2 == 0;
 	}
 
+	public Mono<Boolean> checkReactive(long id) {
+		return Mono.defer(() -> Mono.just(id % 2 == 0));
+	}
+
 	public boolean check(Authentication authentication, String message) {
 		return message != null && message.contains(authentication.getName());
 	}

+ 22 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/DelegatingReactiveMessageService.java

@@ -21,7 +21,9 @@ import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 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;
 
 public class DelegatingReactiveMessageService implements ReactiveMessageService {
 
@@ -60,6 +62,12 @@ public class DelegatingReactiveMessageService implements ReactiveMessageService
 		return this.delegate.monoPreAuthorizeBeanFindById(id);
 	}
 
+	@Override
+	@PreAuthorize("@authz.checkReactive(#id)")
+	public Mono<String> monoPreAuthorizeBeanFindByIdReactiveExpression(long id) {
+		return this.delegate.monoPreAuthorizeBeanFindByIdReactiveExpression(id);
+	}
+
 	@Override
 	@PostAuthorize("@authz.check(authentication, returnObject)")
 	public Mono<String> monoPostAuthorizeBeanFindById(long id) {
@@ -95,6 +103,20 @@ public class DelegatingReactiveMessageService implements ReactiveMessageService
 		return this.delegate.fluxPostAuthorizeBeanFindById(id);
 	}
 
+	@PreFilter("filterObject.length > 3")
+	@PreAuthorize("hasRole('ADMIN')")
+	@PostFilter("filterObject.length > 5")
+	@PostAuthorize("returnObject == 'harold' or returnObject == 'jonathan'")
+	@Override
+	public Flux<String> fluxManyAnnotations(Flux<String> flux) {
+		return flux;
+	}
+
+	@PostFilter("filterObject.length > 5")
+	public Flux<String> fluxPostFilter(Flux<String> flux) {
+		return flux;
+	}
+
 	@Override
 	public Publisher<String> publisherFindById(long id) {
 		return this.delegate.publisherFindById(id);

+ 484 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableAuthorizationManagerReactiveMethodSecurityTests.java

@@ -0,0 +1,484 @@
+/*
+ * Copyright 2002-2022 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.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+import reactor.test.publisher.TestPublisher;
+import reactor.util.context.Context;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.context.ReactiveSecurityContextHolder;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+
+/**
+ * Tests for {@link EnableReactiveMethodSecurity} with the
+ * {@link EnableReactiveMethodSecurity#useAuthorizationManager()} flag set to true.
+ *
+ * @author Evgeniy Cheban
+ */
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration
+public class EnableAuthorizationManagerReactiveMethodSecurityTests {
+
+	@Autowired
+	ReactiveMessageService messageService;
+
+	ReactiveMessageService delegate;
+
+	TestPublisher<String> result = TestPublisher.create();
+
+	Context withAdmin = ReactiveSecurityContextHolder
+			.withAuthentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN"));
+
+	Context withUser = ReactiveSecurityContextHolder
+			.withAuthentication(new TestingAuthenticationToken("user", "password", "ROLE_USER"));
+
+	@AfterEach
+	public void cleanup() {
+		reset(this.delegate);
+	}
+
+	@Autowired
+	public void setConfig(Config config) {
+		this.delegate = config.delegate;
+	}
+
+	@Test
+	public void notPublisherPreAuthorizeFindByIdThenThrowsIllegalStateException() {
+		assertThatIllegalStateException().isThrownBy(() -> this.messageService.notPublisherPreAuthorizeFindById(1L))
+				.withMessage("The returnType class java.lang.String on public abstract java.lang.String "
+						+ "org.springframework.security.config.annotation.method.configuration.ReactiveMessageService"
+						+ ".notPublisherPreAuthorizeFindById(long) must return an instance of org.reactivestreams"
+						+ ".Publisher (for example, a Mono or Flux) in order to support Reactor Context");
+	}
+
+	@Test
+	public void monoWhenPermitAllThenAopDoesNotSubscribe() {
+		given(this.delegate.monoFindById(1L)).willReturn(Mono.from(this.result));
+		this.delegate.monoFindById(1L);
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void monoWhenPermitAllThenSuccess() {
+		given(this.delegate.monoFindById(1L)).willReturn(Mono.just("success"));
+		StepVerifier.create(this.delegate.monoFindById(1L)).expectNext("success").verifyComplete();
+	}
+
+	@Test
+	public void monoPreAuthorizeHasRoleWhenGrantedThenSuccess() {
+		given(this.delegate.monoPreAuthorizeHasRoleFindById(1L)).willReturn(Mono.just("result"));
+		Mono<String> findById = this.messageService.monoPreAuthorizeHasRoleFindById(1L).contextWrite(this.withAdmin);
+		StepVerifier.create(findById).expectNext("result").verifyComplete();
+	}
+
+	@Test
+	public void monoPreAuthorizeHasRoleWhenNoAuthenticationThenDenied() {
+		given(this.delegate.monoPreAuthorizeHasRoleFindById(1L)).willReturn(Mono.from(this.result));
+		Mono<String> findById = this.messageService.monoPreAuthorizeHasRoleFindById(1L);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void monoPreAuthorizeHasRoleWhenNotAuthorizedThenDenied() {
+		given(this.delegate.monoPreAuthorizeHasRoleFindById(1L)).willReturn(Mono.from(this.result));
+		Mono<String> findById = this.messageService.monoPreAuthorizeHasRoleFindById(1L).contextWrite(this.withUser);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void monoPreAuthorizeBeanWhenGrantedThenSuccess() {
+		given(this.delegate.monoPreAuthorizeBeanFindById(2L)).willReturn(Mono.just("result"));
+		Mono<String> findById = this.messageService.monoPreAuthorizeBeanFindById(2L).contextWrite(this.withAdmin);
+		StepVerifier.create(findById).expectNext("result").verifyComplete();
+	}
+
+	@Test
+	public void monoPreAuthorizeBeanWhenNotAuthenticatedAndGrantedThenSuccess() {
+		given(this.delegate.monoPreAuthorizeBeanFindById(2L)).willReturn(Mono.just("result"));
+		Mono<String> findById = this.messageService.monoPreAuthorizeBeanFindById(2L);
+		StepVerifier.create(findById).expectNext("result").verifyComplete();
+	}
+
+	@Test
+	public void monoPreAuthorizeBeanWhenNoAuthenticationThenDenied() {
+		given(this.delegate.monoPreAuthorizeBeanFindById(1L)).willReturn(Mono.from(this.result));
+		Mono<String> findById = this.messageService.monoPreAuthorizeBeanFindById(1L);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void monoPreAuthorizeBeanWhenNotAuthorizedThenDenied() {
+		given(this.delegate.monoPreAuthorizeBeanFindById(1L)).willReturn(Mono.from(this.result));
+		Mono<String> findById = this.messageService.monoPreAuthorizeBeanFindById(1L).contextWrite(this.withUser);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void monoPreAuthorizeBeanReactiveExpressionWhenGrantedThenSuccess() {
+		given(this.delegate.monoPreAuthorizeBeanFindByIdReactiveExpression(2L)).willReturn(Mono.just("result"));
+		Mono<String> findById = this.messageService.monoPreAuthorizeBeanFindByIdReactiveExpression(2L)
+				.contextWrite(this.withAdmin);
+		StepVerifier.create(findById).expectNext("result").verifyComplete();
+	}
+
+	@Test
+	public void monoPreAuthorizeBeanReactiveExpressionWhenNotAuthenticatedAndGrantedThenSuccess() {
+		given(this.delegate.monoPreAuthorizeBeanFindByIdReactiveExpression(2L)).willReturn(Mono.just("result"));
+		Mono<String> findById = this.messageService.monoPreAuthorizeBeanFindByIdReactiveExpression(2L);
+		StepVerifier.create(findById).expectNext("result").verifyComplete();
+	}
+
+	@Test
+	public void monoPreAuthorizeBeanReactiveExpressionWhenNoAuthenticationThenDenied() {
+		given(this.delegate.monoPreAuthorizeBeanFindByIdReactiveExpression(1L)).willReturn(Mono.from(this.result));
+		Mono<String> findById = this.messageService.monoPreAuthorizeBeanFindByIdReactiveExpression(1L);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void monoPreAuthorizeBeanReactiveExpressionWhenNotAuthorizedThenDenied() {
+		given(this.delegate.monoPreAuthorizeBeanFindByIdReactiveExpression(1L)).willReturn(Mono.from(this.result));
+		Mono<String> findById = this.messageService.monoPreAuthorizeBeanFindByIdReactiveExpression(1L)
+				.contextWrite(this.withUser);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void monoPostAuthorizeWhenAuthorizedThenSuccess() {
+		given(this.delegate.monoPostAuthorizeFindById(1L)).willReturn(Mono.just("user"));
+		Mono<String> findById = this.messageService.monoPostAuthorizeFindById(1L).contextWrite(this.withUser);
+		StepVerifier.create(findById).expectNext("user").verifyComplete();
+	}
+
+	@Test
+	public void monoPostAuthorizeWhenNotAuthorizedThenDenied() {
+		given(this.delegate.monoPostAuthorizeBeanFindById(1L)).willReturn(Mono.just("not-authorized"));
+		Mono<String> findById = this.messageService.monoPostAuthorizeBeanFindById(1L).contextWrite(this.withUser);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+	}
+
+	@Test
+	public void monoPostAuthorizeWhenBeanAndAuthorizedThenSuccess() {
+		given(this.delegate.monoPostAuthorizeBeanFindById(2L)).willReturn(Mono.just("user"));
+		Mono<String> findById = this.messageService.monoPostAuthorizeBeanFindById(2L).contextWrite(this.withUser);
+		StepVerifier.create(findById).expectNext("user").verifyComplete();
+	}
+
+	@Test
+	public void monoPostAuthorizeWhenBeanAndNotAuthenticatedAndAuthorizedThenSuccess() {
+		given(this.delegate.monoPostAuthorizeBeanFindById(2L)).willReturn(Mono.just("anonymous"));
+		Mono<String> findById = this.messageService.monoPostAuthorizeBeanFindById(2L);
+		StepVerifier.create(findById).expectNext("anonymous").verifyComplete();
+	}
+
+	@Test
+	public void monoPostAuthorizeWhenBeanAndNotAuthorizedThenDenied() {
+		given(this.delegate.monoPostAuthorizeBeanFindById(1L)).willReturn(Mono.just("not-authorized"));
+		Mono<String> findById = this.messageService.monoPostAuthorizeBeanFindById(1L).contextWrite(this.withUser);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+	}
+
+	// Flux tests
+	@Test
+	public void fluxWhenPermitAllThenAopDoesNotSubscribe() {
+		given(this.delegate.fluxFindById(1L)).willReturn(Flux.from(this.result));
+		this.delegate.fluxFindById(1L);
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void fluxWhenPermitAllThenSuccess() {
+		given(this.delegate.fluxFindById(1L)).willReturn(Flux.just("success"));
+		StepVerifier.create(this.delegate.fluxFindById(1L)).expectNext("success").verifyComplete();
+	}
+
+	@Test
+	public void fluxPreAuthorizeHasRoleWhenGrantedThenSuccess() {
+		given(this.delegate.fluxPreAuthorizeHasRoleFindById(1L)).willReturn(Flux.just("result"));
+		Flux<String> findById = this.messageService.fluxPreAuthorizeHasRoleFindById(1L).contextWrite(this.withAdmin);
+		StepVerifier.create(findById).consumeNextWith((s) -> assertThat(s).isEqualTo("result")).verifyComplete();
+	}
+
+	@Test
+	public void fluxPreAuthorizeHasRoleWhenNoAuthenticationThenDenied() {
+		given(this.delegate.fluxPreAuthorizeHasRoleFindById(1L)).willReturn(Flux.from(this.result));
+		Flux<String> findById = this.messageService.fluxPreAuthorizeHasRoleFindById(1L);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void fluxPreAuthorizeHasRoleWhenNotAuthorizedThenDenied() {
+		given(this.delegate.fluxPreAuthorizeHasRoleFindById(1L)).willReturn(Flux.from(this.result));
+		Flux<String> findById = this.messageService.fluxPreAuthorizeHasRoleFindById(1L).contextWrite(this.withUser);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void fluxPreAuthorizeBeanWhenGrantedThenSuccess() {
+		given(this.delegate.fluxPreAuthorizeBeanFindById(2L)).willReturn(Flux.just("result"));
+		Flux<String> findById = this.messageService.fluxPreAuthorizeBeanFindById(2L).contextWrite(this.withAdmin);
+		StepVerifier.create(findById).expectNext("result").verifyComplete();
+	}
+
+	@Test
+	public void fluxPreAuthorizeBeanWhenNotAuthenticatedAndGrantedThenSuccess() {
+		given(this.delegate.fluxPreAuthorizeBeanFindById(2L)).willReturn(Flux.just("result"));
+		Flux<String> findById = this.messageService.fluxPreAuthorizeBeanFindById(2L);
+		StepVerifier.create(findById).expectNext("result").verifyComplete();
+	}
+
+	@Test
+	public void fluxPreAuthorizeBeanWhenNoAuthenticationThenDenied() {
+		given(this.delegate.fluxPreAuthorizeBeanFindById(1L)).willReturn(Flux.from(this.result));
+		Flux<String> findById = this.messageService.fluxPreAuthorizeBeanFindById(1L);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void fluxPreAuthorizeBeanWhenNotAuthorizedThenDenied() {
+		given(this.delegate.fluxPreAuthorizeBeanFindById(1L)).willReturn(Flux.from(this.result));
+		Flux<String> findById = this.messageService.fluxPreAuthorizeBeanFindById(1L).contextWrite(this.withUser);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void fluxPostAuthorizeWhenAuthorizedThenSuccess() {
+		given(this.delegate.fluxPostAuthorizeFindById(1L)).willReturn(Flux.just("user"));
+		Flux<String> findById = this.messageService.fluxPostAuthorizeFindById(1L).contextWrite(this.withUser);
+		StepVerifier.create(findById).expectNext("user").verifyComplete();
+	}
+
+	@Test
+	public void fluxPostAuthorizeWhenNotAuthorizedThenDenied() {
+		given(this.delegate.fluxPostAuthorizeBeanFindById(1L)).willReturn(Flux.just("not-authorized"));
+		Flux<String> findById = this.messageService.fluxPostAuthorizeBeanFindById(1L).contextWrite(this.withUser);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+	}
+
+	@Test
+	public void fluxPostAuthorizeWhenBeanAndAuthorizedThenSuccess() {
+		given(this.delegate.fluxPostAuthorizeBeanFindById(2L)).willReturn(Flux.just("user"));
+		Flux<String> findById = this.messageService.fluxPostAuthorizeBeanFindById(2L).contextWrite(this.withUser);
+		StepVerifier.create(findById).expectNext("user").verifyComplete();
+	}
+
+	@Test
+	public void fluxPostAuthorizeWhenBeanAndNotAuthenticatedAndAuthorizedThenSuccess() {
+		given(this.delegate.fluxPostAuthorizeBeanFindById(2L)).willReturn(Flux.just("anonymous"));
+		Flux<String> findById = this.messageService.fluxPostAuthorizeBeanFindById(2L);
+		StepVerifier.create(findById).expectNext("anonymous").verifyComplete();
+	}
+
+	@Test
+	public void fluxPostAuthorizeWhenBeanAndNotAuthorizedThenDenied() {
+		given(this.delegate.fluxPostAuthorizeBeanFindById(1L)).willReturn(Flux.just("not-authorized"));
+		Flux<String> findById = this.messageService.fluxPostAuthorizeBeanFindById(1L).contextWrite(this.withUser);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+	}
+
+	@Test
+	public void fluxManyAnnotationsWhenMeetsConditionsThenReturnsFilteredFlux() {
+		Flux<String> flux = this.messageService.fluxManyAnnotations(Flux.just("harold", "jonathan", "pete", "bo"))
+				.contextWrite(this.withAdmin);
+		StepVerifier.create(flux).expectNext("harold", "jonathan").verifyComplete();
+	}
+
+	@Test
+	public void fluxManyAnnotationsWhenUserThenFails() {
+		Flux<String> flux = this.messageService.fluxManyAnnotations(Flux.just("harold", "jonathan", "pete", "bo"))
+				.contextWrite(this.withUser);
+		StepVerifier.create(flux).expectError(AccessDeniedException.class).verify();
+	}
+
+	@Test
+	public void fluxManyAnnotationsWhenNameNotAllowedThenFails() {
+		Flux<String> flux = this.messageService
+				.fluxManyAnnotations(Flux.just("harold", "jonathan", "michael", "pete", "bo"))
+				.contextWrite(this.withAdmin);
+		StepVerifier.create(flux).expectNext("harold", "jonathan").expectError(AccessDeniedException.class).verify();
+	}
+
+	@Test
+	public void fluxPostFilterWhenFilteringThenWorks() {
+		Flux<String> flux = this.messageService.fluxPostFilter(Flux.just("harold", "jonathan", "michael", "pete", "bo"))
+				.contextWrite(this.withAdmin);
+		StepVerifier.create(flux).expectNext("harold", "jonathan", "michael").verifyComplete();
+	}
+
+	// Publisher tests
+	@Test
+	public void publisherWhenPermitAllThenAopDoesNotSubscribe() {
+		given(this.delegate.publisherFindById(1L)).willReturn(this.result);
+		this.delegate.publisherFindById(1L);
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void publisherWhenPermitAllThenSuccess() {
+		given(this.delegate.publisherFindById(1L)).willReturn(publisherJust("success"));
+		StepVerifier.create(this.delegate.publisherFindById(1L)).expectNext("success").verifyComplete();
+	}
+
+	@Test
+	public void publisherPreAuthorizeHasRoleWhenGrantedThenSuccess() {
+		given(this.delegate.publisherPreAuthorizeHasRoleFindById(1L)).willReturn(publisherJust("result"));
+		Publisher<String> findById = Flux.from(this.messageService.publisherPreAuthorizeHasRoleFindById(1L))
+				.contextWrite(this.withAdmin);
+		StepVerifier.create(findById).consumeNextWith((s) -> assertThat(s).isEqualTo("result")).verifyComplete();
+	}
+
+	@Test
+	public void publisherPreAuthorizeHasRoleWhenNoAuthenticationThenDenied() {
+		given(this.delegate.publisherPreAuthorizeHasRoleFindById(1L)).willReturn(this.result);
+		Publisher<String> findById = this.messageService.publisherPreAuthorizeHasRoleFindById(1L);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void publisherPreAuthorizeHasRoleWhenNotAuthorizedThenDenied() {
+		given(this.delegate.publisherPreAuthorizeHasRoleFindById(1L)).willReturn(this.result);
+		Publisher<String> findById = Flux.from(this.messageService.publisherPreAuthorizeHasRoleFindById(1L))
+				.contextWrite(this.withUser);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void publisherPreAuthorizeBeanWhenGrantedThenSuccess() {
+		given(this.delegate.publisherPreAuthorizeBeanFindById(2L)).willReturn(publisherJust("result"));
+		Publisher<String> findById = Flux.from(this.messageService.publisherPreAuthorizeBeanFindById(2L))
+				.contextWrite(this.withAdmin);
+		StepVerifier.create(findById).expectNext("result").verifyComplete();
+	}
+
+	@Test
+	public void publisherPreAuthorizeBeanWhenNotAuthenticatedAndGrantedThenSuccess() {
+		given(this.delegate.publisherPreAuthorizeBeanFindById(2L)).willReturn(publisherJust("result"));
+		Publisher<String> findById = this.messageService.publisherPreAuthorizeBeanFindById(2L);
+		StepVerifier.create(findById).expectNext("result").verifyComplete();
+	}
+
+	@Test
+	public void publisherPreAuthorizeBeanWhenNoAuthenticationThenDenied() {
+		given(this.delegate.publisherPreAuthorizeBeanFindById(1L)).willReturn(this.result);
+		Publisher<String> findById = this.messageService.publisherPreAuthorizeBeanFindById(1L);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void publisherPreAuthorizeBeanWhenNotAuthorizedThenDenied() {
+		given(this.delegate.publisherPreAuthorizeBeanFindById(1L)).willReturn(this.result);
+		Publisher<String> findById = Flux.from(this.messageService.publisherPreAuthorizeBeanFindById(1L))
+				.contextWrite(this.withUser);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+		this.result.assertNoSubscribers();
+	}
+
+	@Test
+	public void publisherPostAuthorizeWhenAuthorizedThenSuccess() {
+		given(this.delegate.publisherPostAuthorizeFindById(1L)).willReturn(publisherJust("user"));
+		Publisher<String> findById = Flux.from(this.messageService.publisherPostAuthorizeFindById(1L))
+				.contextWrite(this.withUser);
+		StepVerifier.create(findById).expectNext("user").verifyComplete();
+	}
+
+	@Test
+	public void publisherPostAuthorizeWhenNotAuthorizedThenDenied() {
+		given(this.delegate.publisherPostAuthorizeBeanFindById(1L)).willReturn(publisherJust("not-authorized"));
+		Publisher<String> findById = Flux.from(this.messageService.publisherPostAuthorizeBeanFindById(1L))
+				.contextWrite(this.withUser);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+	}
+
+	@Test
+	public void publisherPostAuthorizeWhenBeanAndAuthorizedThenSuccess() {
+		given(this.delegate.publisherPostAuthorizeBeanFindById(2L)).willReturn(publisherJust("user"));
+		Publisher<String> findById = Flux.from(this.messageService.publisherPostAuthorizeBeanFindById(2L))
+				.contextWrite(this.withUser);
+		StepVerifier.create(findById).expectNext("user").verifyComplete();
+	}
+
+	@Test
+	public void publisherPostAuthorizeWhenBeanAndNotAuthenticatedAndAuthorizedThenSuccess() {
+		given(this.delegate.publisherPostAuthorizeBeanFindById(2L)).willReturn(publisherJust("anonymous"));
+		Publisher<String> findById = this.messageService.publisherPostAuthorizeBeanFindById(2L);
+		StepVerifier.create(findById).expectNext("anonymous").verifyComplete();
+	}
+
+	@Test
+	public void publisherPostAuthorizeWhenBeanAndNotAuthorizedThenDenied() {
+		given(this.delegate.publisherPostAuthorizeBeanFindById(1L)).willReturn(publisherJust("not-authorized"));
+		Publisher<String> findById = Flux.from(this.messageService.publisherPostAuthorizeBeanFindById(1L))
+				.contextWrite(this.withUser);
+		StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
+	}
+
+	static <T> Publisher<T> publisher(Flux<T> flux) {
+		return (subscriber) -> flux.subscribe(subscriber);
+	}
+
+	static <T> Publisher<T> publisherJust(T... data) {
+		return publisher(Flux.just(data));
+	}
+
+	@EnableReactiveMethodSecurity(useAuthorizationManager = true)
+	static class Config {
+
+		ReactiveMessageService delegate = mock(ReactiveMessageService.class);
+
+		@Bean
+		DelegatingReactiveMessageService defaultMessageService() {
+			return new DelegatingReactiveMessageService(this.delegate);
+		}
+
+		@Bean
+		Authz authz() {
+			return new Authz();
+		}
+
+	}
+
+}

+ 6 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMessageService.java

@@ -32,6 +32,8 @@ public interface ReactiveMessageService {
 
 	Mono<String> monoPreAuthorizeBeanFindById(long id);
 
+	Mono<String> monoPreAuthorizeBeanFindByIdReactiveExpression(long id);
+
 	Mono<String> monoPostAuthorizeBeanFindById(long id);
 
 	Flux<String> fluxFindById(long id);
@@ -44,6 +46,10 @@ public interface ReactiveMessageService {
 
 	Flux<String> fluxPostAuthorizeBeanFindById(long id);
 
+	Flux<String> fluxManyAnnotations(Flux<String> flux);
+
+	Flux<String> fluxPostFilter(Flux<String> flux);
+
 	Publisher<String> publisherFindById(long id);
 
 	Publisher<String> publisherPreAuthorizeHasRoleFindById(long id);

+ 5 - 0
core/src/main/java/org/springframework/security/access/prepost/PrePostAdviceReactiveMethodInterceptor.java

@@ -52,7 +52,12 @@ import org.springframework.util.Assert;
  * @author Rob Winch
  * @author Eleftheria Stein
  * @since 5.0
+ * @deprecated Use
+ * {@link org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor}
+ * or
+ * {@link org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor}
  */
+@Deprecated
 public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor {
 
 	private Authentication anonymous = new AnonymousAuthenticationToken("key", "anonymous",

+ 153 - 0
core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java

@@ -0,0 +1,153 @@
+/*
+ * Copyright 2002-2022 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.Function;
+
+import org.aopalliance.aop.Advice;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+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.ReactiveAdapter;
+import org.springframework.core.ReactiveAdapterRegistry;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.security.authorization.ReactiveAuthorizationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link MethodInterceptor} which can determine if an {@link Authentication} has access
+ * to the returned object from the {@link MethodInvocation} using the configured
+ * {@link ReactiveAuthorizationManager}.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.8
+ */
+public final class AuthorizationManagerAfterReactiveMethodInterceptor
+		implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+
+	private final Pointcut pointcut;
+
+	private final ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager;
+
+	private int order = AuthorizationInterceptorsOrder.LAST.getOrder();
+
+	/**
+	 * Creates an instance for the {@link PostAuthorize} annotation.
+	 * @return the {@link AuthorizationManagerAfterReactiveMethodInterceptor} to use
+	 */
+	public static AuthorizationManagerAfterReactiveMethodInterceptor postAuthorize() {
+		return postAuthorize(new PostAuthorizeReactiveAuthorizationManager());
+	}
+
+	/**
+	 * Creates an instance for the {@link PostAuthorize} annotation.
+	 * @param authorizationManager the {@link ReactiveAuthorizationManager} to use
+	 * @return the {@link AuthorizationManagerAfterReactiveMethodInterceptor} to use
+	 */
+	public static AuthorizationManagerAfterReactiveMethodInterceptor postAuthorize(
+			ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager) {
+		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
+				AuthorizationMethodPointcuts.forAnnotations(PostAuthorize.class), authorizationManager);
+		interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE.getOrder());
+		return interceptor;
+	}
+
+	/**
+	 * Creates an instance.
+	 * @param pointcut the {@link Pointcut} to use
+	 * @param authorizationManager the {@link ReactiveAuthorizationManager} to use
+	 */
+	public AuthorizationManagerAfterReactiveMethodInterceptor(Pointcut pointcut,
+			ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager) {
+		Assert.notNull(pointcut, "pointcut cannot be null");
+		Assert.notNull(authorizationManager, "authorizationManager cannot be null");
+		this.pointcut = pointcut;
+		this.authorizationManager = authorizationManager;
+	}
+
+	/**
+	 * Determines if an {@link Authentication} has access to the returned object from the
+	 * {@link MethodInvocation} using the configured {@link ReactiveAuthorizationManager}.
+	 * @param mi the {@link MethodInvocation} to use
+	 * @return the {@link Publisher} from the {@link MethodInvocation} or a
+	 * {@link Publisher} error if access is denied
+	 */
+	@Override
+	public Object invoke(MethodInvocation mi) throws Throwable {
+		Method method = mi.getMethod();
+		Class<?> type = method.getReturnType();
+		Assert.state(Publisher.class.isAssignableFrom(type),
+				() -> String.format("The returnType %s on %s must return an instance of org.reactivestreams.Publisher "
+						+ "(for example, a Mono or Flux) in order to support Reactor Context", type, method));
+		Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
+		Function<Object, Mono<?>> postAuthorize = (result) -> postAuthorize(authentication, mi, result);
+		ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(type);
+		Publisher<?> publisher = ReactiveMethodInvocationUtils.proceed(mi);
+		if (isMultiValue(type, adapter)) {
+			Flux<?> flux = Flux.from(publisher).flatMap(postAuthorize);
+			return (adapter != null) ? adapter.fromPublisher(flux) : flux;
+		}
+		Mono<?> mono = Mono.from(publisher).flatMap(postAuthorize);
+		return (adapter != null) ? adapter.fromPublisher(mono) : mono;
+	}
+
+	private boolean isMultiValue(Class<?> returnType, ReactiveAdapter adapter) {
+		if (Flux.class.isAssignableFrom(returnType)) {
+			return true;
+		}
+		return adapter == null || adapter.isMultiValue();
+	}
+
+	private Mono<?> postAuthorize(Mono<Authentication> authentication, MethodInvocation mi, Object result) {
+		return this.authorizationManager.verify(authentication, new MethodInvocationResult(mi, result))
+				.thenReturn(result);
+	}
+
+	@Override
+	public Pointcut getPointcut() {
+		return this.pointcut;
+	}
+
+	@Override
+	public Advice getAdvice() {
+		return this;
+	}
+
+	@Override
+	public boolean isPerInstance() {
+		return true;
+	}
+
+	@Override
+	public int getOrder() {
+		return this.order;
+	}
+
+	public void setOrder(int order) {
+		this.order = order;
+	}
+
+}

+ 149 - 0
core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java

@@ -0,0 +1,149 @@
+/*
+ * Copyright 2002-2022 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 org.aopalliance.aop.Advice;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+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.ReactiveAdapter;
+import org.springframework.core.ReactiveAdapterRegistry;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authorization.ReactiveAuthorizationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link MethodInterceptor} which can determine if an {@link Authentication} has access
+ * to the {@link MethodInvocation} using the configured
+ * {@link ReactiveAuthorizationManager}.
+ *
+ * @author Evgeniy Cheban
+ * @author Josh Cummings
+ * @since 5.8
+ */
+public final class AuthorizationManagerBeforeReactiveMethodInterceptor
+		implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+
+	private final Pointcut pointcut;
+
+	private final ReactiveAuthorizationManager<MethodInvocation> authorizationManager;
+
+	private int order = AuthorizationInterceptorsOrder.FIRST.getOrder();
+
+	/**
+	 * Creates an instance for the {@link PreAuthorize} annotation.
+	 * @return the {@link AuthorizationManagerBeforeReactiveMethodInterceptor} to use
+	 */
+	public static AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorize() {
+		return preAuthorize(new PreAuthorizeReactiveAuthorizationManager());
+	}
+
+	/**
+	 * Creates an instance for the {@link PreAuthorize} annotation.
+	 * @param authorizationManager the {@link ReactiveAuthorizationManager} to use
+	 * @return the {@link AuthorizationManagerBeforeReactiveMethodInterceptor} to use
+	 */
+	public static AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorize(
+			ReactiveAuthorizationManager<MethodInvocation> authorizationManager) {
+		AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
+				AuthorizationMethodPointcuts.forAnnotations(PreAuthorize.class), authorizationManager);
+		interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder());
+		return interceptor;
+	}
+
+	/**
+	 * Creates an instance.
+	 * @param pointcut the {@link Pointcut} to use
+	 * @param authorizationManager the {@link ReactiveAuthorizationManager} to use
+	 */
+	public AuthorizationManagerBeforeReactiveMethodInterceptor(Pointcut pointcut,
+			ReactiveAuthorizationManager<MethodInvocation> authorizationManager) {
+		Assert.notNull(pointcut, "pointcut cannot be null");
+		Assert.notNull(authorizationManager, "authorizationManager cannot be null");
+		this.pointcut = pointcut;
+		this.authorizationManager = authorizationManager;
+	}
+
+	/**
+	 * Determines if an {@link Authentication} has access to the {@link MethodInvocation}
+	 * using the configured {@link ReactiveAuthorizationManager}.
+	 * @param mi the {@link MethodInvocation} to use
+	 * @return the {@link Publisher} from the {@link MethodInvocation} or a
+	 * {@link Publisher} error if access is denied
+	 */
+	@Override
+	public Object invoke(MethodInvocation mi) throws Throwable {
+		Method method = mi.getMethod();
+		Class<?> type = method.getReturnType();
+		Assert.state(Publisher.class.isAssignableFrom(type),
+				() -> String.format("The returnType %s on %s must return an instance of org.reactivestreams.Publisher "
+						+ "(for example, a Mono or Flux) in order to support Reactor Context", type, method));
+		Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
+		ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(type);
+		Mono<Void> preAuthorize = this.authorizationManager.verify(authentication, mi);
+		if (isMultiValue(type, adapter)) {
+			Publisher<?> publisher = Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
+			Flux<?> result = preAuthorize.thenMany(publisher);
+			return (adapter != null) ? adapter.fromPublisher(result) : result;
+		}
+		Mono<?> publisher = Mono.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
+		Mono<?> result = preAuthorize.then(publisher);
+		return (adapter != null) ? adapter.fromPublisher(result) : result;
+	}
+
+	private boolean isMultiValue(Class<?> returnType, ReactiveAdapter adapter) {
+		if (Flux.class.isAssignableFrom(returnType)) {
+			return true;
+		}
+		return adapter == null || adapter.isMultiValue();
+	}
+
+	@Override
+	public Pointcut getPointcut() {
+		return this.pointcut;
+	}
+
+	@Override
+	public Advice getAdvice() {
+		return this;
+	}
+
+	@Override
+	public boolean isPerInstance() {
+		return true;
+	}
+
+	@Override
+	public int getOrder() {
+		return this.order;
+	}
+
+	public void setOrder(int order) {
+		this.order = order;
+	}
+
+}

+ 10 - 1
core/src/main/java/org/springframework/security/authorization/method/AuthorizationMethodPointcuts.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -22,12 +22,21 @@ import org.springframework.aop.Pointcut;
 import org.springframework.aop.support.ComposablePointcut;
 import org.springframework.aop.support.Pointcuts;
 import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
+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;
 
 /**
  * @author Josh Cummings
+ * @author Evgeniy Cheban
  */
 final class AuthorizationMethodPointcuts {
 
+	static Pointcut forAllAnnotations() {
+		return forAnnotations(PreFilter.class, PreAuthorize.class, PostFilter.class, PostAuthorize.class);
+	}
+
 	@SafeVarargs
 	static Pointcut forAnnotations(Class<? extends Annotation>... annotations) {
 		ComposablePointcut pointcut = null;

+ 5 - 39
core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java

@@ -16,24 +16,18 @@
 
 package org.springframework.security.authorization.method;
 
-import java.lang.reflect.Method;
 import java.util.function.Supplier;
 
 import org.aopalliance.intercept.MethodInvocation;
-import reactor.util.annotation.NonNull;
 
-import org.springframework.aop.support.AopUtils;
 import org.springframework.expression.EvaluationContext;
-import org.springframework.expression.Expression;
 import org.springframework.security.access.expression.ExpressionUtils;
-import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
 import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
 import org.springframework.security.access.prepost.PostAuthorize;
 import org.springframework.security.authorization.AuthorizationDecision;
 import org.springframework.security.authorization.AuthorizationManager;
 import org.springframework.security.authorization.ExpressionAuthorizationDecision;
 import org.springframework.security.core.Authentication;
-import org.springframework.util.Assert;
 
 /**
  * An {@link AuthorizationManager} which can determine if an {@link Authentication} may
@@ -45,17 +39,14 @@ import org.springframework.util.Assert;
  */
 public final class PostAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocationResult> {
 
-	private final PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry();
-
-	private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+	private PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry();
 
 	/**
 	 * Use this the {@link MethodSecurityExpressionHandler}.
 	 * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use
 	 */
 	public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
-		Assert.notNull(expressionHandler, "expressionHandler cannot be null");
-		this.expressionHandler = expressionHandler;
+		this.registry = new PostAuthorizeExpressionAttributeRegistry(expressionHandler);
 	}
 
 	/**
@@ -73,36 +64,11 @@ public final class PostAuthorizeAuthorizationManager implements AuthorizationMan
 		if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
 			return null;
 		}
-		EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication,
-				mi.getMethodInvocation());
-		this.expressionHandler.setReturnObject(mi.getResult(), ctx);
+		MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
+		EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation());
+		expressionHandler.setReturnObject(mi.getResult(), ctx);
 		boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx);
 		return new ExpressionAuthorizationDecision(granted, attribute.getExpression());
 	}
 
-	private final class PostAuthorizeExpressionAttributeRegistry
-			extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {
-
-		@NonNull
-		@Override
-		ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
-			Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
-			PostAuthorize postAuthorize = findPostAuthorizeAnnotation(specificMethod);
-			if (postAuthorize == null) {
-				return ExpressionAttribute.NULL_ATTRIBUTE;
-			}
-			Expression postAuthorizeExpression = PostAuthorizeAuthorizationManager.this.expressionHandler
-					.getExpressionParser().parseExpression(postAuthorize.value());
-			return new ExpressionAttribute(postAuthorizeExpression);
-		}
-
-		private PostAuthorize findPostAuthorizeAnnotation(Method method) {
-			PostAuthorize postAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method,
-					PostAuthorize.class);
-			return (postAuthorize != null) ? postAuthorize : AuthorizationAnnotationUtils
-					.findUniqueAnnotation(method.getDeclaringClass(), PostAuthorize.class);
-		}
-
-	}
-
 }

+ 72 - 0
core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java

@@ -0,0 +1,72 @@
+/*
+ * Copyright 2002-2022 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 reactor.util.annotation.NonNull;
+
+import org.springframework.aop.support.AopUtils;
+import org.springframework.expression.Expression;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.util.Assert;
+
+/**
+ * For internal use only, as this contract is likely to change.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.8
+ */
+final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {
+
+	private final MethodSecurityExpressionHandler expressionHandler;
+
+	PostAuthorizeExpressionAttributeRegistry() {
+		this(new DefaultMethodSecurityExpressionHandler());
+	}
+
+	PostAuthorizeExpressionAttributeRegistry(MethodSecurityExpressionHandler expressionHandler) {
+		Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+		this.expressionHandler = expressionHandler;
+	}
+
+	MethodSecurityExpressionHandler getExpressionHandler() {
+		return this.expressionHandler;
+	}
+
+	@NonNull
+	@Override
+	ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
+		Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
+		PostAuthorize postAuthorize = findPostAuthorizeAnnotation(specificMethod);
+		if (postAuthorize == null) {
+			return ExpressionAttribute.NULL_ATTRIBUTE;
+		}
+		Expression postAuthorizeExpression = this.expressionHandler.getExpressionParser()
+				.parseExpression(postAuthorize.value());
+		return new ExpressionAttribute(postAuthorizeExpression);
+	}
+
+	private PostAuthorize findPostAuthorizeAnnotation(Method method) {
+		PostAuthorize postAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PostAuthorize.class);
+		return (postAuthorize != null) ? postAuthorize
+				: AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PostAuthorize.class);
+	}
+
+}

+ 78 - 0
core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java

@@ -0,0 +1,78 @@
+/*
+ * Copyright 2002-2022 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.MethodInvocation;
+import reactor.core.publisher.Mono;
+
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.ReactiveAuthorizationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link ReactiveAuthorizationManager} which can determine if an {@link Authentication}
+ * has access to the returned object from the {@link MethodInvocation} by evaluating an
+ * expression from the {@link PostAuthorize} annotation.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.8
+ */
+public final class PostAuthorizeReactiveAuthorizationManager
+		implements ReactiveAuthorizationManager<MethodInvocationResult> {
+
+	private final PostAuthorizeExpressionAttributeRegistry registry;
+
+	public PostAuthorizeReactiveAuthorizationManager() {
+		this(new DefaultMethodSecurityExpressionHandler());
+	}
+
+	public PostAuthorizeReactiveAuthorizationManager(MethodSecurityExpressionHandler expressionHandler) {
+		Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+		this.registry = new PostAuthorizeExpressionAttributeRegistry(expressionHandler);
+	}
+
+	/**
+	 * Determines if an {@link Authentication} has access to the returned object from the
+	 * {@link MethodInvocation} by evaluating an expression from the {@link PostAuthorize}
+	 * annotation.
+	 * @param authentication the {@link Mono} of the {@link Authentication} to check
+	 * @param result the {@link MethodInvocationResult} to check
+	 * @return a Mono of the {@link AuthorizationDecision} or an empty {@link Mono} if the
+	 * {@link PostAuthorize} annotation is not present
+	 */
+	@Override
+	public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, MethodInvocationResult result) {
+		MethodInvocation mi = result.getMethodInvocation();
+		ExpressionAttribute attribute = this.registry.getAttribute(mi);
+		if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
+			return Mono.empty();
+		}
+		MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
+		// @formatter:off
+		return authentication
+				.map((auth) -> expressionHandler.createEvaluationContext(auth, mi))
+				.doOnNext((ctx) -> expressionHandler.setReturnObject(result.getResult(), ctx))
+				.flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx))
+				.map((granted) -> new ExpressionAttributeAuthorizationDecision(granted, attribute));
+		// @formatter:on
+	}
+
+}

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

@@ -16,7 +16,6 @@
 
 package org.springframework.security.authorization.method;
 
-import java.lang.reflect.Method;
 import java.util.function.Supplier;
 
 import org.aopalliance.aop.Advice;
@@ -26,19 +25,14 @@ import org.aopalliance.intercept.MethodInvocation;
 import org.springframework.aop.Pointcut;
 import org.springframework.aop.PointcutAdvisor;
 import org.springframework.aop.framework.AopInfrastructureBean;
-import org.springframework.aop.support.AopUtils;
 import org.springframework.core.Ordered;
 import org.springframework.expression.EvaluationContext;
-import org.springframework.expression.Expression;
-import org.springframework.lang.NonNull;
-import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
 import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
 import org.springframework.security.access.prepost.PostFilter;
 import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextHolderStrategy;
-import org.springframework.util.Assert;
 
 /**
  * A {@link MethodInterceptor} which filters a {@code returnedObject} from the
@@ -55,14 +49,12 @@ public final class PostFilterAuthorizationMethodInterceptor
 	private Supplier<Authentication> authentication = getAuthentication(
 			SecurityContextHolder.getContextHolderStrategy());
 
-	private final PostFilterExpressionAttributeRegistry registry = new PostFilterExpressionAttributeRegistry();
+	private PostFilterExpressionAttributeRegistry registry = new PostFilterExpressionAttributeRegistry();
 
 	private int order = AuthorizationInterceptorsOrder.POST_FILTER.getOrder();
 
 	private final Pointcut pointcut;
 
-	private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
-
 	/**
 	 * Creates a {@link PostFilterAuthorizationMethodInterceptor} using the provided
 	 * parameters
@@ -76,8 +68,7 @@ public final class PostFilterAuthorizationMethodInterceptor
 	 * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use
 	 */
 	public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
-		Assert.notNull(expressionHandler, "expressionHandler cannot be null");
-		this.expressionHandler = expressionHandler;
+		this.registry = new PostFilterExpressionAttributeRegistry(expressionHandler);
 	}
 
 	/**
@@ -133,8 +124,9 @@ public final class PostFilterAuthorizationMethodInterceptor
 		if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
 			return returnedObject;
 		}
-		EvaluationContext ctx = this.expressionHandler.createEvaluationContext(this.authentication, mi);
-		return this.expressionHandler.filter(returnedObject, attribute.getExpression(), ctx);
+		MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
+		EvaluationContext ctx = expressionHandler.createEvaluationContext(this.authentication, mi);
+		return expressionHandler.filter(returnedObject, attribute.getExpression(), ctx);
 	}
 
 	private Supplier<Authentication> getAuthentication(SecurityContextHolderStrategy strategy) {
@@ -148,28 +140,4 @@ public final class PostFilterAuthorizationMethodInterceptor
 		};
 	}
 
-	private final class PostFilterExpressionAttributeRegistry
-			extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {
-
-		@NonNull
-		@Override
-		ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
-			Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
-			PostFilter postFilter = findPostFilterAnnotation(specificMethod);
-			if (postFilter == null) {
-				return ExpressionAttribute.NULL_ATTRIBUTE;
-			}
-			Expression postFilterExpression = PostFilterAuthorizationMethodInterceptor.this.expressionHandler
-					.getExpressionParser().parseExpression(postFilter.value());
-			return new ExpressionAttribute(postFilterExpression);
-		}
-
-		private PostFilter findPostFilterAnnotation(Method method) {
-			PostFilter postFilter = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PostFilter.class);
-			return (postFilter != null) ? postFilter
-					: AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PostFilter.class);
-		}
-
-	}
-
 }

+ 153 - 0
core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java

@@ -0,0 +1,153 @@
+/*
+ * Copyright 2002-2022 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 org.aopalliance.aop.Advice;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+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.ReactiveAdapter;
+import org.springframework.core.ReactiveAdapterRegistry;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations;
+import org.springframework.security.access.prepost.PostFilter;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link MethodInterceptor} which filters the returned object from the
+ * {@link MethodInvocation} by evaluating an expression from the {@link PostFilter}
+ * annotation.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.8
+ */
+public final class PostFilterAuthorizationReactiveMethodInterceptor
+		implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+
+	private final PostFilterExpressionAttributeRegistry registry;
+
+	private final Pointcut pointcut = AuthorizationMethodPointcuts.forAnnotations(PostFilter.class);
+
+	private int order = AuthorizationInterceptorsOrder.POST_FILTER.getOrder();
+
+	/**
+	 * Creates an instance.
+	 */
+	public PostFilterAuthorizationReactiveMethodInterceptor() {
+		this(new DefaultMethodSecurityExpressionHandler());
+	}
+
+	/**
+	 * Creates an instance.
+	 */
+	public PostFilterAuthorizationReactiveMethodInterceptor(MethodSecurityExpressionHandler expressionHandler) {
+		Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+		this.registry = new PostFilterExpressionAttributeRegistry(expressionHandler);
+	}
+
+	/**
+	 * Filters the returned object from the {@link MethodInvocation} by evaluating an
+	 * expression from the {@link PostFilter} annotation.
+	 * @param mi the {@link MethodInvocation} to use
+	 * @return the {@link Publisher} to use
+	 */
+	@Override
+	public Object invoke(MethodInvocation mi) throws Throwable {
+		ExpressionAttribute attribute = this.registry.getAttribute(mi);
+		if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
+			return ReactiveMethodInvocationUtils.proceed(mi);
+		}
+		Mono<EvaluationContext> toInvoke = ReactiveAuthenticationUtils.getAuthentication()
+				.map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi));
+		Method method = mi.getMethod();
+		Class<?> type = method.getReturnType();
+		Assert.state(Publisher.class.isAssignableFrom(type),
+				() -> String.format("The parameter type %s on %s must be an instance of org.reactivestreams.Publisher "
+						+ "(for example, a Mono or Flux) in order to support Reactor Context", type, method));
+		ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(type);
+		if (isMultiValue(type, adapter)) {
+			Publisher<?> publisher = Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
+			Flux<?> flux = toInvoke.flatMapMany((ctx) -> filterMultiValue(publisher, ctx, attribute));
+			return (adapter != null) ? adapter.fromPublisher(flux) : flux;
+		}
+		Publisher<?> publisher = Mono.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
+		Mono<?> mono = toInvoke.flatMap((ctx) -> filterSingleValue(publisher, ctx, attribute));
+		return (adapter != null) ? adapter.fromPublisher(mono) : mono;
+	}
+
+	private boolean isMultiValue(Class<?> returnType, ReactiveAdapter adapter) {
+		if (Flux.class.isAssignableFrom(returnType)) {
+			return true;
+		}
+		return adapter == null || adapter.isMultiValue();
+	}
+
+	private Mono<?> filterSingleValue(Publisher<?> publisher, EvaluationContext ctx, ExpressionAttribute attribute) {
+		return Mono.from(publisher).doOnNext((result) -> setFilterObject(ctx, result))
+				.flatMap((result) -> postFilter(ctx, result, attribute));
+	}
+
+	private Flux<?> filterMultiValue(Publisher<?> publisher, EvaluationContext ctx, ExpressionAttribute attribute) {
+		return Flux.from(publisher).doOnNext((result) -> setFilterObject(ctx, result))
+				.flatMap((result) -> postFilter(ctx, result, attribute));
+	}
+
+	private void setFilterObject(EvaluationContext ctx, Object result) {
+		((MethodSecurityExpressionOperations) ctx.getRootObject().getValue()).setFilterObject(result);
+	}
+
+	private Mono<?> postFilter(EvaluationContext ctx, Object result, ExpressionAttribute attribute) {
+		return ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx)
+				.flatMap((granted) -> granted ? Mono.just(result) : Mono.empty());
+	}
+
+	@Override
+	public Pointcut getPointcut() {
+		return this.pointcut;
+	}
+
+	@Override
+	public Advice getAdvice() {
+		return this;
+	}
+
+	@Override
+	public boolean isPerInstance() {
+		return true;
+	}
+
+	@Override
+	public int getOrder() {
+		return this.order;
+	}
+
+	public void setOrder(int order) {
+		this.order = order;
+	}
+
+}

+ 71 - 0
core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java

@@ -0,0 +1,71 @@
+/*
+ * Copyright 2002-2022 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 org.springframework.aop.support.AopUtils;
+import org.springframework.expression.Expression;
+import org.springframework.lang.NonNull;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.prepost.PostFilter;
+import org.springframework.util.Assert;
+
+/**
+ * For internal use only, as this contract is likely to change.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.8
+ */
+final class PostFilterExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {
+
+	private final MethodSecurityExpressionHandler expressionHandler;
+
+	PostFilterExpressionAttributeRegistry() {
+		this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
+	}
+
+	PostFilterExpressionAttributeRegistry(MethodSecurityExpressionHandler expressionHandler) {
+		Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+		this.expressionHandler = expressionHandler;
+	}
+
+	MethodSecurityExpressionHandler getExpressionHandler() {
+		return this.expressionHandler;
+	}
+
+	@NonNull
+	@Override
+	ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
+		Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
+		PostFilter postFilter = findPostFilterAnnotation(specificMethod);
+		if (postFilter == null) {
+			return ExpressionAttribute.NULL_ATTRIBUTE;
+		}
+		Expression postFilterExpression = this.expressionHandler.getExpressionParser()
+				.parseExpression(postFilter.value());
+		return new ExpressionAttribute(postFilterExpression);
+	}
+
+	private PostFilter findPostFilterAnnotation(Method method) {
+		PostFilter postFilter = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PostFilter.class);
+		return (postFilter != null) ? postFilter
+				: AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PostFilter.class);
+	}
+
+}

+ 3 - 36
core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java

@@ -16,24 +16,18 @@
 
 package org.springframework.security.authorization.method;
 
-import java.lang.reflect.Method;
 import java.util.function.Supplier;
 
 import org.aopalliance.intercept.MethodInvocation;
-import reactor.util.annotation.NonNull;
 
-import org.springframework.aop.support.AopUtils;
 import org.springframework.expression.EvaluationContext;
-import org.springframework.expression.Expression;
 import org.springframework.security.access.expression.ExpressionUtils;
-import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
 import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.authorization.AuthorizationDecision;
 import org.springframework.security.authorization.AuthorizationManager;
 import org.springframework.security.authorization.ExpressionAuthorizationDecision;
 import org.springframework.security.core.Authentication;
-import org.springframework.util.Assert;
 
 /**
  * An {@link AuthorizationManager} which can determine if an {@link Authentication} may
@@ -45,17 +39,14 @@ import org.springframework.util.Assert;
  */
 public final class PreAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocation> {
 
-	private final PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry();
-
-	private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+	private PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry();
 
 	/**
 	 * Sets the {@link MethodSecurityExpressionHandler}.
 	 * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use
 	 */
 	public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
-		Assert.notNull(expressionHandler, "expressionHandler cannot be null");
-		this.expressionHandler = expressionHandler;
+		this.registry = new PreAuthorizeExpressionAttributeRegistry(expressionHandler);
 	}
 
 	/**
@@ -73,33 +64,9 @@ public final class PreAuthorizeAuthorizationManager implements AuthorizationMana
 		if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
 			return null;
 		}
-		EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, mi);
+		EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi);
 		boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx);
 		return new ExpressionAuthorizationDecision(granted, attribute.getExpression());
 	}
 
-	private final class PreAuthorizeExpressionAttributeRegistry
-			extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {
-
-		@NonNull
-		@Override
-		ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
-			Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
-			PreAuthorize preAuthorize = findPreAuthorizeAnnotation(specificMethod);
-			if (preAuthorize == null) {
-				return ExpressionAttribute.NULL_ATTRIBUTE;
-			}
-			Expression preAuthorizeExpression = PreAuthorizeAuthorizationManager.this.expressionHandler
-					.getExpressionParser().parseExpression(preAuthorize.value());
-			return new ExpressionAttribute(preAuthorizeExpression);
-		}
-
-		private PreAuthorize findPreAuthorizeAnnotation(Method method) {
-			PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class);
-			return (preAuthorize != null) ? preAuthorize
-					: AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PreAuthorize.class);
-		}
-
-	}
-
 }

+ 76 - 0
core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java

@@ -0,0 +1,76 @@
+/*
+ * Copyright 2002-2022 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 reactor.util.annotation.NonNull;
+
+import org.springframework.aop.support.AopUtils;
+import org.springframework.expression.Expression;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.Assert;
+
+/**
+ * For internal use only, as this contract is likely to change.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.8
+ */
+final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {
+
+	private final MethodSecurityExpressionHandler expressionHandler;
+
+	PreAuthorizeExpressionAttributeRegistry() {
+		this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
+	}
+
+	PreAuthorizeExpressionAttributeRegistry(MethodSecurityExpressionHandler expressionHandler) {
+		Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+		this.expressionHandler = expressionHandler;
+	}
+
+	/**
+	 * Returns the {@link MethodSecurityExpressionHandler}.
+	 * @return the {@link MethodSecurityExpressionHandler} to use
+	 */
+	MethodSecurityExpressionHandler getExpressionHandler() {
+		return this.expressionHandler;
+	}
+
+	@NonNull
+	@Override
+	ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
+		Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
+		PreAuthorize preAuthorize = findPreAuthorizeAnnotation(specificMethod);
+		if (preAuthorize == null) {
+			return ExpressionAttribute.NULL_ATTRIBUTE;
+		}
+		Expression preAuthorizeExpression = this.expressionHandler.getExpressionParser()
+				.parseExpression(preAuthorize.value());
+		return new ExpressionAttribute(preAuthorizeExpression);
+	}
+
+	private PreAuthorize findPreAuthorizeAnnotation(Method method) {
+		PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class);
+		return (preAuthorize != null) ? preAuthorize
+				: AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PreAuthorize.class);
+	}
+
+}

+ 73 - 0
core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java

@@ -0,0 +1,73 @@
+/*
+ * Copyright 2002-2022 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.MethodInvocation;
+import reactor.core.publisher.Mono;
+
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.ReactiveAuthorizationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link ReactiveAuthorizationManager} which can determine if an {@link Authentication}
+ * has access to the {@link MethodInvocation} by evaluating an expression from the
+ * {@link PreAuthorize} annotation.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.8
+ */
+public final class PreAuthorizeReactiveAuthorizationManager implements ReactiveAuthorizationManager<MethodInvocation> {
+
+	private final PreAuthorizeExpressionAttributeRegistry registry;
+
+	public PreAuthorizeReactiveAuthorizationManager() {
+		this(new DefaultMethodSecurityExpressionHandler());
+	}
+
+	public PreAuthorizeReactiveAuthorizationManager(MethodSecurityExpressionHandler expressionHandler) {
+		Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+		this.registry = new PreAuthorizeExpressionAttributeRegistry(expressionHandler);
+	}
+
+	/**
+	 * Determines if an {@link Authentication} has access to the {@link MethodInvocation}
+	 * by evaluating an expression from the {@link PreAuthorize} annotation.
+	 * @param authentication the {@link Mono} of the {@link Authentication} to check
+	 * @param mi the {@link MethodInvocation} to check
+	 * @return a {@link Mono} of the {@link AuthorizationDecision} or an empty
+	 * {@link Mono} if the {@link PreAuthorize} annotation is not present
+	 */
+	@Override
+	public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, MethodInvocation mi) {
+		ExpressionAttribute attribute = this.registry.getAttribute(mi);
+		if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
+			return Mono.empty();
+		}
+		// @formatter:off
+		return authentication
+				.map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi))
+				.flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx))
+				.map((granted) -> new ExpressionAttributeAuthorizationDecision(granted, attribute));
+		// @formatter:on
+	}
+
+}

+ 8 - 52
core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java

@@ -16,7 +16,6 @@
 
 package org.springframework.security.authorization.method;
 
-import java.lang.reflect.Method;
 import java.util.function.Supplier;
 
 import org.aopalliance.aop.Advice;
@@ -26,12 +25,8 @@ import org.aopalliance.intercept.MethodInvocation;
 import org.springframework.aop.Pointcut;
 import org.springframework.aop.PointcutAdvisor;
 import org.springframework.aop.framework.AopInfrastructureBean;
-import org.springframework.aop.support.AopUtils;
 import org.springframework.core.Ordered;
 import org.springframework.expression.EvaluationContext;
-import org.springframework.expression.Expression;
-import org.springframework.lang.NonNull;
-import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
 import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
 import org.springframework.security.access.prepost.PreFilter;
 import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
@@ -55,14 +50,12 @@ public final class PreFilterAuthorizationMethodInterceptor
 	private Supplier<Authentication> authentication = getAuthentication(
 			SecurityContextHolder.getContextHolderStrategy());
 
-	private final PreFilterExpressionAttributeRegistry registry = new PreFilterExpressionAttributeRegistry();
+	private PreFilterExpressionAttributeRegistry registry = new PreFilterExpressionAttributeRegistry();
 
 	private int order = AuthorizationInterceptorsOrder.PRE_FILTER.getOrder();
 
 	private final Pointcut pointcut;
 
-	private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
-
 	/**
 	 * Creates a {@link PreFilterAuthorizationMethodInterceptor} using the provided
 	 * parameters
@@ -76,8 +69,7 @@ public final class PreFilterAuthorizationMethodInterceptor
 	 * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use
 	 */
 	public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
-		Assert.notNull(expressionHandler, "expressionHandler cannot be null");
-		this.expressionHandler = expressionHandler;
+		this.registry = new PreFilterExpressionAttributeRegistry(expressionHandler);
 	}
 
 	/**
@@ -127,13 +119,14 @@ public final class PreFilterAuthorizationMethodInterceptor
 	 */
 	@Override
 	public Object invoke(MethodInvocation mi) throws Throwable {
-		PreFilterExpressionAttribute attribute = this.registry.getAttribute(mi);
-		if (attribute == PreFilterExpressionAttribute.NULL_ATTRIBUTE) {
+		PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute attribute = this.registry.getAttribute(mi);
+		if (attribute == PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute.NULL_ATTRIBUTE) {
 			return mi.proceed();
 		}
-		EvaluationContext ctx = this.expressionHandler.createEvaluationContext(this.authentication, mi);
-		Object filterTarget = findFilterTarget(attribute.filterTarget, ctx, mi);
-		this.expressionHandler.filter(filterTarget, attribute.getExpression(), ctx);
+		MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
+		EvaluationContext ctx = expressionHandler.createEvaluationContext(this.authentication, mi);
+		Object filterTarget = findFilterTarget(attribute.getFilterTarget(), ctx, mi);
+		expressionHandler.filter(filterTarget, attribute.getExpression(), ctx);
 		return mi.proceed();
 	}
 
@@ -168,41 +161,4 @@ public final class PreFilterAuthorizationMethodInterceptor
 		};
 	}
 
-	private final class PreFilterExpressionAttributeRegistry
-			extends AbstractExpressionAttributeRegistry<PreFilterExpressionAttribute> {
-
-		@NonNull
-		@Override
-		PreFilterExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
-			Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
-			PreFilter preFilter = findPreFilterAnnotation(specificMethod);
-			if (preFilter == null) {
-				return PreFilterExpressionAttribute.NULL_ATTRIBUTE;
-			}
-			Expression preFilterExpression = PreFilterAuthorizationMethodInterceptor.this.expressionHandler
-					.getExpressionParser().parseExpression(preFilter.value());
-			return new PreFilterExpressionAttribute(preFilterExpression, preFilter.filterTarget());
-		}
-
-		private PreFilter findPreFilterAnnotation(Method method) {
-			PreFilter preFilter = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreFilter.class);
-			return (preFilter != null) ? preFilter
-					: AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PreFilter.class);
-		}
-
-	}
-
-	private static final class PreFilterExpressionAttribute extends ExpressionAttribute {
-
-		private static final PreFilterExpressionAttribute NULL_ATTRIBUTE = new PreFilterExpressionAttribute(null, null);
-
-		private final String filterTarget;
-
-		private PreFilterExpressionAttribute(Expression expression, String filterTarget) {
-			super(expression);
-			this.filterTarget = filterTarget;
-		}
-
-	}
-
 }

+ 213 - 0
core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java

@@ -0,0 +1,213 @@
+/*
+ * Copyright 2002-2022 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 org.aopalliance.aop.Advice;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.springframework.aop.Pointcut;
+import org.springframework.aop.PointcutAdvisor;
+import org.springframework.aop.framework.AopInfrastructureBean;
+import org.springframework.aop.support.AopUtils;
+import org.springframework.core.Ordered;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.core.ReactiveAdapter;
+import org.springframework.core.ReactiveAdapterRegistry;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations;
+import org.springframework.security.access.prepost.PreFilter;
+import org.springframework.security.core.parameters.DefaultSecurityParameterNameDiscoverer;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * A {@link MethodInterceptor} which filters a reactive method argument by evaluating an
+ * expression from the {@link PreFilter} annotation.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.8
+ */
+public final class PreFilterAuthorizationReactiveMethodInterceptor
+		implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+
+	private final PreFilterExpressionAttributeRegistry registry;
+
+	private final Pointcut pointcut = AuthorizationMethodPointcuts.forAnnotations(PreFilter.class);
+
+	private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultSecurityParameterNameDiscoverer();
+
+	private int order = AuthorizationInterceptorsOrder.PRE_FILTER.getOrder();
+
+	public PreFilterAuthorizationReactiveMethodInterceptor() {
+		this(new DefaultMethodSecurityExpressionHandler());
+	}
+
+	/**
+	 * Creates an instance.
+	 */
+	public PreFilterAuthorizationReactiveMethodInterceptor(MethodSecurityExpressionHandler expressionHandler) {
+		Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+		this.registry = new PreFilterExpressionAttributeRegistry(expressionHandler);
+	}
+
+	/**
+	 * Sets the {@link ParameterNameDiscoverer}.
+	 * @param parameterNameDiscoverer the {@link ParameterNameDiscoverer} to use
+	 */
+	public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
+		Assert.notNull(parameterNameDiscoverer, "parameterNameDiscoverer cannot be null");
+		this.parameterNameDiscoverer = parameterNameDiscoverer;
+	}
+
+	/**
+	 * Filters a reactive method argument by evaluating an expression from the
+	 * {@link PreFilter} annotation.
+	 * @param mi the {@link MethodInvocation} to use
+	 * @return the {@link Publisher} to use
+	 */
+	@Override
+	public Object invoke(MethodInvocation mi) throws Throwable {
+		PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute attribute = this.registry.getAttribute(mi);
+		if (attribute == PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute.NULL_ATTRIBUTE) {
+			return ReactiveMethodInvocationUtils.proceed(mi);
+		}
+		FilterTarget filterTarget = findFilterTarget(attribute.getFilterTarget(), mi);
+		Mono<EvaluationContext> toInvoke = ReactiveAuthenticationUtils.getAuthentication()
+				.map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi));
+		Method method = mi.getMethod();
+		Class<?> type = filterTarget.value.getClass();
+		Assert.state(Publisher.class.isAssignableFrom(type),
+				() -> String.format("The parameter type %s on %s must be an instance of org.reactivestreams.Publisher "
+						+ "(for example, a Mono or Flux) in order to support Reactor Context", type, method));
+		ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(type);
+		if (isMultiValue(type, adapter)) {
+			Flux<?> result = toInvoke
+					.flatMapMany((ctx) -> filterMultiValue(filterTarget.value, attribute.getExpression(), ctx));
+			mi.getArguments()[filterTarget.index] = (adapter != null) ? adapter.fromPublisher(result) : result;
+		}
+		else {
+			Mono<?> result = toInvoke
+					.flatMap((ctx) -> filterSingleValue(filterTarget.value, attribute.getExpression(), ctx));
+			mi.getArguments()[filterTarget.index] = (adapter != null) ? adapter.fromPublisher(result) : result;
+		}
+		return ReactiveMethodInvocationUtils.proceed(mi);
+	}
+
+	private FilterTarget findFilterTarget(String name, MethodInvocation mi) {
+		Object value = null;
+		int index = 0;
+		if (StringUtils.hasText(name)) {
+			Object target = mi.getThis();
+			Class<?> targetClass = (target != null) ? AopUtils.getTargetClass(target) : null;
+			Method specificMethod = AopUtils.getMostSpecificMethod(mi.getMethod(), targetClass);
+			String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(specificMethod);
+			if (parameterNames != null && parameterNames.length > 0) {
+				Object[] arguments = mi.getArguments();
+				for (index = 0; index < parameterNames.length; index++) {
+					if (name.equals(parameterNames[index])) {
+						value = arguments[index];
+						break;
+					}
+				}
+				Assert.notNull(value,
+						"Filter target was null, or no argument with name '" + name + "' found in method.");
+			}
+		}
+		else {
+			Object[] arguments = mi.getArguments();
+			Assert.state(arguments.length == 1,
+					"Unable to determine the method argument for filtering. Specify the filter target.");
+			value = arguments[0];
+			Assert.notNull(value,
+					"Filter target was null. Make sure you passing the correct value in the method argument.");
+		}
+		Assert.state(value instanceof Publisher<?>, "Filter target must be an instance of Publisher.");
+		return new FilterTarget((Publisher<?>) value, index);
+	}
+
+	private boolean isMultiValue(Class<?> returnType, ReactiveAdapter adapter) {
+		if (Flux.class.isAssignableFrom(returnType)) {
+			return true;
+		}
+		return adapter == null || adapter.isMultiValue();
+	}
+
+	private Mono<?> filterSingleValue(Publisher<?> filterTarget, Expression filterExpression, EvaluationContext ctx) {
+		MethodSecurityExpressionOperations rootObject = (MethodSecurityExpressionOperations) ctx.getRootObject()
+				.getValue();
+		return Mono.from(filterTarget).filterWhen((filterObject) -> {
+			rootObject.setFilterObject(filterObject);
+			return ReactiveExpressionUtils.evaluateAsBoolean(filterExpression, ctx);
+		});
+	}
+
+	private Flux<?> filterMultiValue(Publisher<?> filterTarget, Expression filterExpression, EvaluationContext ctx) {
+		MethodSecurityExpressionOperations rootObject = (MethodSecurityExpressionOperations) ctx.getRootObject()
+				.getValue();
+		return Flux.from(filterTarget).filterWhen((filterObject) -> {
+			rootObject.setFilterObject(filterObject);
+			return ReactiveExpressionUtils.evaluateAsBoolean(filterExpression, ctx);
+		});
+	}
+
+	@Override
+	public Pointcut getPointcut() {
+		return this.pointcut;
+	}
+
+	@Override
+	public Advice getAdvice() {
+		return this;
+	}
+
+	@Override
+	public boolean isPerInstance() {
+		return true;
+	}
+
+	@Override
+	public int getOrder() {
+		return this.order;
+	}
+
+	public void setOrder(int order) {
+		this.order = order;
+	}
+
+	private static final class FilterTarget {
+
+		private final Publisher<?> value;
+
+		private final int index;
+
+		private FilterTarget(Publisher<?> value, int index) {
+			this.value = value;
+			this.index = index;
+		}
+
+	}
+
+}

+ 89 - 0
core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java

@@ -0,0 +1,89 @@
+/*
+ * Copyright 2002-2022 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 org.springframework.aop.support.AopUtils;
+import org.springframework.expression.Expression;
+import org.springframework.lang.NonNull;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.prepost.PreFilter;
+import org.springframework.util.Assert;
+
+/**
+ * For internal use only, as this contract is likely to change.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.8
+ */
+final class PreFilterExpressionAttributeRegistry
+		extends AbstractExpressionAttributeRegistry<PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute> {
+
+	private final MethodSecurityExpressionHandler expressionHandler;
+
+	PreFilterExpressionAttributeRegistry() {
+		this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
+	}
+
+	PreFilterExpressionAttributeRegistry(MethodSecurityExpressionHandler expressionHandler) {
+		Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+		this.expressionHandler = expressionHandler;
+	}
+
+	MethodSecurityExpressionHandler getExpressionHandler() {
+		return this.expressionHandler;
+	}
+
+	@NonNull
+	@Override
+	PreFilterExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
+		Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
+		PreFilter preFilter = findPreFilterAnnotation(specificMethod);
+		if (preFilter == null) {
+			return PreFilterExpressionAttribute.NULL_ATTRIBUTE;
+		}
+		Expression preFilterExpression = this.expressionHandler.getExpressionParser()
+				.parseExpression(preFilter.value());
+		return new PreFilterExpressionAttribute(preFilterExpression, preFilter.filterTarget());
+	}
+
+	private PreFilter findPreFilterAnnotation(Method method) {
+		PreFilter preFilter = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreFilter.class);
+		return (preFilter != null) ? preFilter
+				: AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PreFilter.class);
+	}
+
+	static final class PreFilterExpressionAttribute extends ExpressionAttribute {
+
+		static final PreFilterExpressionAttribute NULL_ATTRIBUTE = new PreFilterExpressionAttribute(null, null);
+
+		private final String filterTarget;
+
+		private PreFilterExpressionAttribute(Expression expression, String filterTarget) {
+			super(expression);
+			this.filterTarget = filterTarget;
+		}
+
+		String getFilterTarget() {
+			return this.filterTarget;
+		}
+
+	}
+
+}

+ 46 - 0
core/src/main/java/org/springframework/security/authorization/method/ReactiveAuthenticationUtils.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright 2002-2022 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 reactor.core.publisher.Mono;
+
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.context.ReactiveSecurityContextHolder;
+import org.springframework.security.core.context.SecurityContext;
+
+/**
+ * For internal use only, as this contract is likely to change.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.8
+ */
+final class ReactiveAuthenticationUtils {
+
+	private static final Authentication ANONYMOUS = new AnonymousAuthenticationToken("key", "anonymous",
+			AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
+
+	static Mono<Authentication> getAuthentication() {
+		return ReactiveSecurityContextHolder.getContext().map(SecurityContext::getAuthentication)
+				.defaultIfEmpty(ANONYMOUS);
+	}
+
+	private ReactiveAuthenticationUtils() {
+	}
+
+}

+ 67 - 0
core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java

@@ -0,0 +1,67 @@
+/*
+ * Copyright 2002-2022 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 reactor.core.publisher.Mono;
+
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.EvaluationException;
+import org.springframework.expression.Expression;
+
+/**
+ * For internal use only, as this contract is likely to change.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.8
+ */
+final class ReactiveExpressionUtils {
+
+	static Mono<Boolean> evaluateAsBoolean(Expression expr, EvaluationContext ctx) {
+		return Mono.defer(() -> {
+			Object value;
+			try {
+				value = expr.getValue(ctx);
+			}
+			catch (EvaluationException ex) {
+				return Mono.error(() -> new IllegalArgumentException(
+						"Failed to evaluate expression '" + expr.getExpressionString() + "'", ex));
+			}
+			if (value instanceof Boolean) {
+				return Mono.just((Boolean) value);
+			}
+			if (value instanceof Mono<?>) {
+				Mono<?> monoValue = (Mono<?>) value;
+				// @formatter:off
+				return monoValue
+						.filter(Boolean.class::isInstance)
+						.map(Boolean.class::cast)
+						.switchIfEmpty(createInvalidReturnTypeMono(expr));
+				// @formatter:on
+			}
+			return createInvalidReturnTypeMono(expr);
+		});
+	}
+
+	private static Mono<Boolean> createInvalidReturnTypeMono(Expression expr) {
+		return Mono.error(() -> new IllegalStateException(
+				"Expression: '" + expr.getExpressionString() + "' must return boolean or Mono<Boolean>"));
+	}
+
+	private ReactiveExpressionUtils() {
+	}
+
+}

+ 42 - 0
core/src/main/java/org/springframework/security/authorization/method/ReactiveMethodInvocationUtils.java

@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2022 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.MethodInvocation;
+import reactor.core.Exceptions;
+
+/**
+ * For internal use only, as this contract is likely to change.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.8
+ */
+final class ReactiveMethodInvocationUtils {
+
+	static <T> T proceed(MethodInvocation mi) {
+		try {
+			return (T) mi.proceed();
+		}
+		catch (Throwable ex) {
+			throw Exceptions.propagate(ex);
+		}
+	}
+
+	private ReactiveMethodInvocationUtils() {
+	}
+
+}

+ 6 - 1
core/src/test/java/org/springframework/security/access/intercept/method/MockMethodInvocation.java

@@ -38,10 +38,15 @@ public class MockMethodInvocation implements MethodInvocation {
 
 	public MockMethodInvocation(Object targetObject, Class clazz, String methodName, Class... parameterTypes)
 			throws NoSuchMethodException {
-		this.method = clazz.getMethod(methodName, parameterTypes);
+		this(targetObject, clazz.getMethod(methodName, parameterTypes));
 		this.targetObject = targetObject;
 	}
 
+	public MockMethodInvocation(Object targetObject, Method method) {
+		this.targetObject = targetObject;
+		this.method = method;
+	}
+
 	@Override
 	public Object[] getArguments() {
 		return this.arguments;

+ 124 - 0
core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptorTests.java

@@ -0,0 +1,124 @@
+/*
+ * Copyright 2002-2022 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.MethodInvocation;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.springframework.aop.Pointcut;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.intercept.method.MockMethodInvocation;
+import org.springframework.security.authorization.ReactiveAuthorizationManager;
+
+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;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link AuthorizationManagerAfterReactiveMethodInterceptor}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
+
+	@Test
+	public void instantiateWhenPointcutNullThenException() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> new AuthorizationManagerAfterReactiveMethodInterceptor(null,
+						mock(ReactiveAuthorizationManager.class)))
+				.withMessage("pointcut cannot be null");
+	}
+
+	@Test
+	public void instantiateWhenAuthorizationManagerNullThenException() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> new AuthorizationManagerAfterReactiveMethodInterceptor(mock(Pointcut.class), null))
+				.withMessage("authorizationManager cannot be null");
+	}
+
+	@Test
+	public void invokeMonoWhenMockReactiveAuthorizationManagerThenVerify() throws Throwable {
+		MethodInvocation mockMethodInvocation = spy(
+				new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
+		given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
+		ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
+				ReactiveAuthorizationManager.class);
+		given(mockReactiveAuthorizationManager.verify(any(), any())).willReturn(Mono.empty());
+		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
+				Pointcut.TRUE, mockReactiveAuthorizationManager);
+		Object result = interceptor.invoke(mockMethodInvocation);
+		assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)).extracting(Mono::block)
+				.isEqualTo("john");
+		verify(mockReactiveAuthorizationManager).verify(any(), any());
+	}
+
+	@Test
+	public void invokeFluxWhenMockReactiveAuthorizationManagerThenVerify() throws Throwable {
+		MethodInvocation mockMethodInvocation = spy(
+				new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
+		given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
+		ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
+				ReactiveAuthorizationManager.class);
+		given(mockReactiveAuthorizationManager.verify(any(), any())).willReturn(Mono.empty());
+		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
+				Pointcut.TRUE, mockReactiveAuthorizationManager);
+		Object result = interceptor.invoke(mockMethodInvocation);
+		assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Flux.class)).extracting(Flux::collectList)
+				.extracting(Mono::block, InstanceOfAssertFactories.list(String.class)).containsExactly("john", "bob");
+		verify(mockReactiveAuthorizationManager, times(2)).verify(any(), any());
+	}
+
+	@Test
+	public void invokeWhenMockReactiveAuthorizationManagerDeniedThenAccessDeniedException() throws Throwable {
+		MethodInvocation mockMethodInvocation = spy(
+				new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
+		given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
+		ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
+				ReactiveAuthorizationManager.class);
+		given(mockReactiveAuthorizationManager.verify(any(), any()))
+				.willReturn(Mono.error(new AccessDeniedException("Access Denied")));
+		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
+				Pointcut.TRUE, mockReactiveAuthorizationManager);
+		Object result = interceptor.invoke(mockMethodInvocation);
+		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> assertThat(result)
+				.asInstanceOf(InstanceOfAssertFactories.type(Mono.class)).extracting(Mono::block))
+				.withMessage("Access Denied");
+		verify(mockReactiveAuthorizationManager).verify(any(), any());
+	}
+
+	class Sample {
+
+		Mono<String> mono() {
+			return Mono.just("john");
+		}
+
+		Flux<String> flux() {
+			return Flux.just("john", "bob");
+		}
+
+	}
+
+}

+ 125 - 0
core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptorTests.java

@@ -0,0 +1,125 @@
+/*
+ * Copyright 2002-2022 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.MethodInvocation;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.springframework.aop.Pointcut;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.intercept.method.MockMethodInvocation;
+import org.springframework.security.authorization.ReactiveAuthorizationManager;
+
+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.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link AuthorizationManagerBeforeReactiveMethodInterceptor}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
+
+	@Test
+	public void instantiateWhenPointcutNullThenException() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> new AuthorizationManagerBeforeReactiveMethodInterceptor(null,
+						mock(ReactiveAuthorizationManager.class)))
+				.withMessage("pointcut cannot be null");
+
+	}
+
+	@Test
+	public void instantiateWhenAuthorizationManagerNullThenException() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> new AuthorizationManagerBeforeReactiveMethodInterceptor(mock(Pointcut.class), null))
+				.withMessage("authorizationManager cannot be null");
+	}
+
+	@Test
+	public void invokeMonoWhenMockReactiveAuthorizationManagerThenVerify() throws Throwable {
+		MethodInvocation mockMethodInvocation = spy(
+				new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
+		given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
+		ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
+				ReactiveAuthorizationManager.class);
+		given(mockReactiveAuthorizationManager.verify(any(), eq(mockMethodInvocation))).willReturn(Mono.empty());
+		AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
+				Pointcut.TRUE, mockReactiveAuthorizationManager);
+		Object result = interceptor.invoke(mockMethodInvocation);
+		assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)).extracting(Mono::block)
+				.isEqualTo("john");
+		verify(mockReactiveAuthorizationManager).verify(any(), eq(mockMethodInvocation));
+	}
+
+	@Test
+	public void invokeFluxWhenMockReactiveAuthorizationManagerThenVerify() throws Throwable {
+		MethodInvocation mockMethodInvocation = spy(
+				new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
+		given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
+		ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
+				ReactiveAuthorizationManager.class);
+		given(mockReactiveAuthorizationManager.verify(any(), eq(mockMethodInvocation))).willReturn(Mono.empty());
+		AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
+				Pointcut.TRUE, mockReactiveAuthorizationManager);
+		Object result = interceptor.invoke(mockMethodInvocation);
+		assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Flux.class)).extracting(Flux::collectList)
+				.extracting(Mono::block, InstanceOfAssertFactories.list(String.class)).containsExactly("john", "bob");
+		verify(mockReactiveAuthorizationManager).verify(any(), eq(mockMethodInvocation));
+	}
+
+	@Test
+	public void invokeWhenMockReactiveAuthorizationManagerDeniedThenAccessDeniedException() throws Throwable {
+		MethodInvocation mockMethodInvocation = spy(
+				new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
+		given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
+		ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
+				ReactiveAuthorizationManager.class);
+		given(mockReactiveAuthorizationManager.verify(any(), eq(mockMethodInvocation)))
+				.willReturn(Mono.error(new AccessDeniedException("Access Denied")));
+		AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
+				Pointcut.TRUE, mockReactiveAuthorizationManager);
+		Object result = interceptor.invoke(mockMethodInvocation);
+		assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> assertThat(result)
+				.asInstanceOf(InstanceOfAssertFactories.type(Mono.class)).extracting(Mono::block))
+				.withMessage("Access Denied");
+		verify(mockReactiveAuthorizationManager).verify(any(), eq(mockMethodInvocation));
+	}
+
+	class Sample {
+
+		Mono<String> mono() {
+			return Mono.just("john");
+		}
+
+		Flux<String> flux() {
+			return Flux.just("john", "bob");
+		}
+
+	}
+
+}

+ 2 - 2
core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -51,7 +51,7 @@ public class PostAuthorizeAuthorizationManagerTests {
 		MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
 		PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
 		manager.setExpressionHandler(expressionHandler);
-		assertThat(manager).extracting("expressionHandler").isEqualTo(expressionHandler);
+		assertThat(manager).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler);
 	}
 
 	@Test

+ 246 - 0
core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManagerTests.java

@@ -0,0 +1,246 @@
+/*
+ * Copyright 2002-2022 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.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Mono;
+
+import org.springframework.core.annotation.AnnotationConfigurationException;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.intercept.method.MockMethodInvocation;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.core.Authentication;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link PostAuthorizeReactiveAuthorizationManager}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class PostAuthorizeReactiveAuthorizationManagerTests {
+
+	@Test
+	public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() {
+		MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+		PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(
+				expressionHandler);
+		assertThat(manager).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler);
+	}
+
+	@Test
+	public void setExpressionHandlerWhenNullThenException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> new PostAuthorizeReactiveAuthorizationManager(null))
+				.withMessage("expressionHandler cannot be null");
+	}
+
+	@Test
+	public void checkDoSomethingWhenNoPostAuthorizeAnnotationThenNullDecision() throws Exception {
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"doSomething", new Class[] {}, new Object[] {});
+		PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager();
+		MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null);
+		AuthorizationDecision decision = manager.check(ReactiveAuthenticationUtils.getAuthentication(), result).block();
+		assertThat(decision).isNull();
+	}
+
+	@Test
+	public void checkDoSomethingStringWhenArgIsGrantThenGrantedDecision() throws Exception {
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"doSomethingString", new Class[] { String.class }, new Object[] { "grant" });
+		PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager();
+		MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null);
+		AuthorizationDecision decision = manager.check(ReactiveAuthenticationUtils.getAuthentication(), result).block();
+		assertThat(decision).isNotNull();
+		assertThat(decision.isGranted()).isTrue();
+	}
+
+	@Test
+	public void checkDoSomethingStringWhenArgIsNotGrantThenDeniedDecision() throws Exception {
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"doSomethingString", new Class[] { String.class }, new Object[] { "deny" });
+		MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null);
+		PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager();
+		AuthorizationDecision decision = manager.check(ReactiveAuthenticationUtils.getAuthentication(), result).block();
+		assertThat(decision).isNotNull();
+		assertThat(decision.isGranted()).isFalse();
+	}
+
+	@Test
+	public void checkDoSomethingListWhenReturnObjectContainsGrantThenGrantedDecision() throws Exception {
+		List<String> list = Arrays.asList("grant", "deny");
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"doSomethingList", new Class[] { List.class }, new Object[] { list });
+		MethodInvocationResult result = new MethodInvocationResult(methodInvocation, list);
+		PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager();
+		AuthorizationDecision decision = manager.check(ReactiveAuthenticationUtils.getAuthentication(), result).block();
+		assertThat(decision).isNotNull();
+		assertThat(decision.isGranted()).isTrue();
+	}
+
+	@Test
+	public void checkDoSomethingListWhenReturnObjectNotContainsGrantThenDeniedDecision() throws Exception {
+		List<String> list = Collections.singletonList("deny");
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"doSomethingList", new Class[] { List.class }, new Object[] { list });
+		MethodInvocationResult result = new MethodInvocationResult(methodInvocation, list);
+		PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager();
+		AuthorizationDecision decision = manager.check(ReactiveAuthenticationUtils.getAuthentication(), result).block();
+		assertThat(decision).isNotNull();
+		assertThat(decision.isGranted()).isFalse();
+	}
+
+	@Test
+	public void checkRequiresAdminWhenClassAnnotationsThenMethodAnnotationsTakePrecedence() throws Exception {
+		Mono<Authentication> authentication = Mono
+				.just(new TestingAuthenticationToken("user", "password", "ROLE_USER"));
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+				ClassLevelAnnotations.class, "securedAdmin");
+		MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null);
+		PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager();
+		AuthorizationDecision decision = manager.check(authentication, result).block();
+		assertThat(decision).isNotNull();
+		assertThat(decision.isGranted()).isFalse();
+		authentication = Mono.just(new TestingAuthenticationToken("user", "password", "ROLE_ADMIN"));
+		decision = manager.check(authentication, result).block();
+		assertThat(decision).isNotNull();
+		assertThat(decision.isGranted()).isTrue();
+	}
+
+	@Test
+	public void checkRequiresUserWhenClassAnnotationsThenApplies() throws Exception {
+		Mono<Authentication> authentication = Mono
+				.just(new TestingAuthenticationToken("user", "password", "ROLE_USER"));
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+				ClassLevelAnnotations.class, "securedUser");
+		MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null);
+		PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager();
+		AuthorizationDecision decision = manager.check(authentication, result).block();
+		assertThat(decision).isNotNull();
+		assertThat(decision.isGranted()).isTrue();
+		authentication = Mono.just(new TestingAuthenticationToken("user", "password", "ROLE_ADMIN"));
+		decision = manager.check(authentication, result).block();
+		assertThat(decision).isNotNull();
+		assertThat(decision.isGranted()).isFalse();
+	}
+
+	@Test
+	public void checkInheritedAnnotationsWhenDuplicatedThenAnnotationConfigurationException() throws Exception {
+		Mono<Authentication> authentication = Mono
+				.just(new TestingAuthenticationToken("user", "password", "ROLE_USER"));
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"inheritedAnnotations");
+		MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null);
+		PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager();
+		assertThatExceptionOfType(AnnotationConfigurationException.class)
+				.isThrownBy(() -> manager.check(authentication, result));
+	}
+
+	@Test
+	public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationException() throws Exception {
+		Mono<Authentication> authentication = Mono
+				.just(new TestingAuthenticationToken("user", "password", "ROLE_USER"));
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+				ClassLevelAnnotations.class, "inheritedAnnotations");
+		MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null);
+		PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager();
+		assertThatExceptionOfType(AnnotationConfigurationException.class)
+				.isThrownBy(() -> manager.check(authentication, result));
+	}
+
+	public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
+
+		public void doSomething() {
+
+		}
+
+		@PostAuthorize("#s == 'grant'")
+		public String doSomethingString(String s) {
+			return s;
+		}
+
+		@PostAuthorize("returnObject.contains('grant')")
+		public List<String> doSomethingList(List<String> list) {
+			return list;
+		}
+
+		@Override
+		public void inheritedAnnotations() {
+
+		}
+
+	}
+
+	@PostAuthorize("hasRole('USER')")
+	public static class ClassLevelAnnotations implements InterfaceAnnotationsThree {
+
+		@PostAuthorize("hasRole('ADMIN')")
+		public void securedAdmin() {
+
+		}
+
+		public void securedUser() {
+
+		}
+
+		@Override
+		@PostAuthorize("hasRole('ADMIN')")
+		public void inheritedAnnotations() {
+
+		}
+
+	}
+
+	public interface InterfaceAnnotationsOne {
+
+		@PostAuthorize("hasRole('ADMIN')")
+		void inheritedAnnotations();
+
+	}
+
+	public interface InterfaceAnnotationsTwo {
+
+		@PostAuthorize("hasRole('USER')")
+		void inheritedAnnotations();
+
+	}
+
+	public interface InterfaceAnnotationsThree {
+
+		@MyPostAuthorize
+		void inheritedAnnotations();
+
+	}
+
+	@Retention(RetentionPolicy.RUNTIME)
+	@PostAuthorize("hasRole('USER')")
+	public @interface MyPostAuthorize {
+
+	}
+
+}

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

@@ -67,7 +67,7 @@ public class PostFilterAuthorizationMethodInterceptorTests {
 		MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
 		PostFilterAuthorizationMethodInterceptor advice = new PostFilterAuthorizationMethodInterceptor();
 		advice.setExpressionHandler(expressionHandler);
-		assertThat(advice).extracting("expressionHandler").isEqualTo(expressionHandler);
+		assertThat(advice).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler);
 	}
 
 	@Test

+ 191 - 0
core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptorTests.java

@@ -0,0 +1,191 @@
+/*
+ * Copyright 2002-2022 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.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.springframework.core.annotation.AnnotationConfigurationException;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.intercept.method.MockMethodInvocation;
+import org.springframework.security.access.prepost.PostFilter;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link PostFilterAuthorizationReactiveMethodInterceptor}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class PostFilterAuthorizationReactiveMethodInterceptorTests {
+
+	@Test
+	public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() {
+		MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+		PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor(
+				expressionHandler);
+		assertThat(interceptor).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler);
+	}
+
+	@Test
+	public void setExpressionHandlerWhenNullThenException() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> new PostFilterAuthorizationReactiveMethodInterceptor(null))
+				.withMessage("expressionHandler cannot be null");
+	}
+
+	@Test
+	public void methodMatcherWhenMethodHasNotPostFilterAnnotationThenNotMatches() throws Exception {
+		PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor();
+		assertThat(interceptor.getPointcut().getMethodMatcher()
+				.matches(NoPostFilterClass.class.getMethod("doSomething"), NoPostFilterClass.class)).isFalse();
+	}
+
+	@Test
+	public void methodMatcherWhenMethodHasPostFilterAnnotationThenMatches() throws Exception {
+		PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor();
+		assertThat(interceptor.getPointcut().getMethodMatcher()
+				.matches(TestClass.class.getMethod("doSomethingFlux", Flux.class), TestClass.class)).isTrue();
+	}
+
+	@Test
+	public void invokeWhenMonoThenFilteredMono() throws Throwable {
+		Mono<String> mono = Mono.just("bob");
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"doSomethingMono", new Class[] { Mono.class }, new Object[] { mono }) {
+			@Override
+			public Object proceed() {
+				return mono;
+			}
+		};
+		PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor();
+		Object result = interceptor.invoke(methodInvocation);
+		assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)).extracting(Mono::block).isNull();
+	}
+
+	@Test
+	public void invokeWhenFluxThenFilteredFlux() throws Throwable {
+		Flux<String> flux = Flux.just("john", "bob");
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"doSomethingFluxClassLevel", new Class[] { Flux.class }, new Object[] { flux }) {
+			@Override
+			public Object proceed() {
+				return flux;
+			}
+		};
+		PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor();
+		Object result = interceptor.invoke(methodInvocation);
+		assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Flux.class)).extracting(Flux::collectList)
+				.extracting(Mono::block, InstanceOfAssertFactories.list(String.class)).containsOnly("john");
+	}
+
+	@Test
+	public void checkInheritedAnnotationsWhenDuplicatedThenAnnotationConfigurationException() throws Exception {
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"inheritedAnnotations");
+		PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor();
+		assertThatExceptionOfType(AnnotationConfigurationException.class)
+				.isThrownBy(() -> interceptor.invoke(methodInvocation));
+	}
+
+	@Test
+	public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationException() throws Exception {
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new ConflictingAnnotations(),
+				ConflictingAnnotations.class, "inheritedAnnotations");
+		PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor();
+		assertThatExceptionOfType(AnnotationConfigurationException.class)
+				.isThrownBy(() -> interceptor.invoke(methodInvocation));
+	}
+
+	@PostFilter("filterObject == 'john'")
+	public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
+
+		@PostFilter("filterObject == 'john'")
+		public Flux<String> doSomethingFlux(Flux<String> flux) {
+			return flux;
+		}
+
+		public Flux<String> doSomethingFluxClassLevel(Flux<String> flux) {
+			return flux;
+		}
+
+		@PostFilter("filterObject == 'john'")
+		public Mono<String> doSomethingMono(Mono<String> mono) {
+			return mono;
+		}
+
+		@Override
+		public void inheritedAnnotations() {
+
+		}
+
+	}
+
+	public static class NoPostFilterClass {
+
+		public void doSomething() {
+
+		}
+
+	}
+
+	public static class ConflictingAnnotations implements InterfaceAnnotationsThree {
+
+		@Override
+		@PostFilter("filterObject == 'jack'")
+		public void inheritedAnnotations() {
+
+		}
+
+	}
+
+	public interface InterfaceAnnotationsOne {
+
+		@PostFilter("filterObject == 'jim'")
+		void inheritedAnnotations();
+
+	}
+
+	public interface InterfaceAnnotationsTwo {
+
+		@PostFilter("filterObject == 'jane'")
+		void inheritedAnnotations();
+
+	}
+
+	public interface InterfaceAnnotationsThree {
+
+		@MyPostFilter
+		void inheritedAnnotations();
+
+	}
+
+	@Retention(RetentionPolicy.RUNTIME)
+	@PostFilter("filterObject == 'john'")
+	public @interface MyPostFilter {
+
+	}
+
+}

+ 2 - 2
core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -49,7 +49,7 @@ public class PreAuthorizeAuthorizationManagerTests {
 		MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
 		PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
 		manager.setExpressionHandler(expressionHandler);
-		assertThat(manager).extracting("expressionHandler").isEqualTo(expressionHandler);
+		assertThat(manager).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler);
 	}
 
 	@Test

+ 210 - 0
core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManagerTests.java

@@ -0,0 +1,210 @@
+/*
+ * Copyright 2002-2022 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.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Mono;
+
+import org.springframework.core.annotation.AnnotationConfigurationException;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.intercept.method.MockMethodInvocation;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.core.Authentication;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link PreAuthorizeReactiveAuthorizationManager}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class PreAuthorizeReactiveAuthorizationManagerTests {
+
+	@Test
+	public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() {
+		MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+		PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(
+				expressionHandler);
+		assertThat(manager).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler);
+	}
+
+	@Test
+	public void setExpressionHandlerWhenNullThenException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> new PreAuthorizeReactiveAuthorizationManager(null))
+				.withMessage("expressionHandler cannot be null");
+	}
+
+	@Test
+	public void checkDoSomethingWhenNoPostAuthorizeAnnotationThenNullDecision() throws Exception {
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"doSomething", new Class[] {}, new Object[] {});
+		PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager();
+		AuthorizationDecision decision = manager
+				.check(ReactiveAuthenticationUtils.getAuthentication(), methodInvocation).block();
+		assertThat(decision).isNull();
+	}
+
+	@Test
+	public void checkDoSomethingStringWhenArgIsGrantThenGrantedDecision() throws Exception {
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"doSomethingString", new Class[] { String.class }, new Object[] { "grant" });
+		PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager();
+		AuthorizationDecision decision = manager
+				.check(ReactiveAuthenticationUtils.getAuthentication(), methodInvocation).block();
+		assertThat(decision).isNotNull();
+		assertThat(decision.isGranted()).isTrue();
+	}
+
+	@Test
+	public void checkDoSomethingStringWhenArgIsNotGrantThenDeniedDecision() throws Exception {
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"doSomethingString", new Class[] { String.class }, new Object[] { "deny" });
+		PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager();
+		AuthorizationDecision decision = manager
+				.check(ReactiveAuthenticationUtils.getAuthentication(), methodInvocation).block();
+		assertThat(decision).isNotNull();
+		assertThat(decision.isGranted()).isFalse();
+	}
+
+	@Test
+	public void checkRequiresAdminWhenClassAnnotationsThenMethodAnnotationsTakePrecedence() throws Exception {
+		Mono<Authentication> authentication = Mono
+				.just(new TestingAuthenticationToken("user", "password", "ROLE_USER"));
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+				ClassLevelAnnotations.class, "securedAdmin");
+		PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager();
+		AuthorizationDecision decision = manager.check(authentication, methodInvocation).block();
+		assertThat(decision).isNotNull();
+		assertThat(decision.isGranted()).isFalse();
+		authentication = Mono.just(new TestingAuthenticationToken("user", "password", "ROLE_ADMIN"));
+		decision = manager.check(authentication, methodInvocation).block();
+		assertThat(decision).isNotNull();
+		assertThat(decision.isGranted()).isTrue();
+	}
+
+	@Test
+	public void checkRequiresUserWhenClassAnnotationsThenApplies() throws Exception {
+		Mono<Authentication> authentication = Mono
+				.just(new TestingAuthenticationToken("user", "password", "ROLE_USER"));
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+				ClassLevelAnnotations.class, "securedUser");
+		PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager();
+		AuthorizationDecision decision = manager.check(authentication, methodInvocation).block();
+		assertThat(decision).isNotNull();
+		assertThat(decision.isGranted()).isTrue();
+		authentication = Mono.just(new TestingAuthenticationToken("user", "password", "ROLE_ADMIN"));
+		decision = manager.check(authentication, methodInvocation).block();
+		assertThat(decision).isNotNull();
+		assertThat(decision.isGranted()).isFalse();
+	}
+
+	@Test
+	public void checkInheritedAnnotationsWhenDuplicatedThenAnnotationConfigurationException() throws Exception {
+		Mono<Authentication> authentication = Mono
+				.just(new TestingAuthenticationToken("user", "password", "ROLE_USER"));
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"inheritedAnnotations");
+		PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager();
+		assertThatExceptionOfType(AnnotationConfigurationException.class)
+				.isThrownBy(() -> manager.check(authentication, methodInvocation));
+	}
+
+	@Test
+	public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationException() throws Exception {
+		Mono<Authentication> authentication = Mono
+				.just(new TestingAuthenticationToken("user", "password", "ROLE_USER"));
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+				ClassLevelAnnotations.class, "inheritedAnnotations");
+		PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager();
+		assertThatExceptionOfType(AnnotationConfigurationException.class)
+				.isThrownBy(() -> manager.check(authentication, methodInvocation));
+	}
+
+	public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
+
+		public void doSomething() {
+
+		}
+
+		@PreAuthorize("#s == 'grant'")
+		public String doSomethingString(String s) {
+			return s;
+		}
+
+		@Override
+		public void inheritedAnnotations() {
+
+		}
+
+	}
+
+	@PreAuthorize("hasRole('USER')")
+	public static class ClassLevelAnnotations implements InterfaceAnnotationsThree {
+
+		@PreAuthorize("hasRole('ADMIN')")
+		public void securedAdmin() {
+
+		}
+
+		public void securedUser() {
+
+		}
+
+		@Override
+		@PreAuthorize("hasRole('ADMIN')")
+		public void inheritedAnnotations() {
+
+		}
+
+	}
+
+	public interface InterfaceAnnotationsOne {
+
+		@PreAuthorize("hasRole('ADMIN')")
+		void inheritedAnnotations();
+
+	}
+
+	public interface InterfaceAnnotationsTwo {
+
+		@PreAuthorize("hasRole('USER')")
+		void inheritedAnnotations();
+
+	}
+
+	public interface InterfaceAnnotationsThree {
+
+		@MyPreAuthorize
+		void inheritedAnnotations();
+
+	}
+
+	@Retention(RetentionPolicy.RUNTIME)
+	@PreAuthorize("hasRole('USER')")
+	public @interface MyPreAuthorize {
+
+	}
+
+}

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

@@ -69,7 +69,7 @@ public class PreFilterAuthorizationMethodInterceptorTests {
 		MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
 		PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
 		advice.setExpressionHandler(expressionHandler);
-		assertThat(advice).extracting("expressionHandler").isEqualTo(expressionHandler);
+		assertThat(advice).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler);
 	}
 
 	@Test

+ 236 - 0
core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptorTests.java

@@ -0,0 +1,236 @@
+/*
+ * Copyright 2002-2022 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.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.core.annotation.AnnotationConfigurationException;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.intercept.method.MockMethodInvocation;
+import org.springframework.security.access.prepost.PreFilter;
+import org.springframework.security.core.parameters.DefaultSecurityParameterNameDiscoverer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link PreFilterAuthorizationReactiveMethodInterceptor}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class PreFilterAuthorizationReactiveMethodInterceptorTests {
+
+	@Test
+	public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() {
+		MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+		PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(
+				expressionHandler);
+		assertThat(interceptor).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler);
+	}
+
+	@Test
+	public void setExpressionHandlerWhenNullThenException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> new PreFilterAuthorizationReactiveMethodInterceptor(null))
+				.withMessage("expressionHandler cannot be null");
+	}
+
+	@Test
+	public void setParameterNameDiscovererWhenNotNullThenSetsParameterNameDiscoverer() {
+		ParameterNameDiscoverer parameterNameDiscoverer = new DefaultSecurityParameterNameDiscoverer();
+		PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor();
+		interceptor.setParameterNameDiscoverer(parameterNameDiscoverer);
+		assertThat(interceptor).extracting("parameterNameDiscoverer").isEqualTo(parameterNameDiscoverer);
+	}
+
+	@Test
+	public void setParameterNameDiscovererWhenNullThenException() {
+		PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor();
+		assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setParameterNameDiscoverer(null))
+				.withMessage("parameterNameDiscoverer cannot be null");
+	}
+
+	@Test
+	public void methodMatcherWhenMethodHasNotPreFilterAnnotationThenNotMatches() throws Exception {
+		PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor();
+		assertThat(interceptor.getPointcut().getMethodMatcher().matches(NoPreFilterClass.class.getMethod("doSomething"),
+				NoPreFilterClass.class)).isFalse();
+	}
+
+	@Test
+	public void methodMatcherWhenMethodHasPreFilterAnnotationThenMatches() throws Exception {
+		PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor();
+		assertThat(interceptor.getPointcut().getMethodMatcher()
+				.matches(TestClass.class.getMethod("doSomethingFluxFilterTargetMatch", Flux.class), TestClass.class))
+						.isTrue();
+	}
+
+	@Test
+	public void findFilterTargetWhenNameProvidedAndNotMatchThenException() throws Exception {
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"doSomethingFluxFilterTargetNotMatch", new Class[] { Flux.class }, new Object[] { Flux.empty() });
+		PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor();
+		assertThatIllegalArgumentException().isThrownBy(() -> interceptor.invoke(methodInvocation)).withMessage(
+				"Filter target was null, or no argument with name 'filterTargetNotMatch' found in method.");
+	}
+
+	@Test
+	public void findFilterTargetWhenNameProvidedAndMatchAndNullThenException() throws Exception {
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"doSomethingFluxFilterTargetMatch", new Class[] { Flux.class }, new Object[] { null });
+		PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor();
+		assertThatIllegalArgumentException().isThrownBy(() -> interceptor.invoke(methodInvocation))
+				.withMessage("Filter target was null, or no argument with name 'flux' found in method.");
+	}
+
+	@Test
+	public void findFilterTargetWhenNameNotProvidedAndSingleArgMonoThenFiltersMono() throws Throwable {
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"doSomethingMonoFilterTargetNotProvided", new Class[] { Mono.class },
+				new Object[] { Mono.just("bob") }) {
+			@Override
+			public Object proceed() {
+				return getArguments()[0];
+			}
+		};
+		PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor();
+		Object result = interceptor.invoke(methodInvocation);
+		assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)).extracting(Mono::block).isNull();
+	}
+
+	@Test
+	public void findFilterTargetWhenNameNotProvidedAndSingleArgFluxThenFiltersFlux() throws Throwable {
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"doSomethingFluxFilterTargetNotProvided", new Class[] { Flux.class },
+				new Object[] { Flux.just("john", "bob") }) {
+			@Override
+			public Object proceed() {
+				return getArguments()[0];
+			}
+		};
+		PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor();
+		Object result = interceptor.invoke(methodInvocation);
+		assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Flux.class)).extracting(Flux::collectList)
+				.extracting(Mono::block, InstanceOfAssertFactories.list(String.class)).containsOnly("john");
+	}
+
+	@Test
+	public void checkInheritedAnnotationsWhenDuplicatedThenAnnotationConfigurationException() throws Exception {
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+				"inheritedAnnotations");
+		PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor();
+		assertThatExceptionOfType(AnnotationConfigurationException.class)
+				.isThrownBy(() -> interceptor.invoke(methodInvocation));
+	}
+
+	@Test
+	public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationException() throws Exception {
+		MockMethodInvocation methodInvocation = new MockMethodInvocation(new ConflictingAnnotations(),
+				ConflictingAnnotations.class, "inheritedAnnotations");
+		PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor();
+		assertThatExceptionOfType(AnnotationConfigurationException.class)
+				.isThrownBy(() -> interceptor.invoke(methodInvocation));
+	}
+
+	@PreFilter("filterObject == 'john'")
+	public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
+
+		@PreFilter(value = "filterObject == 'john'", filterTarget = "filterTargetNotMatch")
+		public Flux<String> doSomethingFluxFilterTargetNotMatch(Flux<String> flux) {
+			return flux;
+		}
+
+		@PreFilter(value = "filterObject == 'john'", filterTarget = "flux")
+		public Flux<String> doSomethingFluxFilterTargetMatch(Flux<String> flux) {
+			return flux;
+		}
+
+		@PreFilter("filterObject == 'john'")
+		public Flux<String> doSomethingFluxFilterTargetNotProvided(Flux<String> flux) {
+			return flux;
+		}
+
+		@PreFilter("filterObject == 'john'")
+		public Mono<String> doSomethingMonoFilterTargetNotProvided(Mono<String> mono) {
+			return mono;
+		}
+
+		public Flux<String> doSomethingTwoArgsFilterTargetNotProvided(String s, Flux<String> flux) {
+			return flux;
+		}
+
+		@Override
+		public void inheritedAnnotations() {
+
+		}
+
+	}
+
+	public static class NoPreFilterClass {
+
+		public void doSomething() {
+
+		}
+
+	}
+
+	public static class ConflictingAnnotations implements InterfaceAnnotationsThree {
+
+		@Override
+		@PreFilter("filterObject == 'jack'")
+		public void inheritedAnnotations() {
+
+		}
+
+	}
+
+	public interface InterfaceAnnotationsOne {
+
+		@PreFilter("filterObject == 'jim'")
+		void inheritedAnnotations();
+
+	}
+
+	public interface InterfaceAnnotationsTwo {
+
+		@PreFilter("filterObject == 'jane'")
+		void inheritedAnnotations();
+
+	}
+
+	public interface InterfaceAnnotationsThree {
+
+		@MyPreFilter
+		void inheritedAnnotations();
+
+	}
+
+	@Retention(RetentionPolicy.RUNTIME)
+	@PreFilter("filterObject == 'john'")
+	public @interface MyPreFilter {
+
+	}
+
+}

+ 282 - 1
docs/modules/ROOT/pages/reactive/authorization/method.adoc

@@ -6,10 +6,291 @@ The following example shows how to retrieve the currently logged in user's messa
 
 [NOTE]
 ====
-For this example to work, the return type of the method must be a `org.reactivestreams.Publisher` (that is, a `Mono` or a `Flux`) or the function must be a Kotlin coroutine function.
+For this example to work, the return type of the method must be a `org.reactivestreams.Publisher` (that is, a `Mono` or a `Flux`).
 This is necessary to integrate with Reactor's `Context`.
 ====
 
+[[jc-enable-reactive-method-security-authorization-manager]]
+== EnableReactiveMethodSecurity with AuthorizationManager
+
+In Spring Security 5.8, we can enable annotation-based security using the `@EnableReactiveMethodSecurity(useAuthorizationManager=true)` annotation on any `@Configuration` instance.
+
+This improves upon `@EnableReactiveMethodSecurity` in a number of ways. `@EnableReactiveMethodSecurity(useAuthorizationManager=true)`:
+
+1. Uses the simplified `AuthorizationManager` API instead of metadata sources, config attributes, decision managers, and voters.
+This simplifies reuse and customization.
+2. Supports reactive return types. Note that we are waiting on https://github.com/spring-projects/spring-framework/issues/22462[additional coroutine support from the Spring Framework] before adding coroutine support.
+3. Is built using native Spring AOP, removing abstractions and allowing you to use Spring AOP building blocks to customize
+4. Checks for conflicting annotations to ensure an unambiguous security configuration
+5. Complies with JSR-250
+
+[NOTE]
+====
+For earlier versions, please read about similar support with <<jc-enable-reactive-method-security, @EnableReactiveMethodSecurity>>.
+====
+
+For example, the following would enable Spring Security's `@PreAuthorize` annotation:
+
+.Method Security Configuration
+====
+.Java
+[source,java,role="primary"]
+----
+@EnableReactiveMethodSecurity(useAuthorizationManager=true)
+public class MethodSecurityConfig {
+	// ...
+}
+----
+====
+
+Adding an annotation to a method (on a class or interface) would then limit the access to that method accordingly.
+Spring Security's native annotation support defines a set of attributes for the method.
+These will be passed to the various method interceptors, like `AuthorizationManagerBeforeReactiveMethodInterceptor`, for it to make the actual decision:
+
+.Method Security Annotation Usage
+====
+.Java
+[source,java,role="primary"]
+----
+public interface BankService {
+	@PreAuthorize("hasRole('USER')")
+	Mono<Account> readAccount(Long id);
+
+	@PreAuthorize("hasRole('USER')")
+	Flux<Account> findAccounts();
+
+	@PreAuthorize("@func.apply(#account)")
+	Mono<Account> post(Account account, Double amount);
+}
+----
+====
+
+In this case `hasRole` refers to the method found in `SecurityExpressionRoot` that gets invoked by the SpEL evaluation engine.
+
+`@bean` refers to a custom component you have defined, where `apply` can return `Boolean` or `Mono<Boolean>` to indicate the authorization decision.
+A bean like that might look something like this:
+
+.Method Security Reactive Boolean Expression
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+public Function<Account, Mono<Boolean>> func() {
+    return (account) -> Mono.defer(() -> Mono.just(account.getId().equals(12)));
+}
+----
+====
+
+=== Customizing Authorization
+
+Spring Security's `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter` ship with rich expression-based support.
+
+[[jc-reactive-method-security-custom-granted-authority-defaults]]
+
+Also, for role-based authorization, Spring Security adds a default `ROLE_` prefix, which is uses when evaluating expressions like `hasRole`.
+You can configure the authorization rules to use a different prefix by exposing a `GrantedAuthorityDefaults` bean, like so:
+
+.Custom MethodSecurityExpressionHandler
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+static GrantedAuthorityDefaults grantedAuthorityDefaults() {
+	return new GrantedAuthorityDefaults("MYPREFIX_");
+}
+----
+====
+
+[TIP]
+====
+We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes
+====
+
+[[jc-reactive-method-security-custom-authorization-manager]]
+=== Custom Authorization Managers
+
+Method authorization is a combination of before- and after-method authorization.
+
+[NOTE]
+====
+Before-method authorization is performed before the method is invoked.
+If that authorization denies access, the method is not invoked, and an `AccessDeniedException` is thrown.
+After-method authorization is performed after the method is invoked, but before the method returns to the caller.
+If that authorization denies access, the value is not returned, and an `AccessDeniedException` is thrown
+====
+
+To recreate what adding `@EnableReactiveMethodSecurity(useAuthorizationManager=true)` does by default, you would publish the following configuration:
+
+.Full Pre-post Method Security Configuration
+====
+.Java
+[source,java,role="primary"]
+----
+@Configuration
+class MethodSecurityConfig {
+	@Bean
+	BeanDefinitionRegistryPostProcessor aopConfig() {
+		return AopConfigUtils::registerAutoProxyCreatorIfNecessary;
+	}
+
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor() {
+		return new PreFilterAuthorizationReactiveMethodInterceptor();
+	}
+
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor() {
+		return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize();
+	}
+
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor() {
+		return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize();
+	}
+
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor() {
+		return new PostFilterAuthorizationReactiveMethodInterceptor();
+	}
+}
+----
+====
+
+Notice that Spring Security's method security is built using Spring AOP.
+So, interceptors are invoked based on the order specified.
+This can be customized by calling `setOrder` on the interceptor instances like so:
+
+.Publish Custom Advisor
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+Advisor postFilterAuthorizationMethodInterceptor() {
+	PostFilterAuthorizationMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor();
+	interceptor.setOrder(AuthorizationInterceptorOrders.POST_AUTHORIZE.getOrder() - 1);
+	return interceptor;
+}
+----
+====
+
+You may want to only support `@PreAuthorize` in your application, in which case you can do the following:
+
+.Only @PreAuthorize Configuration
+====
+.Java
+[source,java,role="primary"]
+----
+@Configuration
+class MethodSecurityConfig {
+	@Bean
+	BeanDefinitionRegistryPostProcessor aopConfig() {
+		return AopConfigUtils::registerAutoProxyCreatorIfNecessary;
+	}
+
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	Advisor preAuthorize() {
+		return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
+	}
+}
+----
+====
+
+Or, you may have a custom before-method `ReactiveAuthorizationManager` that you want to add to the list.
+
+In this case, you will need to tell Spring Security both the `ReactiveAuthorizationManager` and to which methods and classes your authorization manager applies.
+
+Thus, you can configure Spring Security to invoke your `ReactiveAuthorizationManager` in between `@PreAuthorize` and `@PostAuthorize` like so:
+
+.Custom Before Advisor
+====
+
+.Java
+[source,java,role="primary"]
+----
+@EnableReactiveMethodSecurity(useAuthorizationManager=true)
+class MethodSecurityConfig {
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	public Advisor customAuthorize() {
+		JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
+		pattern.setPattern("org.mycompany.myapp.service.*");
+		ReactiveAuthorizationManager<MethodInvocation> rule = AuthorityAuthorizationManager.isAuthenticated();
+		AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(pattern, rule);
+		interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
+		return interceptor;
+    }
+}
+----
+====
+
+[TIP]
+====
+You can place your interceptor in between Spring Security method interceptors using the order constants specified in `AuthorizationInterceptorsOrder`.
+====
+
+The same can be done for after-method authorization.
+After-method authorization is generally concerned with analysing the return value to verify access.
+
+For example, you might have a method that confirms that the account requested actually belongs to the logged-in user like so:
+
+.@PostAuthorize example
+====
+.Java
+[source,java,role="primary"]
+----
+public interface BankService {
+
+	@PreAuthorize("hasRole('USER')")
+	@PostAuthorize("returnObject.owner == authentication.name")
+	Mono<Account> readAccount(Long id);
+}
+----
+====
+
+You can supply your own `AuthorizationMethodInterceptor` to customize how access to the return value is evaluated.
+
+For example, if you have your own custom annotation, you can configure it like so:
+
+
+.Custom After Advisor
+====
+.Java
+[source,java,role="primary"]
+----
+@EnableReactiveMethodSecurity(useAuthorizationManager=true)
+class MethodSecurityConfig {
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	public Advisor customAuthorize(ReactiveAuthorizationManager<MethodInvocationResult> rules) {
+		AnnotationMethodMatcher pattern = new AnnotationMethodMatcher(MySecurityAnnotation.class);
+		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(pattern, rules);
+		interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
+		return interceptor;
+	}
+}
+----
+====
+
+and it will be invoked after the `@PostAuthorize` interceptor.
+
+== EnableReactiveMethodSecurity
+
+[WARNING]
+====
+`@EnableReactiveMethodSecurity` also supports Kotlin coroutines, though only to a limited degree.
+When intercepting coroutines, only the first interceptor participates.
+If any other interceptors are present and come after Spring Security's method security interceptor, https://github.com/spring-projects/spring-framework/issues/22462[they will be skipped].
+====
+
 ====
 .Java
 [source,java,role="primary"]