Sfoglia il codice sorgente

ReactiveAuthorizationManager + Reactive Method Security

Closes gh-9401
Evgeniy Cheban 4 anni fa
parent
commit
cbb4f40f0c
38 ha cambiato i file con 3231 aggiunte e 185 eliminazioni
  1. 8 0
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurity.java
  2. 91 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. 17 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/DelegatingReactiveMessageService.java
  6. 478 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableAuthorizationManagerReactiveMethodSecurityTests.java
  7. 4 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMessageService.java
  8. 76 0
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationAfterReactiveMethodInterceptor.java
  9. 59 0
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationBeanFactoryPostProcessor.java
  10. 82 0
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationBeforeReactiveMethodInterceptor.java
  11. 146 0
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java
  12. 140 0
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java
  13. 10 1
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationMethodPointcuts.java
  14. 4 38
      core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java
  15. 68 0
      core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java
  16. 75 0
      core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java
  17. 4 36
      core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java
  18. 146 0
      core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java
  19. 67 0
      core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java
  20. 2 35
      core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java
  21. 76 0
      core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java
  22. 70 0
      core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java
  23. 7 51
      core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java
  24. 211 0
      core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java
  25. 85 0
      core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java
  26. 46 0
      core/src/main/java/org/springframework/security/authorization/method/ReactiveAuthenticationUtils.java
  27. 67 0
      core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java
  28. 42 0
      core/src/main/java/org/springframework/security/authorization/method/ReactiveMethodInvocationUtils.java
  29. 107 0
      core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptorTests.java
  30. 108 0
      core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptorTests.java
  31. 2 2
      core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java
  32. 247 0
      core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManagerTests.java
  33. 1 1
      core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java
  34. 191 0
      core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptorTests.java
  35. 2 2
      core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java
  36. 211 0
      core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManagerTests.java
  37. 1 1
      core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java
  38. 237 0
      core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptorTests.java

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

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

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

@@ -0,0 +1,91 @@
+/*
+ * 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) {
+		PreFilterAuthorizationReactiveMethodInterceptor preFilter = new PreFilterAuthorizationReactiveMethodInterceptor();
+		preFilter.setExpressionHandler(expressionHandler);
+		return preFilter;
+	}
+
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor(
+			MethodSecurityExpressionHandler expressionHandler) {
+		PreAuthorizeReactiveAuthorizationManager authorizationManager = new PreAuthorizeReactiveAuthorizationManager();
+		authorizationManager.setExpressionHandler(expressionHandler);
+		return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(authorizationManager);
+	}
+
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor(
+			MethodSecurityExpressionHandler expressionHandler) {
+		PostFilterAuthorizationReactiveMethodInterceptor postFilter = new PostFilterAuthorizationReactiveMethodInterceptor();
+		postFilter.setExpressionHandler(expressionHandler);
+		return postFilter;
+	}
+
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor(
+			MethodSecurityExpressionHandler expressionHandler) {
+		PostAuthorizeReactiveAuthorizationManager authorizationManager = new PostAuthorizeReactiveAuthorizationManager();
+		authorizationManager.setExpressionHandler(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.authorizationManager()) {
+			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());
 	}

+ 17 - 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,15 @@ 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;
+	}
+
 	@Override
 	public Publisher<String> publisherFindById(long id) {
 		return this.delegate.publisherFindById(id);

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

@@ -0,0 +1,478 @@
+/*
+ * 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#authorizationManager()} 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 (i.e. Mono / Flux) or the function must be a Kotlin coroutine "
+						+ "function 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();
+	}
+
+	// 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(authorizationManager = true)
+	static class Config {
+
+		ReactiveMessageService delegate = mock(ReactiveMessageService.class);
+
+		@Bean
+		DelegatingReactiveMessageService defaultMessageService() {
+			return new DelegatingReactiveMessageService(this.delegate);
+		}
+
+		@Bean
+		Authz authz() {
+			return new Authz();
+		}
+
+	}
+
+}

+ 4 - 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,8 @@ public interface ReactiveMessageService {
 
 	Flux<String> fluxPostAuthorizeBeanFindById(long id);
 
+	Flux<String> fluxManyAnnotations(Flux<String> flux);
+
 	Publisher<String> publisherFindById(long id);
 
 	Publisher<String> publisherPreAuthorizeHasRoleFindById(long id);

+ 76 - 0
core/src/main/java/org/springframework/security/authorization/method/AuthorizationAfterReactiveMethodInterceptor.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 org.aopalliance.aop.Advice;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+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;
+
+/**
+ * A {@link MethodInterceptor} that wraps a {@link Mono} or a {@link Flux} using
+ * <code>deffer</code> call.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.8
+ */
+final class AuthorizationAfterReactiveMethodInterceptor
+		implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+
+	private final Pointcut pointcut = AuthorizationMethodPointcuts.forAllAnnotations();
+
+	private final int order = AuthorizationInterceptorsOrder.LAST.getOrder();
+
+	@Override
+	public Object invoke(MethodInvocation mi) throws Throwable {
+		Method method = mi.getMethod();
+		Class<?> returnType = method.getReturnType();
+		if (Mono.class.isAssignableFrom(returnType)) {
+			return Mono.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
+		}
+		return Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
+	}
+
+	@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;
+	}
+
+}

+ 59 - 0
core/src/main/java/org/springframework/security/authorization/method/AuthorizationBeanFactoryPostProcessor.java

@@ -0,0 +1,59 @@
+/*
+ * 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.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+
+/**
+ * Adds {@link AuthorizationBeforeReactiveMethodInterceptor} and
+ * {@link AuthorizationAfterReactiveMethodInterceptor} bean definitions to the
+ * {@link BeanDefinitionRegistry} if they have not already been added.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.8
+ */
+final class AuthorizationBeanFactoryPostProcessor implements BeanDefinitionRegistryPostProcessor {
+
+	private static final String BEFORE_INTERCEPTOR_BEAN_NAME = "org.springframework.security.authorization.method.authorizationBeforeReactiveMethodInterceptor";
+
+	private static final String AFTER_INTERCEPTOR_BEAN_NAME = "org.springframework.security.authorization.method.authorizationAfterReactiveMethodInterceptor";
+
+	@Override
+	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
+		if (!registry.containsBeanDefinition(BEFORE_INTERCEPTOR_BEAN_NAME)) {
+			RootBeanDefinition beforeInterceptor = new RootBeanDefinition(
+					AuthorizationBeforeReactiveMethodInterceptor.class);
+			beforeInterceptor.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
+			registry.registerBeanDefinition(BEFORE_INTERCEPTOR_BEAN_NAME, beforeInterceptor);
+		}
+		if (!registry.containsBeanDefinition(AFTER_INTERCEPTOR_BEAN_NAME)) {
+			RootBeanDefinition afterInterceptor = new RootBeanDefinition(
+					AuthorizationAfterReactiveMethodInterceptor.class);
+			afterInterceptor.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
+			registry.registerBeanDefinition(AFTER_INTERCEPTOR_BEAN_NAME, afterInterceptor);
+		}
+	}
+
+	@Override
+	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
+	}
+
+}

+ 82 - 0
core/src/main/java/org/springframework/security/authorization/method/AuthorizationBeforeReactiveMethodInterceptor.java

@@ -0,0 +1,82 @@
+/*
+ * 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 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.util.Assert;
+
+/**
+ * A {@link MethodInterceptor} which validates and transforms the return type for methods
+ * that return a {@link Publisher}.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.8
+ */
+final class AuthorizationBeforeReactiveMethodInterceptor
+		implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+
+	private final Pointcut pointcut = AuthorizationMethodPointcuts.forAllAnnotations();
+
+	private final int order = AuthorizationInterceptorsOrder.FIRST.getOrder();
+
+	@Override
+	public Object invoke(MethodInvocation mi) throws Throwable {
+		Method method = mi.getMethod();
+		Class<?> returnType = method.getReturnType();
+		Assert.state(Publisher.class.isAssignableFrom(returnType),
+				() -> "The returnType " + returnType + " on " + method
+						+ " must return an instance of org.reactivestreams.Publisher "
+						+ "(i.e. Mono / Flux) or the function must be a Kotlin coroutine "
+						+ "function in order to support Reactor Context");
+		Publisher<?> publisher = ReactiveMethodInvocationUtils.proceed(mi);
+		ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(returnType);
+		return (adapter != null) ? adapter.fromPublisher(publisher) : publisher;
+	}
+
+	@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;
+	}
+
+}

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

@@ -0,0 +1,146 @@
+/*
+ * 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.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.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
+import org.springframework.core.Ordered;
+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, BeanDefinitionRegistryPostProcessor {
+
+	private final AuthorizationBeanFactoryPostProcessor beanFactoryPostProcessor = new AuthorizationBeanFactoryPostProcessor();
+
+	private final Pointcut pointcut;
+
+	private final ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager;
+
+	private int order = AuthorizationInterceptorsOrder.POST_AUTHORIZE.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) {
+		return new AuthorizationManagerAfterReactiveMethodInterceptor(
+				AuthorizationMethodPointcuts.forAnnotations(PostAuthorize.class), authorizationManager);
+	}
+
+	/**
+	 * 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 {
+		Publisher<?> publisher = ReactiveMethodInvocationUtils.proceed(mi);
+		Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
+		if (publisher instanceof Mono<?>) {
+			Mono<?> mono = (Mono<?>) publisher;
+			return mono.flatMap((result) -> postAuthorize(authentication, mi, result));
+		}
+		return Flux.from(publisher).flatMap((result) -> postAuthorize(authentication, mi, result));
+	}
+
+	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;
+	}
+
+	@Override
+	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
+		this.beanFactoryPostProcessor.postProcessBeanDefinitionRegistry(registry);
+	}
+
+	@Override
+	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
+		this.beanFactoryPostProcessor.postProcessBeanFactory(beanFactory);
+	}
+
+}

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

@@ -0,0 +1,140 @@
+/*
+ * 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.aop.Advice;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Mono;
+
+import org.springframework.aop.Pointcut;
+import org.springframework.aop.PointcutAdvisor;
+import org.springframework.aop.framework.AopInfrastructureBean;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
+import org.springframework.core.Ordered;
+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
+ * @since 5.8
+ */
+public final class AuthorizationManagerBeforeReactiveMethodInterceptor implements Ordered, MethodInterceptor,
+		PointcutAdvisor, AopInfrastructureBean, BeanDefinitionRegistryPostProcessor {
+
+	private final AuthorizationBeanFactoryPostProcessor beanFactoryPostProcessor = new AuthorizationBeanFactoryPostProcessor();
+
+	private final Pointcut pointcut;
+
+	private final ReactiveAuthorizationManager<MethodInvocation> authorizationManager;
+
+	private int order = AuthorizationInterceptorsOrder.PRE_AUTHORIZE.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) {
+		return new AuthorizationManagerBeforeReactiveMethodInterceptor(
+				AuthorizationMethodPointcuts.forAnnotations(PreAuthorize.class), authorizationManager);
+	}
+
+	/**
+	 * 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 {
+		Publisher<?> publisher = ReactiveMethodInvocationUtils.proceed(mi);
+		Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
+		Mono<Void> preAuthorize = this.authorizationManager.verify(authentication, mi);
+		if (publisher instanceof Mono<?>) {
+			return preAuthorize.then((Mono<?>) publisher);
+		}
+		return preAuthorize.thenMany(publisher);
+	}
+
+	@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;
+	}
+
+	@Override
+	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
+		this.beanFactoryPostProcessor.postProcessBeanDefinitionRegistry(registry);
+	}
+
+	@Override
+	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
+		this.beanFactoryPostProcessor.postProcessBeanFactory(beanFactory);
+	}
+
+}

+ 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;

+ 4 - 38
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
@@ -47,15 +41,12 @@ public final class PostAuthorizeAuthorizationManager implements AuthorizationMan
 
 	private final PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry();
 
-	private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
-
 	/**
 	 * 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.setExpressionHandler(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);
-		}
-
-	}
-
 }

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

@@ -0,0 +1,68 @@
+/*
+ * 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 MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+
+	MethodSecurityExpressionHandler getExpressionHandler() {
+		return this.expressionHandler;
+	}
+
+	void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
+		Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+		this.expressionHandler = 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);
+	}
+
+}

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

@@ -0,0 +1,75 @@
+/*
+ * 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.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;
+
+/**
+ * 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 = new PostAuthorizeExpressionAttributeRegistry();
+
+	/**
+	 * Sets the {@link MethodSecurityExpressionHandler}.
+	 * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use
+	 */
+	public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
+		this.registry.setExpressionHandler(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
+	}
+
+}

+ 4 - 36
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
@@ -61,8 +55,6 @@ public final class PostFilterAuthorizationMethodInterceptor
 
 	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.setExpressionHandler(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);
-		}
-
-	}
-
 }

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

@@ -0,0 +1,146 @@
+/*
+ * 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.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.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
+import org.springframework.core.Ordered;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations;
+import org.springframework.security.access.prepost.PostFilter;
+
+/**
+ * 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, BeanDefinitionRegistryPostProcessor {
+
+	private final AuthorizationBeanFactoryPostProcessor beanFactoryPostProcessor = new AuthorizationBeanFactoryPostProcessor();
+
+	private final PostFilterExpressionAttributeRegistry registry = new PostFilterExpressionAttributeRegistry();
+
+	private final Pointcut pointcut;
+
+	private int order = AuthorizationInterceptorsOrder.POST_FILTER.getOrder();
+
+	/**
+	 * Creates an instance.
+	 */
+	public PostFilterAuthorizationReactiveMethodInterceptor() {
+		this.pointcut = AuthorizationMethodPointcuts.forAnnotations(PostFilter.class);
+	}
+
+	/**
+	 * Sets the {@link MethodSecurityExpressionHandler}.
+	 * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use
+	 */
+	public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
+		this.registry.setExpressionHandler(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 {
+		Publisher<?> publisher = ReactiveMethodInvocationUtils.proceed(mi);
+		ExpressionAttribute attribute = this.registry.getAttribute(mi);
+		if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
+			return publisher;
+		}
+		Mono<EvaluationContext> toInvoke = ReactiveAuthenticationUtils.getAuthentication()
+				.map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi));
+		if (publisher instanceof Mono<?>) {
+			return toInvoke.flatMap((ctx) -> filterMono((Mono<?>) publisher, ctx, attribute));
+		}
+		return toInvoke.flatMapMany((ctx) -> filterPublisher(publisher, ctx, attribute));
+	}
+
+	private Mono<?> filterMono(Mono<?> mono, EvaluationContext ctx, ExpressionAttribute attribute) {
+		return mono.doOnNext((result) -> setFilterObject(ctx, result))
+				.flatMap((result) -> postFilter(ctx, result, attribute));
+	}
+
+	private Flux<?> filterPublisher(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;
+	}
+
+	@Override
+	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
+		this.beanFactoryPostProcessor.postProcessBeanDefinitionRegistry(registry);
+	}
+
+	@Override
+	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
+		this.beanFactoryPostProcessor.postProcessBeanFactory(beanFactory);
+	}
+
+}

+ 67 - 0
core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.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 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 MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+
+	MethodSecurityExpressionHandler getExpressionHandler() {
+		return this.expressionHandler;
+	}
+
+	void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
+		Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+		this.expressionHandler = 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);
+	}
+
+}

+ 2 - 35
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
@@ -47,15 +41,12 @@ public final class PreAuthorizeAuthorizationManager implements AuthorizationMana
 
 	private final PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry();
 
-	private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
-
 	/**
 	 * 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.setExpressionHandler(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 MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+
+	/**
+	 * Returns the {@link MethodSecurityExpressionHandler}.
+	 * @return the {@link MethodSecurityExpressionHandler} to use
+	 */
+	MethodSecurityExpressionHandler getExpressionHandler() {
+		return this.expressionHandler;
+	}
+
+	/**
+	 * Sets the {@link MethodSecurityExpressionHandler}.
+	 * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use
+	 */
+	void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
+		Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+		this.expressionHandler = 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);
+	}
+
+}

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

@@ -0,0 +1,70 @@
+/*
+ * 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.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;
+
+/**
+ * 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 = new PreAuthorizeExpressionAttributeRegistry();
+
+	/**
+	 * Sets the {@link MethodSecurityExpressionHandler}.
+	 * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use
+	 */
+	public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
+		this.registry.setExpressionHandler(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
+	}
+
+}

+ 7 - 51
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;
@@ -61,8 +56,6 @@ public final class PreFilterAuthorizationMethodInterceptor
 
 	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.setExpressionHandler(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;
-		}
-
-	}
-
 }

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

@@ -0,0 +1,211 @@
+/*
+ * 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.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
+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.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, BeanDefinitionRegistryPostProcessor {
+
+	private final AuthorizationBeanFactoryPostProcessor beanFactoryPostProcessor = new AuthorizationBeanFactoryPostProcessor();
+
+	private final PreFilterExpressionAttributeRegistry registry = new PreFilterExpressionAttributeRegistry();
+
+	private final Pointcut pointcut = AuthorizationMethodPointcuts.forAnnotations(PreFilter.class);
+
+	private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultSecurityParameterNameDiscoverer();
+
+	private int order = AuthorizationInterceptorsOrder.PRE_FILTER.getOrder();
+
+	/**
+	 * Sets the {@link MethodSecurityExpressionHandler}.
+	 * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use
+	 */
+	public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
+		this.registry.setExpressionHandler(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.<Publisher<?>>proceed(mi);
+		}
+		FilterTarget filterTarget = findFilterTarget(attribute.getFilterTarget(), mi);
+		Mono<EvaluationContext> toInvoke = ReactiveAuthenticationUtils.getAuthentication()
+				.map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi));
+		if (filterTarget.value instanceof Mono<?>) {
+			mi.getArguments()[filterTarget.index] = toInvoke
+					.flatMap((ctx) -> filterMono((Mono<?>) filterTarget.value, attribute.getExpression(), ctx));
+		}
+		else {
+			Flux<?> result = toInvoke
+					.flatMapMany((ctx) -> filterPublisher(filterTarget.value, attribute.getExpression(), ctx));
+			ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance()
+					.getAdapter(filterTarget.value.getClass());
+			mi.getArguments()[filterTarget.index] = (adapter != null) ? adapter.fromPublisher(result) : result;
+		}
+		return ReactiveMethodInvocationUtils.<Publisher<?>>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 Mono<?> filterMono(Mono<?> filterTarget, Expression filterExpression, EvaluationContext ctx) {
+		MethodSecurityExpressionOperations rootObject = (MethodSecurityExpressionOperations) ctx.getRootObject()
+				.getValue();
+		return filterTarget.filterWhen((filterObject) -> {
+			rootObject.setFilterObject(filterObject);
+			return ReactiveExpressionUtils.evaluateAsBoolean(filterExpression, ctx);
+		});
+	}
+
+	private Flux<?> filterPublisher(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;
+	}
+
+	@Override
+	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
+		this.beanFactoryPostProcessor.postProcessBeanDefinitionRegistry(registry);
+	}
+
+	@Override
+	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
+		this.beanFactoryPostProcessor.postProcessBeanFactory(beanFactory);
+	}
+
+	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;
+		}
+
+	}
+
+}

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

@@ -0,0 +1,85 @@
+/*
+ * 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 MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+
+	MethodSecurityExpressionHandler getExpressionHandler() {
+		return this.expressionHandler;
+	}
+
+	void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
+		Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+		this.expressionHandler = 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() {
+	}
+
+}

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

@@ -0,0 +1,107 @@
+/*
+ * 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.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.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 = mock(MethodInvocation.class);
+		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 = mock(MethodInvocation.class);
+		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 = mock(MethodInvocation.class);
+		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());
+	}
+
+}

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

@@ -0,0 +1,108 @@
+/*
+ * 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.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.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 = mock(MethodInvocation.class);
+		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 = mock(MethodInvocation.class);
+		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 = mock(MethodInvocation.class);
+		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));
+	}
+
+}

+ 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

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

@@ -0,0 +1,247 @@
+/*
+ * 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();
+		manager.setExpressionHandler(expressionHandler);
+		assertThat(manager).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler);
+	}
+
+	@Test
+	public void setExpressionHandlerWhenNullThenException() {
+		PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager();
+		assertThatIllegalArgumentException().isThrownBy(() -> manager.setExpressionHandler(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();
+		interceptor.setExpressionHandler(expressionHandler);
+		assertThat(interceptor).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler);
+	}
+
+	@Test
+	public void setExpressionHandlerWhenNullThenException() {
+		PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor();
+		assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setExpressionHandler(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

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

@@ -0,0 +1,211 @@
+/*
+ * 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();
+		manager.setExpressionHandler(expressionHandler);
+		assertThat(manager).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler);
+	}
+
+	@Test
+	public void setExpressionHandlerWhenNullThenException() {
+		PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager();
+		assertThatIllegalArgumentException().isThrownBy(() -> manager.setExpressionHandler(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

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

@@ -0,0 +1,237 @@
+/*
+ * 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();
+		interceptor.setExpressionHandler(expressionHandler);
+		assertThat(interceptor).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler);
+	}
+
+	@Test
+	public void setExpressionHandlerWhenNullThenException() {
+		PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor();
+		assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setExpressionHandler(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 {
+
+	}
+
+}