EnableReactiveMethodSecurity

Spring Security使用 Reactor’s Context支持方法安全性,它由`ReactiveSecurityContextHolder`设置。以下示例演示如何检索当前登录用户的消息:

Spring Security supports method security by using Reactor’s Context, which is set up by ReactiveSecurityContextHolder. The following example shows how to retrieve the currently logged in user’s message:

要使此示例正常工作,该方法的返回类型必须为 org.reactivestreams.Publisher(即 MonoFlux)。这是与 Reactor 的 Context 集成的必要条件。

For this example to work, the return type of the method must be a org.reactivestreams.Publisher (that is, a Mono or a Flux). This is necessary to integrate with Reactor’s Context.

EnableReactiveMethodSecurity with AuthorizationManager

在 Spring Security 5.8 中,我们可以使用 @EnableReactiveMethodSecurity(useAuthorizationManager=true) 注解在任何 @Configuration 实例上启用基于注解的安全性。

In Spring Security 5.8, we can enable annotation-based security using the @EnableReactiveMethodSecurity(useAuthorizationManager=true) annotation on any @Configuration instance.

这在多个方面改进了 @EnableReactiveMethodSecurity

This improves upon @EnableReactiveMethodSecurity in a number of ways. @EnableReactiveMethodSecurity(useAuthorizationManager=true):

  1. Uses the simplified AuthorizationManager API instead of metadata sources, config attributes, decision managers, and voters. This simplifies reuse and customization.

  2. Supports reactive return types. Note that we are waiting on additional coroutine support from the Spring Framework before adding coroutine support.

  3. Is built using native Spring AOP, removing abstractions and allowing you to use Spring AOP building blocks to customize

  4. Checks for conflicting annotations to ensure an unambiguous security configuration

  5. Complies with JSR-250

对于早期版本,请阅读 <<@EnableReactiveMethodSecurity,jc-enable-reactive-method-security>> 类似支持。

For earlier versions, please read about similar support with <<@EnableReactiveMethodSecurity,jc-enable-reactive-method-security>>.

例如,以下内容将启用 Spring Security 的 @PreAuthorize 注解:

For example, the following would enable Spring Security’s @PreAuthorize annotation:

Method Security Configuration
  • Java

@EnableReactiveMethodSecurity(useAuthorizationManager=true)
public class MethodSecurityConfig {
	// ...
}

把一个注释添加到一个方法(在一个类或接口上)会根据此限制该方法的访问权限。Spring Security 的本机注释支持为方法定义一组属性。这些属性会传递到不同的方法拦截器,比如 AuthorizationManagerBeforeReactiveMethodInterceptor,以便它做出实际决策:

Adding an annotation to a method (on a class or interface) would then limit the access to that method accordingly. Spring Security’s native annotation support defines a set of attributes for the method. These will be passed to the various method interceptors, like AuthorizationManagerBeforeReactiveMethodInterceptor, for it to make the actual decision:

Method Security Annotation Usage
  • Java

public interface BankService {
	@PreAuthorize("hasRole('USER')")
	Mono<Account> readAccount(Long id);

	@PreAuthorize("hasRole('USER')")
	Flux<Account> findAccounts();

	@PreAuthorize("@func.apply(#account)")
	Mono<Account> post(Account account, Double amount);
}

在这种情况下,hasRole 指在 SecurityExpressionRoot 中找到的方法,该方法由 SpEL 评估引擎调用。

In this case hasRole refers to the method found in SecurityExpressionRoot that gets invoked by the SpEL evaluation engine.

@bean 指你已经定义的一个自定义组件,其中 apply 可以返回 BooleanMono<Boolean> 来指示授权决策。类似这样的 Bean 可能长这样:

@bean refers to a custom component you have defined, where apply can return Boolean or Mono<Boolean> to indicate the authorization decision. A bean like that might look something like this:

Method Security Reactive Boolean Expression
  • Java

@Bean
public Function<Account, Mono<Boolean>> func() {
    return (account) -> Mono.defer(() -> Mono.just(account.getId().equals(12)));
}

Customizing Authorization

Spring Security 的 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 附带丰富的基于表达式的支持。

Spring Security’s @PreAuthorize, @PostAuthorize, @PreFilter, and @PostFilter ship with rich expression-based support.

此外,对于基于角色的授权,Spring Security 添加默认 ROLE_ 前缀,用于在评估 hasRole 类似的表达式时使用。你可以配置授权规则来使用不同的前缀,方法是公开一个 GrantedAuthorityDefaults Bean,如下所示:

Also, for role-based authorization, Spring Security adds a default ROLE_ prefix, which is uses when evaluating expressions like hasRole. You can configure the authorization rules to use a different prefix by exposing a GrantedAuthorityDefaults bean, like so:

Custom MethodSecurityExpressionHandler
  • Java

@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
	return new GrantedAuthorityDefaults("MYPREFIX_");
}

我们使用 static 方法公开 GrantedAuthorityDefaults 以确保 Spring 在初始化 Spring Security 的方法安全 @Configuration 类之前发布它

We expose GrantedAuthorityDefaults using a static method to ensure that Spring publishes it before it initializes Spring Security’s method security @Configuration classes

Custom Authorization Managers

方法授权是方法前置(before)授权和后置(after)授权的组合。

Method authorization is a combination of before- and after-method authorization.

方法前置授权在调用方法之前执行。如果该授权拒绝访问,则不会调用该方法并会抛出一个 AccessDeniedException。方法后置授权在调用方法后但在方法返回给调用者之前执行。如果该授权拒绝访问,则不会返回该值并会抛出一个 AccessDeniedException

Before-method authorization is performed before the method is invoked. If that authorization denies access, the method is not invoked, and an AccessDeniedException is thrown. After-method authorization is performed after the method is invoked, but before the method returns to the caller. If that authorization denies access, the value is not returned, and an AccessDeniedException is thrown

若要重建 @EnableReactiveMethodSecurity(useAuthorizationManager=true) 默认执行的操作,你可以发布以下配置:

To recreate what adding @EnableReactiveMethodSecurity(useAuthorizationManager=true) does by default, you would publish the following configuration:

Full Pre-post Method Security Configuration
  • Java

@Configuration
class MethodSecurityConfig {
	@Bean
	BeanDefinitionRegistryPostProcessor aopConfig() {
		return AopConfigUtils::registerAutoProxyCreatorIfNecessary;
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor() {
		return new PreFilterAuthorizationReactiveMethodInterceptor();
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor() {
		return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize();
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor() {
		return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize();
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor() {
		return new PostFilterAuthorizationReactiveMethodInterceptor();
	}
}

注意 Spring Security 的方法安全是使用 Spring AOP 构建的。因此,拦截器会根据指定的顺序调用。可以通过在拦截器实例上调用 setOrder 来对其进行自定义,如下所示:

Notice that Spring Security’s method security is built using Spring AOP. So, interceptors are invoked based on the order specified. This can be customized by calling setOrder on the interceptor instances like so:

Publish Custom Advisor
  • Java

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postFilterAuthorizationMethodInterceptor() {
	PostFilterAuthorizationMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor();
	interceptor.setOrder(AuthorizationInterceptorOrders.POST_AUTHORIZE.getOrder() - 1);
	return interceptor;
}

你可能只希望在你的应用程序中支持 @PreAuthorize,在这种情况下,你可以执行以下操作:

You may want to only support @PreAuthorize in your application, in which case you can do the following:

Only @PreAuthorize Configuration
  • Java

@Configuration
class MethodSecurityConfig {
	@Bean
	BeanDefinitionRegistryPostProcessor aopConfig() {
		return AopConfigUtils::registerAutoProxyCreatorIfNecessary;
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	Advisor preAuthorize() {
		return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
	}
}

或者,你有一个自定义方法前置 ReactiveAuthorizationManager,你希望将它添加到列表中。

Or, you may have a custom before-method ReactiveAuthorizationManager that you want to add to the list.

在这种情况下,你需要告诉 Spring Security ReactiveAuthorizationManager 以及你的授权管理器应用到的方法和类。

In this case, you will need to tell Spring Security both the ReactiveAuthorizationManager and to which methods and classes your authorization manager applies.

因此,你可以配置 Spring Security 在 @PreAuthorize@PostAuthorize 之间调用你的 ReactiveAuthorizationManager,如下所示:

Thus, you can configure Spring Security to invoke your ReactiveAuthorizationManager in between @PreAuthorize and @PostAuthorize like so:

Custom Before Advisor
  • Java

@EnableReactiveMethodSecurity(useAuthorizationManager=true)
class MethodSecurityConfig {
	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public Advisor customAuthorize() {
		JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
		pattern.setPattern("org.mycompany.myapp.service.*");
		ReactiveAuthorizationManager<MethodInvocation> rule = AuthorityAuthorizationManager.isAuthenticated();
		AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(pattern, rule);
		interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
		return interceptor;
    }
}

您可以使用 AuthorizationInterceptorsOrder 中指定的顺序常量将拦截器放在 Spring Security 方法拦截器之间。

You can place your interceptor in between Spring Security method interceptors using the order constants specified in AuthorizationInterceptorsOrder.

同样的操作也可以对方法后置授权执行。方法后置授权通常关心分析返回值以验证访问。

The same can be done for after-method authorization. After-method authorization is generally concerned with analysing the return value to verify access.

例如,你可能有一个方法确认请求的账户实际上属于登录用户,如下所示:

For example, you might have a method that confirms that the account requested actually belongs to the logged-in user like so:

@PostAuthorize example
  • Java

public interface BankService {

	@PreAuthorize("hasRole('USER')")
	@PostAuthorize("returnObject.owner == authentication.name")
	Mono<Account> readAccount(Long id);
}

你可以提供你自己的 AuthorizationMethodInterceptor 以自定义如何评估对返回值的访问。

You can supply your own AuthorizationMethodInterceptor to customize how access to the return value is evaluated.

例如,如果你有你自己的自定义注释,你可以对其进行如下配置:

For example, if you have your own custom annotation, you can configure it like so:

Custom After Advisor
  • Java

@EnableReactiveMethodSecurity(useAuthorizationManager=true)
class MethodSecurityConfig {
	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public Advisor customAuthorize(ReactiveAuthorizationManager<MethodInvocationResult> rules) {
		AnnotationMethodMatcher pattern = new AnnotationMethodMatcher(MySecurityAnnotation.class);
		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(pattern, rules);
		interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
		return interceptor;
	}
}

然后它会在 @PostAuthorize 拦截器之后被调用。

and it will be invoked after the @PostAuthorize interceptor.

EnableReactiveMethodSecurity

`@EnableReactiveMethodSecurity`还支持Kotlin协程,但仅限于一定程度。在拦截协程时,仅第一个拦截器参与。如果存在任何其他拦截器并在Spring Security方法安全性拦截器之后, they will be skipped

@EnableReactiveMethodSecurity also supports Kotlin coroutines, though only to a limited degree. When intercepting coroutines, only the first interceptor participates. If any other interceptors are present and come after Spring Security’s method security interceptor, they will be skipped.

  • Java

  • Kotlin

Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");

Mono<String> messageByUsername = ReactiveSecurityContextHolder.getContext()
	.map(SecurityContext::getAuthentication)
	.map(Authentication::getName)
	.flatMap(this::findMessageByUsername)
	// In a WebFlux application the `subscriberContext` is automatically setup using `ReactorContextWebFilter`
	.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));

StepVerifier.create(messageByUsername)
	.expectNext("Hi user")
	.verifyComplete();
val authentication: Authentication = TestingAuthenticationToken("user", "password", "ROLE_USER")

val messageByUsername: Mono<String> = ReactiveSecurityContextHolder.getContext()
	.map(SecurityContext::getAuthentication)
	.map(Authentication::getName)
	.flatMap(this::findMessageByUsername) // In a WebFlux application the `subscriberContext` is automatically setup using `ReactorContextWebFilter`
	.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication))

StepVerifier.create(messageByUsername)
	.expectNext("Hi user")
	.verifyComplete()

其中 this::findMessageByUsername 的定义为:

Where this::findMessageByUsername is defined as:

  • Java

  • Kotlin

Mono<String> findMessageByUsername(String username) {
	return Mono.just("Hi " + username);
}
fun findMessageByUsername(username: String): Mono<String> {
	return Mono.just("Hi $username")
}

下面的 minimal 方法安全配置方法对响应式应用程序中的安全进行了配置:

The following minimal method security configures method security in reactive applications:

  • Java

  • Kotlin

@Configuration
@EnableReactiveMethodSecurity
public class SecurityConfig {
	@Bean
	public MapReactiveUserDetailsService userDetailsService() {
		User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
		UserDetails rob = userBuilder.username("rob")
			.password("rob")
			.roles("USER")
			.build();
		UserDetails admin = userBuilder.username("admin")
			.password("admin")
			.roles("USER","ADMIN")
			.build();
		return new MapReactiveUserDetailsService(rob, admin);
	}
}
@Configuration
@EnableReactiveMethodSecurity
class SecurityConfig {
	@Bean
	fun userDetailsService(): MapReactiveUserDetailsService {
		val userBuilder: User.UserBuilder = User.withDefaultPasswordEncoder()
		val rob = userBuilder.username("rob")
			.password("rob")
			.roles("USER")
			.build()
		val admin = userBuilder.username("admin")
			.password("admin")
			.roles("USER", "ADMIN")
			.build()
		return MapReactiveUserDetailsService(rob, admin)
	}
}

考虑以下类:

Consider the following class:

  • Java

  • Kotlin

@Component
public class HelloWorldMessageService {
	@PreAuthorize("hasRole('ADMIN')")
	public Mono<String> findMessage() {
		return Mono.just("Hello World!");
	}
}
@Component
class HelloWorldMessageService {
	@PreAuthorize("hasRole('ADMIN')")
	fun findMessage(): Mono<String> {
		return Mono.just("Hello World!")
	}
}

或者,下面的类使用 Kotlin 协程:

Alternatively, the following class uses Kotlin coroutines:

  • Kotlin

@Component
class HelloWorldMessageService {
    @PreAuthorize("hasRole('ADMIN')")
    suspend fun findMessage(): String {
        delay(10)
        return "Hello World!"
    }
}

与我们的上述配置结合后,@PreAuthorize("hasRole('ADMIN')") 确保只允许具有 ADMIN 角色的用户调用 findByMessage。只要遵循标准方法安全的任何表达式,@EnableReactiveMethodSecurity 都是可以使用的。但是,目前只支持 Booleanboolean 返回类型的表达式。这意味着表达式不能阻塞。

Combined with our configuration above, @PreAuthorize("hasRole('ADMIN')") ensures that findByMessage is invoked only by a user with the ADMIN role. Note that any of the expressions in standard method security work for @EnableReactiveMethodSecurity. However, at this time, we support only a return type of Boolean or boolean of the expression. This means that the expression must not block.

在与 WebFlux Security集成时,响应式上下文是由 Spring Security 根据已验证的用户自动建立的:

When integrating with WebFlux Security, the Reactor Context is automatically established by Spring Security according to the authenticated user:

  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {

	@Bean
	SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exception {
		return http
			// Demonstrate that method security works
			// Best practice to use both for defense in depth
			.authorizeExchange(exchanges -> exchanges
				.anyExchange().permitAll()
			)
			.httpBasic(withDefaults())
			.build();
	}

	@Bean
	MapReactiveUserDetailsService userDetailsService() {
		User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
		UserDetails rob = userBuilder.username("rob")
			.password("rob")
			.roles("USER")
			.build();
		UserDetails admin = userBuilder.username("admin")
			.password("admin")
			.roles("USER","ADMIN")
			.build();
		return new MapReactiveUserDetailsService(rob, admin);
	}
}
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
class SecurityConfig {
	@Bean
	open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
		return http {
			authorizeExchange {
				authorize(anyExchange, permitAll)
			}
			httpBasic { }
		}
	}

	@Bean
	fun userDetailsService(): MapReactiveUserDetailsService {
		val userBuilder: User.UserBuilder = User.withDefaultPasswordEncoder()
		val rob = userBuilder.username("rob")
			.password("rob")
			.roles("USER")
			.build()
		val admin = userBuilder.username("admin")
			.password("admin")
			.roles("USER", "ADMIN")
			.build()
		return MapReactiveUserDetailsService(rob, admin)
	}
}

您可以在https://github.com/spring-projects/spring-security-samples/tree/{gh-tag}/reactive/webflux/java/method[hellowebflux-method]找到一个完整的示例。

You can find a complete sample in hellowebflux-method.