跳到主要内容
版本:7.0.2

EnableReactiveMethodSecurity

DeepSeek V3 中英对照 EnableReactiveMethodSecurity

Spring Security 通过使用 Reactor 的 Context 来支持方法安全,该 Context 由 ReactiveSecurityContextHolder 进行设置。以下示例展示了如何获取当前已登录用户的消息:

备注

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

启用响应式方法安全与授权管理器

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

这相较于 @EnableReactiveMethodSecurity 在多个方面有所改进。@EnableReactiveMethodSecurity(useAuthorizationManager=true)

  1. 使用简化的 AuthorizationManager API 替代元数据源、配置属性、决策管理器和投票器。这简化了重用和定制过程。

  2. 支持响应式返回类型,包括 Kotlin 协程。

  3. 基于原生 Spring AOP 构建,移除了抽象层,允许您使用 Spring AOP 构建块进行定制。

  4. 检查冲突的注解,以确保安全配置明确无误。

  5. 符合 JSR-250 规范。

备注

对于早期版本,请阅读关于使用 @EnableReactiveMethodSecurity 的类似支持。

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

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

在方法(类或接口上)添加注解后,将相应地限制对该方法的访问。Spring Security 的原生注解支持为方法定义了一组属性。这些属性将被传递给各种方法拦截器,例如 AuthorizationManagerBeforeReactiveMethodInterceptor,以便其做出实际决策:

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 表达式评估引擎调用。

@bean 指的是您定义的自定义组件,其中 apply 方法可以返回 BooleanMono<Boolean> 来表示授权决策。这样的一个 bean 可能看起来像这样:

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

方法授权是方法前授权和方法后授权的结合。

备注

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

要重现默认情况下添加 @EnableReactiveMethodSecurity(useAuthorizationManager=true) 所实现的功能,你需要发布以下配置:

@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 构建的。

自定义授权

Spring Security 的 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 提供了丰富的基于表达式的支持。

此外,对于基于角色的授权,Spring Security 会添加一个默认的 ROLE_ 前缀,这在评估诸如 hasRole 这类表达式时会用到。你可以通过暴露一个 GrantedAuthorityDefaults bean 来配置授权规则,以使用不同的前缀,如下所示:

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("MYPREFIX_");
}
提示

我们通过 static 方法暴露 GrantedAuthorityDefaults,以确保 Spring 在初始化 Spring Security 的方法安全 @Configuration 类之前发布它。由于 GrantedAuthorityDefaults bean 是 Spring Security 内部工作机制的一部分,我们也应将其作为基础设施 bean 暴露,从而有效避免与 bean 后处理相关的一些警告(参见 gh-14751)。

以编程方式授权方法

正如您已经看到的,有几种方法可以使用方法安全SpEL表达式来指定非平凡的授权规则。

您可以通过多种方式将逻辑实现从基于SpEL转换为基于Java。这使我们能够利用完整的Java语言功能,从而提升可测试性和流程控制能力。

在 SpEL 中使用自定义 Bean

以编程方式授权方法的第一种方式是一个两步过程。

首先,声明一个包含接收 MethodSecurityExpressionOperations 实例方法的 Bean,如下所示:

@Component("authz")
public class AuthorizationLogic {
public decide(MethodSecurityExpressionOperations operations): Mono<Boolean> {
// ... authorization logic
}
}

然后,在您的注解中按以下方式引用该bean:

@Controller
public class MyController {
@PreAuthorize("@authz.decide(#root)")
@GetMapping("/endpoint")
public Mono<String> endpoint() {
// ...
}
}

Spring Security 将在每次方法调用时,对该 bean 调用给定的方法。

这样做的好处在于,所有的授权逻辑都封装在一个独立的类中,可以独立进行单元测试并验证其正确性。同时,它还能充分利用完整的 Java 语言功能。

提示

除了返回 Mono<Boolean>,你也可以返回 Mono.empty() 来表示代码选择弃权,不做出决定。

如果你想包含更多关于决策性质的信息,可以改为返回一个自定义的 AuthorizationDecision,如下所示:

@Component("authz")
public class AuthorizationLogic {
public Mono<AuthorizationDecision> decide(MethodSecurityExpressionOperations operations) {
// ... authorization logic
return Mono.just(new MyAuthorizationDecision(false, details));
}
}

或者抛出一个自定义的 AuthorizationDeniedException 实例。但请注意,返回一个对象是更推荐的做法,因为这不会产生生成堆栈跟踪的开销。

然后,您可以在自定义授权结果处理方式时访问自定义的详细信息。

使用自定义授权管理器

第二种以编程方式授权方法的方式是创建一个自定义的 AuthorizationManager

首先,声明一个授权管理器实例,或许像这样:

@Component
public class MyPreAuthorizeAuthorizationManager implements ReactiveAuthorizationManager<MethodInvocation> {
@Override
public Mono<AuthorizationResult> authorize(Supplier<Authentication> authentication, MethodInvocation invocation) {
// ... authorization logic
}

}

然后,发布一个方法拦截器,并配置一个切入点,以指定你希望该 ReactiveAuthorizationManager 何时运行。例如,你可以像这样替换 @PreAuthorize@PostAuthorize 的工作方式:

@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorize(MyPreAuthorizeAuthorizationManager manager) {
return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(manager);
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorize(MyPostAuthorizeAuthorizationManager manager) {
return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(manager);
}
}
提示

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

自定义表达式处理

或者,第三,你可以自定义每个SpEL表达式的处理方式。为此,你可以公开一个自定义的 MethodSecurityExpressionHandler,如下所示:

@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy);
return handler;
}
提示

我们通过 static 方法暴露 MethodSecurityExpressionHandler,以确保 Spring 在初始化 Spring Security 的方法安全 @Configuration 类之前发布它

你也可以继承 DefaultMessageSecurityExpressionHandler 来添加超出默认范围的自定义授权表达式。

EnableReactiveMethodSecurity

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();

其中 this::findMessageByUsername 的定义为:

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

以下最小方法安全配置用于在响应式应用程序中配置方法安全:

@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);
}
}

考虑以下类:

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

或者,以下类使用了 Kotlin 协程:

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

结合我们上面的配置,@PreAuthorize("hasRole('ADMIN')") 确保了 findByMessage 方法仅能被拥有 ADMIN 角色的用户调用。请注意,标准方法安全中的任何表达式都适用于 @EnableReactiveMethodSecurity。然而,目前我们仅支持表达式的返回类型为 Booleanboolean。这意味着表达式不能阻塞。

当与 WebFlux Security 集成时,Spring Security 会根据已认证用户自动建立 Reactor Context:

@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((authorize) -> authorize
.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);
}
}

你可以在 hellowebflux-method 中找到完整的示例。