diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurity.java
new file mode 100644
index 0000000000..19996cd3dc
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurity.java
@@ -0,0 +1,71 @@
+/*
+ *
+ * * Copyright 2002-2017 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
+ * *
+ * * http://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.context.annotation.AdviceMode;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.core.Ordered;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ *
+ * @author Rob Winch
+ * @since 5.0
+ */
+@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
+@Target(value = { java.lang.annotation.ElementType.TYPE })
+@Documented
+@Import({ ReactiveMethodSecuritySelector.class })
+@Configuration
+public @interface EnableReactiveMethodSecurity {
+ /**
+ * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
+ * to standard Java interface-based proxies. The default is {@code false}.
+ * Applicable only if {@link #mode()} is set to {@link AdviceMode#PROXY}.
+ *
Note that setting this attribute to {@code true} will affect all
+ * Spring-managed beans requiring proxying, not just those marked with {@code @Cacheable}.
+ * For example, other beans marked with Spring's {@code @Transactional} annotation will
+ * be upgraded to subclass proxying at the same time. This approach has no negative
+ * impact in practice unless one is explicitly expecting one type of proxy vs another,
+ * e.g. in tests.
+ */
+ boolean proxyTargetClass() default false;
+
+ /**
+ * Indicate how security advice should be applied. The default is
+ * {@link AdviceMode#PROXY}.
+ * @see AdviceMode
+ *
+ * @return the {@link AdviceMode} to use
+ */
+ AdviceMode mode() default AdviceMode.PROXY;
+
+ /**
+ * Indicate the ordering of the execution of the security advisor when multiple
+ * advices are applied at a specific joinpoint. The default is
+ * {@link Ordered#LOWEST_PRECEDENCE}.
+ *
+ * @return the order the security advisor should be applied
+ */
+ int order() default Ordered.LOWEST_PRECEDENCE;
+}
diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java
new file mode 100644
index 0000000000..5c4f1a5b38
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java
@@ -0,0 +1,85 @@
+/*
+ *
+ * * Copyright 2002-2017 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
+ * *
+ * * http://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.config.BeanDefinition;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.ImportAware;
+import org.springframework.context.annotation.Role;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.security.access.expression.method.*;
+import org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor;
+import org.springframework.security.access.method.AbstractMethodSecurityMetadataSource;
+import org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource;
+import org.springframework.security.access.method.PrePostAdviceMethodInterceptor;
+import org.springframework.security.access.prepost.PrePostAnnotationSecurityMetadataSource;
+
+import java.util.Arrays;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+@Configuration
+class ReactiveMethodSecurityConfiguration implements ImportAware {
+ private int advisorOrder;
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ public MethodSecurityMetadataSourceAdvisor methodSecurityInterceptor(AbstractMethodSecurityMetadataSource source) throws Exception {
+ MethodSecurityMetadataSourceAdvisor advisor = new MethodSecurityMetadataSourceAdvisor(
+ "securityMethodInterceptor", source, "methodMetadataSource");
+ advisor.setOrder(advisorOrder);
+ return advisor;
+ }
+
+ @Bean
+ public DelegatingMethodSecurityMetadataSource methodMetadataSource() {
+ ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
+ new DefaultMethodSecurityExpressionHandler());
+ PrePostAnnotationSecurityMetadataSource prePostSource = new PrePostAnnotationSecurityMetadataSource(
+ attributeFactory);
+ return new DelegatingMethodSecurityMetadataSource(Arrays.asList(prePostSource));
+ }
+
+ @Bean
+ public PrePostAdviceMethodInterceptor securityMethodInterceptor(AbstractMethodSecurityMetadataSource source, MethodSecurityExpressionHandler handler) {
+
+ ExpressionBasedPostInvocationAdvice postAdvice = new ExpressionBasedPostInvocationAdvice(
+ handler);
+ ExpressionBasedPreInvocationAdvice preAdvice = new ExpressionBasedPreInvocationAdvice();
+ preAdvice.setExpressionHandler(handler);
+
+ PrePostAdviceMethodInterceptor result = new PrePostAdviceMethodInterceptor(source);
+ result.setPostAdvice(postAdvice);
+ result.setPreAdvice(preAdvice);
+ return result;
+ }
+
+ @Bean
+ public DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler() {
+ return new DefaultMethodSecurityExpressionHandler();
+ }
+
+ @Override
+ public void setImportMetadata(AnnotationMetadata importMetadata) {
+ this.advisorOrder = (int) importMetadata.getAnnotationAttributes(EnableReactiveMethodSecurity.class.getName()).get("order");
+ }
+}
diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java
new file mode 100644
index 0000000000..acb0888d59
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java
@@ -0,0 +1,57 @@
+/*
+ *
+ * * Copyright 2002-2017 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
+ * *
+ * * http://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.cache.annotation.ProxyCachingConfiguration;
+import org.springframework.context.annotation.AdviceMode;
+import org.springframework.context.annotation.AdviceModeImportSelector;
+import org.springframework.context.annotation.AutoProxyRegistrar;
+import org.springframework.lang.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+class ReactiveMethodSecuritySelector extends
+ AdviceModeImportSelector {
+
+ @Override
+ protected String[] selectImports(AdviceMode adviceMode) {
+ switch (adviceMode) {
+ case PROXY:
+ return getProxyImports();
+ default:
+ throw new IllegalStateException("AdviceMode " + adviceMode + " is not supported");
+ }
+ }
+
+ /**
+ * Return the imports to use if the {@link AdviceMode} is set to {@link AdviceMode#PROXY}.
+ * Take care of adding the necessary JSR-107 import if it is available.
+ */
+ private String[] getProxyImports() {
+ List result = new ArrayList<>();
+ result.add(AutoProxyRegistrar.class.getName());
+ result.add(ReactiveMethodSecurityConfiguration.class.getName());
+ return result.toArray(new String[result.size()]);
+ }
+}
diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java
new file mode 100644
index 0000000000..c596b3ca1a
--- /dev/null
+++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java
@@ -0,0 +1,38 @@
+/*
+ *
+ * * Copyright 2002-2017 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
+ * *
+ * * http://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.security.core.Authentication;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+@Component
+public class Authz {
+ public boolean check(long id) {
+ return id % 2 == 0;
+ }
+
+ public boolean check(Authentication authentication, String message) {
+ return message != null &&
+ message.contains(authentication.getName());
+ }
+}
diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/DelegatingReactiveMessageService.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/DelegatingReactiveMessageService.java
new file mode 100644
index 0000000000..8298012d58
--- /dev/null
+++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/DelegatingReactiveMessageService.java
@@ -0,0 +1,131 @@
+/*
+ *
+ * * Copyright 2002-2017 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
+ * *
+ * * http://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.reactivestreams.Publisher;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.security.access.prepost.PreAuthorize;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public class DelegatingReactiveMessageService implements ReactiveMessageService {
+ private final ReactiveMessageService delegate;
+
+ public DelegatingReactiveMessageService(ReactiveMessageService delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Mono monoFindById(long id) {
+ return delegate.monoFindById(id);
+ }
+
+ @Override
+ @PreAuthorize("hasRole('ADMIN')")
+ public Mono monoPreAuthorizeHasRoleFindById(
+ long id) {
+ return delegate.monoPreAuthorizeHasRoleFindById(id);
+ }
+
+ @Override
+ @PostAuthorize("returnObject?.contains(authentication?.name)")
+ public Mono monoPostAuthorizeFindById(
+ long id) {
+ return delegate.monoPostAuthorizeFindById(id);
+ }
+
+ @Override
+ @PreAuthorize("@authz.check(#id)")
+ public Mono monoPreAuthorizeBeanFindById(
+ long id) {
+ return delegate.monoPreAuthorizeBeanFindById(id);
+ }
+
+ @Override
+ @PostAuthorize("@authz.check(authentication, returnObject)")
+ public Mono monoPostAuthorizeBeanFindById(
+ long id) {
+ return delegate.monoPostAuthorizeBeanFindById(id);
+ }
+
+ @Override
+ public Flux fluxFindById(long id) {
+ return delegate.fluxFindById(id);
+ }
+
+ @Override
+ @PreAuthorize("hasRole('ADMIN')")
+ public Flux fluxPreAuthorizeHasRoleFindById(
+ long id) {
+ return delegate.fluxPreAuthorizeHasRoleFindById(id);
+ }
+
+ @Override
+ @PostAuthorize("returnObject?.contains(authentication?.name)")
+ public Flux fluxPostAuthorizeFindById(
+ long id) {
+ return delegate.fluxPostAuthorizeFindById(id);
+ }
+
+ @Override
+ @PreAuthorize("@authz.check(#id)")
+ public Flux fluxPreAuthorizeBeanFindById(
+ long id) {
+ return delegate.fluxPreAuthorizeBeanFindById(id);
+ }
+
+ @Override
+ @PostAuthorize("@authz.check(authentication, returnObject)")
+ public Flux fluxPostAuthorizeBeanFindById(
+ long id) {
+ return delegate.fluxPostAuthorizeBeanFindById(id);
+ }
+
+ @Override
+ public Publisher publisherFindById(long id) {
+ return delegate.publisherFindById(id);
+ }
+
+ @Override
+ @PreAuthorize("hasRole('ADMIN')")
+ public Publisher publisherPreAuthorizeHasRoleFindById(
+ long id) {
+ return delegate.publisherPreAuthorizeHasRoleFindById(id);
+ }
+
+ @Override
+ @PostAuthorize("returnObject?.contains(authentication?.name)")
+ public Publisher publisherPostAuthorizeFindById(
+ long id) {
+ return delegate.publisherPostAuthorizeFindById(id);
+ }
+
+ @Override
+ @PreAuthorize("@authz.check(#id)")
+ public Publisher publisherPreAuthorizeBeanFindById(
+ long id) {
+ return delegate.publisherPreAuthorizeBeanFindById(id);
+ }
+
+ @Override
+ @PostAuthorize("@authz.check(authentication, returnObject)")
+ public Publisher publisherPostAuthorizeBeanFindById(
+ long id) {
+ return delegate.publisherPostAuthorizeBeanFindById(id);
+ }
+}
diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurityTests.java
new file mode 100644
index 0000000000..ba611b6ab1
--- /dev/null
+++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurityTests.java
@@ -0,0 +1,593 @@
+/*
+ *
+ * * Copyright 2002-2017 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
+ * *
+ * * http://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.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.reactivestreams.Publisher;
+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.Authentication;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+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 java.util.function.Function;
+
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+@RunWith(SpringRunner.class)
+@ContextConfiguration
+public class EnableReactiveMethodSecurityTests {
+ @Autowired ReactiveMessageService messageService;
+ ReactiveMessageService delegate;
+ TestPublisher result = TestPublisher.create();
+
+ Function withAdmin = context -> context.put(Authentication.class, Mono
+ .just(new TestingAuthenticationToken("admin","password","ROLE_USER", "ROLE_ADMIN")));
+ Function withUser = context -> context.put(Authentication.class, Mono
+ .just(new TestingAuthenticationToken("user","password","ROLE_USER")));
+
+ @After
+ public void cleanup() {
+ reset(delegate);
+ }
+
+ @Autowired
+ public void setConfig(Config config) {
+ this.delegate = config.delegate;
+ }
+
+ @Test
+ public void monoWhenPermitAllThenAopDoesNotSubscribe() {
+ when(this.delegate.monoFindById(1L)).thenReturn(Mono.from(result));
+
+ this.delegate.monoFindById(1L);
+
+ result.assertNoSubscribers();
+ }
+
+ @Test
+ public void monoWhenPermitAllThenSuccess() {
+ when(this.delegate.monoFindById(1L)).thenReturn(Mono.just("success"));
+
+ StepVerifier.create(this.delegate.monoFindById(1L))
+ .expectNext("success")
+ .verifyComplete();
+ }
+
+ @Test
+ public void monoPreAuthorizeHasRoleWhenGrantedThenSuccess() {
+ when(this.delegate.monoPreAuthorizeHasRoleFindById(1L)).thenReturn(Mono.just("result"));
+
+ Mono findById = this.messageService.monoPreAuthorizeHasRoleFindById(1L)
+ .contextStart(withAdmin);
+ StepVerifier
+ .create(findById)
+ .expectNext("result")
+ .verifyComplete();
+ }
+
+ @Test
+ public void monoPreAuthorizeHasRoleWhenNoAuthenticationThenDenied() {
+ when(this.delegate.monoPreAuthorizeHasRoleFindById(1L)).thenReturn(Mono.from(result));
+
+ Mono findById = this.messageService.monoPreAuthorizeHasRoleFindById(1L);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+
+ result.assertNoSubscribers();
+ }
+
+ @Test
+ public void monoPreAuthorizeHasRoleWhenNotAuthorizedThenDenied() {
+ when(this.delegate.monoPreAuthorizeHasRoleFindById(1L)).thenReturn(Mono.from(result));
+
+ Mono findById = this.messageService.monoPreAuthorizeHasRoleFindById(1L)
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+
+ result.assertNoSubscribers();
+ }
+
+ @Test
+ public void monoPreAuthorizeBeanWhenGrantedThenSuccess() {
+ when(this.delegate.monoPreAuthorizeBeanFindById(2L)).thenReturn(Mono.just("result"));
+
+ Mono findById = this.messageService.monoPreAuthorizeBeanFindById(2L)
+ .contextStart(withAdmin);
+ StepVerifier
+ .create(findById)
+ .expectNext("result")
+ .verifyComplete();
+ }
+
+ @Test
+ public void monoPreAuthorizeBeanWhenNotAuthenticatedAndGrantedThenSuccess() {
+ when(this.delegate.monoPreAuthorizeBeanFindById(2L)).thenReturn(Mono.just("result"));
+
+ Mono findById = this.messageService.monoPreAuthorizeBeanFindById(2L);
+ StepVerifier
+ .create(findById)
+ .expectNext("result")
+ .verifyComplete();
+ }
+
+ @Test
+ public void monoPreAuthorizeBeanWhenNoAuthenticationThenDenied() {
+ when(this.delegate.monoPreAuthorizeBeanFindById(1L)).thenReturn(Mono.from(result));
+
+ Mono findById = this.messageService.monoPreAuthorizeBeanFindById(1L);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+
+ result.assertNoSubscribers();
+ }
+
+ @Test
+ public void monoPreAuthorizeBeanWhenNotAuthorizedThenDenied() {
+ when(this.delegate.monoPreAuthorizeBeanFindById(1L)).thenReturn(Mono.from(result));
+
+ Mono findById = this.messageService.monoPreAuthorizeBeanFindById(1L)
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+
+ result.assertNoSubscribers();
+ }
+
+ @Test
+ public void monoPostAuthorizeWhenAuthorizedThenSuccess() {
+ when(this.delegate.monoPostAuthorizeFindById(1L)).thenReturn(Mono.just("user"));
+
+ Mono findById = this.messageService.monoPostAuthorizeFindById(1L)
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectNext("user")
+ .verifyComplete();
+ }
+
+ @Test
+ public void monoPostAuthorizeWhenNotAuthorizedThenDenied() {
+ when(this.delegate.monoPostAuthorizeBeanFindById(1L)).thenReturn(Mono.just("not-authorized"));
+
+ Mono findById = this.messageService.monoPostAuthorizeBeanFindById(1L)
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+ }
+
+ @Test
+ public void monoPostAuthorizeWhenBeanAndAuthorizedThenSuccess() {
+ when(this.delegate.monoPostAuthorizeBeanFindById(2L)).thenReturn(Mono.just("user"));
+
+ Mono findById = this.messageService.monoPostAuthorizeBeanFindById(2L)
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectNext("user")
+ .verifyComplete();
+ }
+
+ @Test
+ public void monoPostAuthorizeWhenBeanAndNotAuthenticatedAndAuthorizedThenSuccess() {
+ when(this.delegate.monoPostAuthorizeBeanFindById(2L)).thenReturn(Mono.just("anonymous"));
+
+ Mono findById = this.messageService.monoPostAuthorizeBeanFindById(2L);
+ StepVerifier
+ .create(findById)
+ .expectNext("anonymous")
+ .verifyComplete();
+ }
+
+ @Test
+ public void monoPostAuthorizeWhenBeanAndNotAuthorizedThenDenied() {
+ when(this.delegate.monoPostAuthorizeBeanFindById(1L)).thenReturn(Mono.just("not-authorized"));
+
+ Mono findById = this.messageService.monoPostAuthorizeBeanFindById(1L)
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+ }
+
+ // Flux tests
+
+ @Test
+ public void fluxWhenPermitAllThenAopDoesNotSubscribe() {
+ when(this.delegate.fluxFindById(1L)).thenReturn(Flux.from(result));
+
+ this.delegate.fluxFindById(1L);
+
+ result.assertNoSubscribers();
+ }
+
+ @Test
+ public void fluxWhenPermitAllThenSuccess() {
+ when(this.delegate.fluxFindById(1L)).thenReturn(Flux.just("success"));
+
+ StepVerifier.create(this.delegate.fluxFindById(1L))
+ .expectNext("success")
+ .verifyComplete();
+ }
+
+ @Test
+ public void fluxPreAuthorizeHasRoleWhenGrantedThenSuccess() {
+ when(this.delegate.fluxPreAuthorizeHasRoleFindById(1L)).thenReturn(Flux.just("result"));
+
+ Flux findById = this.messageService.fluxPreAuthorizeHasRoleFindById(1L)
+ .contextStart(withAdmin);
+ StepVerifier
+ .create(findById)
+ .consumeNextWith( s -> AssertionsForClassTypes.assertThat(s).isEqualTo("result"))
+ .verifyComplete();
+ }
+
+ @Test
+ public void fluxPreAuthorizeHasRoleWhenNoAuthenticationThenDenied() {
+ when(this.delegate.fluxPreAuthorizeHasRoleFindById(1L)).thenReturn(Flux.from(result));
+
+ Flux findById = this.messageService.fluxPreAuthorizeHasRoleFindById(1L);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+
+ result.assertNoSubscribers();
+ }
+
+ @Test
+ public void fluxPreAuthorizeHasRoleWhenNotAuthorizedThenDenied() {
+ when(this.delegate.fluxPreAuthorizeHasRoleFindById(1L)).thenReturn(Flux.from(result));
+
+ Flux findById = this.messageService.fluxPreAuthorizeHasRoleFindById(1L)
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+
+ result.assertNoSubscribers();
+ }
+
+ @Test
+ public void fluxPreAuthorizeBeanWhenGrantedThenSuccess() {
+ when(this.delegate.fluxPreAuthorizeBeanFindById(2L)).thenReturn(Flux.just("result"));
+
+ Flux findById = this.messageService.fluxPreAuthorizeBeanFindById(2L)
+ .contextStart(withAdmin);
+ StepVerifier
+ .create(findById)
+ .expectNext("result")
+ .verifyComplete();
+ }
+
+ @Test
+ public void fluxPreAuthorizeBeanWhenNotAuthenticatedAndGrantedThenSuccess() {
+ when(this.delegate.fluxPreAuthorizeBeanFindById(2L)).thenReturn(Flux.just("result"));
+
+ Flux findById = this.messageService.fluxPreAuthorizeBeanFindById(2L);
+ StepVerifier
+ .create(findById)
+ .expectNext("result")
+ .verifyComplete();
+ }
+
+ @Test
+ public void fluxPreAuthorizeBeanWhenNoAuthenticationThenDenied() {
+ when(this.delegate.fluxPreAuthorizeBeanFindById(1L)).thenReturn(Flux.from(result));
+
+ Flux findById = this.messageService.fluxPreAuthorizeBeanFindById(1L);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+
+ result.assertNoSubscribers();
+ }
+
+ @Test
+ public void fluxPreAuthorizeBeanWhenNotAuthorizedThenDenied() {
+ when(this.delegate.fluxPreAuthorizeBeanFindById(1L)).thenReturn(Flux.from(result));
+
+ Flux findById = this.messageService.fluxPreAuthorizeBeanFindById(1L)
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+
+ result.assertNoSubscribers();
+ }
+
+ @Test
+ public void fluxPostAuthorizeWhenAuthorizedThenSuccess() {
+ when(this.delegate.fluxPostAuthorizeFindById(1L)).thenReturn(Flux.just("user"));
+
+ Flux findById = this.messageService.fluxPostAuthorizeFindById(1L)
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectNext("user")
+ .verifyComplete();
+ }
+
+ @Test
+ public void fluxPostAuthorizeWhenNotAuthorizedThenDenied() {
+ when(this.delegate.fluxPostAuthorizeBeanFindById(1L)).thenReturn(Flux.just("not-authorized"));
+
+ Flux findById = this.messageService.fluxPostAuthorizeBeanFindById(1L)
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+ }
+
+ @Test
+ public void fluxPostAuthorizeWhenBeanAndAuthorizedThenSuccess() {
+ when(this.delegate.fluxPostAuthorizeBeanFindById(2L)).thenReturn(Flux.just("user"));
+
+ Flux findById = this.messageService.fluxPostAuthorizeBeanFindById(2L)
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectNext("user")
+ .verifyComplete();
+ }
+
+ @Test
+ public void fluxPostAuthorizeWhenBeanAndNotAuthenticatedAndAuthorizedThenSuccess() {
+ when(this.delegate.fluxPostAuthorizeBeanFindById(2L)).thenReturn(Flux.just("anonymous"));
+
+ Flux findById = this.messageService.fluxPostAuthorizeBeanFindById(2L);
+ StepVerifier
+ .create(findById)
+ .expectNext("anonymous")
+ .verifyComplete();
+ }
+
+ @Test
+ public void fluxPostAuthorizeWhenBeanAndNotAuthorizedThenDenied() {
+ when(this.delegate.fluxPostAuthorizeBeanFindById(1L)).thenReturn(Flux.just("not-authorized"));
+
+ Flux findById = this.messageService.fluxPostAuthorizeBeanFindById(1L)
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+ }
+
+ // Publisher tests
+
+ @Test
+ public void publisherWhenPermitAllThenAopDoesNotSubscribe() {
+ when(this.delegate.publisherFindById(1L)).thenReturn(result);
+
+ this.delegate.publisherFindById(1L);
+
+ result.assertNoSubscribers();
+ }
+
+ @Test
+ public void publisherWhenPermitAllThenSuccess() {
+ when(this.delegate.publisherFindById(1L)).thenReturn(publisherJust("success"));
+
+ StepVerifier.create(this.delegate.publisherFindById(1L))
+ .expectNext("success")
+ .verifyComplete();
+ }
+
+ @Test
+ public void publisherPreAuthorizeHasRoleWhenGrantedThenSuccess() {
+ when(this.delegate.publisherPreAuthorizeHasRoleFindById(1L)).thenReturn(publisherJust("result"));
+
+ Publisher findById = Flux.from(this.messageService.publisherPreAuthorizeHasRoleFindById(1L))
+ .contextStart(withAdmin);
+ StepVerifier
+ .create(findById)
+ .consumeNextWith( s -> AssertionsForClassTypes.assertThat(s).isEqualTo("result"))
+ .verifyComplete();
+ }
+
+ @Test
+ public void publisherPreAuthorizeHasRoleWhenNoAuthenticationThenDenied() {
+ when(this.delegate.publisherPreAuthorizeHasRoleFindById(1L)).thenReturn(result);
+
+ Publisher findById = this.messageService.publisherPreAuthorizeHasRoleFindById(1L);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+
+ result.assertNoSubscribers();
+ }
+
+ @Test
+ public void publisherPreAuthorizeHasRoleWhenNotAuthorizedThenDenied() {
+ when(this.delegate.publisherPreAuthorizeHasRoleFindById(1L)).thenReturn(result);
+
+ Publisher findById = Flux.from(this.messageService.publisherPreAuthorizeHasRoleFindById(1L))
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+
+ result.assertNoSubscribers();
+ }
+
+ @Test
+ public void publisherPreAuthorizeBeanWhenGrantedThenSuccess() {
+ when(this.delegate.publisherPreAuthorizeBeanFindById(2L)).thenReturn(publisherJust("result"));
+
+ Publisher findById = Flux.from(this.messageService.publisherPreAuthorizeBeanFindById(2L))
+ .contextStart(withAdmin);
+ StepVerifier
+ .create(findById)
+ .expectNext("result")
+ .verifyComplete();
+ }
+
+ @Test
+ public void publisherPreAuthorizeBeanWhenNotAuthenticatedAndGrantedThenSuccess() {
+ when(this.delegate.publisherPreAuthorizeBeanFindById(2L)).thenReturn(publisherJust("result"));
+
+ Publisher findById = this.messageService.publisherPreAuthorizeBeanFindById(2L);
+ StepVerifier
+ .create(findById)
+ .expectNext("result")
+ .verifyComplete();
+ }
+
+ @Test
+ public void publisherPreAuthorizeBeanWhenNoAuthenticationThenDenied() {
+ when(this.delegate.publisherPreAuthorizeBeanFindById(1L)).thenReturn(result);
+
+ Publisher findById = this.messageService.publisherPreAuthorizeBeanFindById(1L);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+
+ result.assertNoSubscribers();
+ }
+
+ @Test
+ public void publisherPreAuthorizeBeanWhenNotAuthorizedThenDenied() {
+ when(this.delegate.publisherPreAuthorizeBeanFindById(1L)).thenReturn(result);
+
+ Publisher findById = Flux.from(this.messageService.publisherPreAuthorizeBeanFindById(1L))
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+
+ result.assertNoSubscribers();
+ }
+
+ @Test
+ public void publisherPostAuthorizeWhenAuthorizedThenSuccess() {
+ when(this.delegate.publisherPostAuthorizeFindById(1L)).thenReturn(publisherJust("user"));
+
+ Publisher findById = Flux.from(this.messageService.publisherPostAuthorizeFindById(1L))
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectNext("user")
+ .verifyComplete();
+ }
+
+ @Test
+ public void publisherPostAuthorizeWhenNotAuthorizedThenDenied() {
+ when(this.delegate.publisherPostAuthorizeBeanFindById(1L)).thenReturn(publisherJust("not-authorized"));
+
+ Publisher findById = Flux.from(this.messageService.publisherPostAuthorizeBeanFindById(1L))
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+ }
+
+ @Test
+ public void publisherPostAuthorizeWhenBeanAndAuthorizedThenSuccess() {
+ when(this.delegate.publisherPostAuthorizeBeanFindById(2L)).thenReturn(publisherJust("user"));
+
+ Publisher findById = Flux.from(this.messageService.publisherPostAuthorizeBeanFindById(2L))
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectNext("user")
+ .verifyComplete();
+ }
+
+ @Test
+ public void publisherPostAuthorizeWhenBeanAndNotAuthenticatedAndAuthorizedThenSuccess() {
+ when(this.delegate.publisherPostAuthorizeBeanFindById(2L)).thenReturn(publisherJust("anonymous"));
+
+ Publisher findById = this.messageService.publisherPostAuthorizeBeanFindById(2L);
+ StepVerifier
+ .create(findById)
+ .expectNext("anonymous")
+ .verifyComplete();
+ }
+
+ @Test
+ public void publisherPostAuthorizeWhenBeanAndNotAuthorizedThenDenied() {
+ when(this.delegate.publisherPostAuthorizeBeanFindById(1L)).thenReturn(publisherJust("not-authorized"));
+
+ Publisher findById = Flux.from(this.messageService.publisherPostAuthorizeBeanFindById(1L))
+ .contextStart(withUser);
+ StepVerifier
+ .create(findById)
+ .expectError(AccessDeniedException.class)
+ .verify();
+ }
+
+ static Publisher publisher(Flux flux) {
+ return subscriber -> flux.subscribe(subscriber);
+ }
+
+ static Publisher publisherJust(T... data) {
+ return publisher(Flux.just(data));
+ }
+
+ @EnableReactiveMethodSecurity
+ static class Config {
+ ReactiveMessageService delegate = mock(ReactiveMessageService.class);
+
+ @Bean
+ public DelegatingReactiveMessageService defaultMessageService() {
+ return new DelegatingReactiveMessageService(delegate);
+ }
+
+ @Bean
+ public Authz authz() {
+ return new Authz();
+ }
+ }
+}
diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMessageService.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMessageService.java
new file mode 100644
index 0000000000..979b2f48a8
--- /dev/null
+++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMessageService.java
@@ -0,0 +1,42 @@
+/*
+ *
+ * * Copyright 2002-2017 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
+ * *
+ * * http://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.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public interface ReactiveMessageService {
+ Mono monoFindById(long id);
+ Mono monoPreAuthorizeHasRoleFindById(long id);
+ Mono monoPostAuthorizeFindById(long id);
+ Mono monoPreAuthorizeBeanFindById(long id);
+ Mono monoPostAuthorizeBeanFindById(long id);
+
+ Flux fluxFindById(long id);
+ Flux fluxPreAuthorizeHasRoleFindById(long id);
+ Flux fluxPostAuthorizeFindById(long id);
+ Flux fluxPreAuthorizeBeanFindById(long id);
+ Flux fluxPostAuthorizeBeanFindById(long id);
+
+ Publisher publisherFindById(long id);
+ Publisher publisherPreAuthorizeHasRoleFindById(long id);
+ Publisher publisherPostAuthorizeFindById(long id);
+ Publisher publisherPreAuthorizeBeanFindById(long id);
+ Publisher publisherPostAuthorizeBeanFindById(long id);
+}
diff --git a/core/src/main/java/org/springframework/security/access/method/PrePostAdviceMethodInterceptor.java b/core/src/main/java/org/springframework/security/access/method/PrePostAdviceMethodInterceptor.java
new file mode 100644
index 0000000000..32e64910b2
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/access/method/PrePostAdviceMethodInterceptor.java
@@ -0,0 +1,147 @@
+/*
+ *
+ * * Copyright 2002-2017 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
+ * *
+ * * http://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.access.method;
+
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.reactivestreams.Publisher;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.ExpressionBasedPostInvocationAdvice;
+import org.springframework.security.access.expression.method.ExpressionBasedPreInvocationAdvice;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.method.MethodSecurityMetadataSource;
+import org.springframework.security.access.prepost.PostInvocationAttribute;
+import org.springframework.security.access.prepost.PostInvocationAuthorizationAdvice;
+import org.springframework.security.access.prepost.PreInvocationAttribute;
+import org.springframework.security.access.prepost.PreInvocationAuthorizationAdvice;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.util.Assert;
+import reactor.core.Exceptions;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.util.context.Context;
+
+import java.lang.reflect.Method;
+import java.util.Collection;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+public class PrePostAdviceMethodInterceptor implements MethodInterceptor {
+ private Authentication anonymous = new AnonymousAuthenticationToken("key", "anonymous",
+ AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
+
+ private final MethodSecurityMetadataSource attributeSource;
+
+ private PostInvocationAuthorizationAdvice postAdvice;
+
+ private PreInvocationAuthorizationAdvice preAdvice;
+
+ public PrePostAdviceMethodInterceptor(MethodSecurityMetadataSource attributeSource) {
+ this.attributeSource = attributeSource;
+
+ MethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
+ this.postAdvice = new ExpressionBasedPostInvocationAdvice(handler);
+ this.preAdvice = new ExpressionBasedPreInvocationAdvice();
+ }
+
+ public void setPostAdvice(PostInvocationAuthorizationAdvice postAdvice) {
+ Assert.notNull(postAdvice, "postAdvice cannot be null");
+ this.postAdvice = postAdvice;
+ }
+
+ public void setPreAdvice(PreInvocationAuthorizationAdvice preAdvice) {
+ Assert.notNull(preAdvice, "preAdvice cannot be null");
+ this.preAdvice = preAdvice;
+ }
+
+ @Override
+ public Object invoke(final MethodInvocation invocation)
+ throws Throwable {
+ Method method = invocation.getMethod();
+ Class> returnType = method.getReturnType();
+ Class> targetClass = invocation.getThis().getClass();
+ Collection attributes = this.attributeSource
+ .getAttributes(method, targetClass);
+
+ PreInvocationAttribute preAttr = findPreInvocationAttribute(attributes);
+ Mono toInvoke = Mono.currentContext()
+ .defaultIfEmpty(Context.empty())
+ .flatMap( cxt -> cxt.getOrDefault(Authentication.class, Mono.just(anonymous)))
+ .filter( auth -> this.preAdvice.before(auth, invocation, preAttr))
+ .switchIfEmpty(Mono.error(new AccessDeniedException("Denied")));
+
+
+ PostInvocationAttribute attr = findPostInvocationAttribute(attributes);
+
+ if(Mono.class.isAssignableFrom(returnType)) {
+ return toInvoke
+ .flatMap( auth -> this.>proceed(invocation)
+ .map( r -> attr == null ? r : this.postAdvice.after(auth, invocation, attr, r))
+ );
+ }
+
+ if(Flux.class.isAssignableFrom(returnType)) {
+ return toInvoke
+ .flatMapMany( auth -> this.>proceed(invocation)
+ .map( r -> attr == null ? r : this.postAdvice.after(auth, invocation, attr, r))
+ );
+ }
+
+ return toInvoke
+ .flatMapMany( auth -> Flux.from(this.>proceed(invocation))
+ .map( r -> attr == null ? r : this.postAdvice.after(auth, invocation, attr, r))
+ );
+ }
+
+ private> T proceed(final MethodInvocation invocation) {
+ try {
+ return (T) invocation.proceed();
+ } catch(Throwable throwable) {
+ throw Exceptions.propagate(throwable);
+ }
+ }
+
+ private static PostInvocationAttribute findPostInvocationAttribute(
+ Collection config) {
+ for (ConfigAttribute attribute : config) {
+ if (attribute instanceof PostInvocationAttribute) {
+ return (PostInvocationAttribute) attribute;
+ }
+ }
+
+ return null;
+ }
+
+ private PreInvocationAttribute findPreInvocationAttribute(
+ Collection config) {
+ for (ConfigAttribute attribute : config) {
+ if (attribute instanceof PreInvocationAttribute) {
+ return (PreInvocationAttribute) attribute;
+ }
+ }
+
+ return null;
+ }
+}