浏览代码

Add EnableReactiveMethodSecurity

Issue gh-4496
Rob Winch 8 年之前
父节点
当前提交
416ff3c77a

+ 71 - 0
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}. <strong>
+	 * Applicable only if {@link #mode()} is set to {@link AdviceMode#PROXY}</strong>.
+	 * <p>Note that setting this attribute to {@code true} will affect <em>all</em>
+	 * 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;
+}

+ 85 - 0
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");
+	}
+}

+ 57 - 0
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<EnableReactiveMethodSecurity> {
+
+	@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}.
+	 * <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[result.size()]);
+	}
+}

+ 38 - 0
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());
+	}
+}

+ 131 - 0
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<String> monoFindById(long id) {
+		return delegate.monoFindById(id);
+	}
+
+	@Override
+	@PreAuthorize("hasRole('ADMIN')")
+	public Mono<String> monoPreAuthorizeHasRoleFindById(
+			long id) {
+		return delegate.monoPreAuthorizeHasRoleFindById(id);
+	}
+
+	@Override
+	@PostAuthorize("returnObject?.contains(authentication?.name)")
+	public Mono<String> monoPostAuthorizeFindById(
+			long id) {
+		return delegate.monoPostAuthorizeFindById(id);
+	}
+
+	@Override
+	@PreAuthorize("@authz.check(#id)")
+	public Mono<String> monoPreAuthorizeBeanFindById(
+			long id) {
+		return delegate.monoPreAuthorizeBeanFindById(id);
+	}
+
+	@Override
+	@PostAuthorize("@authz.check(authentication, returnObject)")
+	public Mono<String> monoPostAuthorizeBeanFindById(
+			long id) {
+		return delegate.monoPostAuthorizeBeanFindById(id);
+	}
+
+	@Override
+	public Flux<String> fluxFindById(long id) {
+		return delegate.fluxFindById(id);
+	}
+
+	@Override
+	@PreAuthorize("hasRole('ADMIN')")
+	public Flux<String> fluxPreAuthorizeHasRoleFindById(
+			long id) {
+		return delegate.fluxPreAuthorizeHasRoleFindById(id);
+	}
+
+	@Override
+	@PostAuthorize("returnObject?.contains(authentication?.name)")
+	public Flux<String> fluxPostAuthorizeFindById(
+			long id) {
+		return delegate.fluxPostAuthorizeFindById(id);
+	}
+
+	@Override
+	@PreAuthorize("@authz.check(#id)")
+	public Flux<String> fluxPreAuthorizeBeanFindById(
+			long id) {
+		return delegate.fluxPreAuthorizeBeanFindById(id);
+	}
+
+	@Override
+	@PostAuthorize("@authz.check(authentication, returnObject)")
+	public Flux<String> fluxPostAuthorizeBeanFindById(
+			long id) {
+		return delegate.fluxPostAuthorizeBeanFindById(id);
+	}
+
+	@Override
+	public Publisher<String> publisherFindById(long id) {
+		return delegate.publisherFindById(id);
+	}
+
+	@Override
+	@PreAuthorize("hasRole('ADMIN')")
+	public Publisher<String> publisherPreAuthorizeHasRoleFindById(
+			long id) {
+		return delegate.publisherPreAuthorizeHasRoleFindById(id);
+	}
+
+	@Override
+	@PostAuthorize("returnObject?.contains(authentication?.name)")
+	public Publisher<String> publisherPostAuthorizeFindById(
+			long id) {
+		return delegate.publisherPostAuthorizeFindById(id);
+	}
+
+	@Override
+	@PreAuthorize("@authz.check(#id)")
+	public Publisher<String> publisherPreAuthorizeBeanFindById(
+			long id) {
+		return delegate.publisherPreAuthorizeBeanFindById(id);
+	}
+
+	@Override
+	@PostAuthorize("@authz.check(authentication, returnObject)")
+	public Publisher<String> publisherPostAuthorizeBeanFindById(
+			long id) {
+		return delegate.publisherPostAuthorizeBeanFindById(id);
+	}
+}

+ 593 - 0
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<String> result = TestPublisher.create();
+
+	Function<Context, Context> withAdmin = context -> context.put(Authentication.class, Mono
+		.just(new TestingAuthenticationToken("admin","password","ROLE_USER", "ROLE_ADMIN")));
+	Function<Context, Context> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> findById = this.messageService.publisherPreAuthorizeBeanFindById(2L);
+		StepVerifier
+				.create(findById)
+				.expectNext("result")
+				.verifyComplete();
+	}
+
+	@Test
+	public void publisherPreAuthorizeBeanWhenNoAuthenticationThenDenied() {
+		when(this.delegate.publisherPreAuthorizeBeanFindById(1L)).thenReturn(result);
+
+		Publisher<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> findById = Flux.from(this.messageService.publisherPostAuthorizeBeanFindById(1L))
+				.contextStart(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
+	static class Config {
+		ReactiveMessageService delegate = mock(ReactiveMessageService.class);
+
+		@Bean
+		public DelegatingReactiveMessageService defaultMessageService() {
+			return new DelegatingReactiveMessageService(delegate);
+		}
+
+		@Bean
+		public Authz authz() {
+			return new Authz();
+		}
+	}
+}

+ 42 - 0
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<String> monoFindById(long id);
+	Mono<String> monoPreAuthorizeHasRoleFindById(long id);
+	Mono<String> monoPostAuthorizeFindById(long id);
+	Mono<String> monoPreAuthorizeBeanFindById(long id);
+	Mono<String> monoPostAuthorizeBeanFindById(long id);
+
+	Flux<String> fluxFindById(long id);
+	Flux<String> fluxPreAuthorizeHasRoleFindById(long id);
+	Flux<String> fluxPostAuthorizeFindById(long id);
+	Flux<String> fluxPreAuthorizeBeanFindById(long id);
+	Flux<String> fluxPostAuthorizeBeanFindById(long id);
+
+	Publisher<String> publisherFindById(long id);
+	Publisher<String> publisherPreAuthorizeHasRoleFindById(long id);
+	Publisher<String> publisherPostAuthorizeFindById(long id);
+	Publisher<String> publisherPreAuthorizeBeanFindById(long id);
+	Publisher<String> publisherPostAuthorizeBeanFindById(long id);
+}

+ 147 - 0
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<ConfigAttribute> attributes = this.attributeSource
+			.getAttributes(method, targetClass);
+
+		PreInvocationAttribute preAttr = findPreInvocationAttribute(attributes);
+		Mono<Authentication> 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.<Mono<?>>proceed(invocation)
+					.map( r -> attr == null ? r : this.postAdvice.after(auth, invocation, attr, r))
+				);
+		}
+
+		if(Flux.class.isAssignableFrom(returnType)) {
+			return toInvoke
+				.flatMapMany( auth -> this.<Flux<?>>proceed(invocation)
+					.map( r -> attr == null ? r : this.postAdvice.after(auth, invocation, attr, r))
+				);
+		}
+
+		return toInvoke
+			.flatMapMany( auth -> Flux.from(this.<Publisher<?>>proceed(invocation))
+				.map( r -> attr == null ? r : this.postAdvice.after(auth, invocation, attr, r))
+			);
+	}
+
+	private<T extends Publisher<?>> T proceed(final MethodInvocation invocation) {
+		try {
+			return (T) invocation.proceed();
+		} catch(Throwable throwable) {
+			throw Exceptions.propagate(throwable);
+		}
+	}
+
+	private static PostInvocationAttribute findPostInvocationAttribute(
+		Collection<ConfigAttribute> config) {
+		for (ConfigAttribute attribute : config) {
+			if (attribute instanceof PostInvocationAttribute) {
+				return (PostInvocationAttribute) attribute;
+			}
+		}
+
+		return null;
+	}
+
+	private PreInvocationAttribute findPreInvocationAttribute(
+		Collection<ConfigAttribute> config) {
+		for (ConfigAttribute attribute : config) {
+			if (attribute instanceof PreInvocationAttribute) {
+				return (PreInvocationAttribute) attribute;
+			}
+		}
+
+		return null;
+	}
+}