EnableReactiveMethodSecurity
Spring Security 通过使用 Reactor 的 Context 来支持方法安全性,该 Context 由 ReactiveSecurityContextHolder
设置。以下示例展示了如何检索当前登录用户的消息:
对于此示例要正常工作,方法的返回类型必须是 org.reactivestreams.Publisher
(即 Mono
或 Flux
)。这是为了与 Reactor 的 Context
集成所必需的。
使用 EnableReactiveMethodSecurity
和 AuthorizationManager
在 Spring Security 5.8 中,我们可以通过在任何 @Configuration
实例上使用 @EnableReactiveMethodSecurity(useAuthorizationManager=true)
注解来启用基于注解的安全性。
这在很多方面改进了 @EnableReactiveMethodSecurity
。@EnableReactiveMethodSecurity(useAuthorizationManager=true)
:
-
使用简化的
AuthorizationManager
API,而不是元数据源、配置属性、决策管理器和投票器。这简化了复用和自定义。 -
支持响应式返回类型,包括 Kotlin 协程。
-
使用原生的 Spring AOP 构建,移除了抽象层,并允许你使用 Spring AOP 构建块进行自定义。
-
检查冲突的注解以确保安全配置无歧义。
-
遵守 JSR-250
对于早期版本,请阅读有关使用 @EnableReactiveMethodSecurity 的类似支持。
例如,以下将启用 Spring Security 的 @PreAuthorize
注解:
- Java
@EnableReactiveMethodSecurity(useAuthorizationManager=true)
public class MethodSecurityConfig {
// ...
}
在方法(在类或接口上)上添加注解会相应地限制对该方法的访问。Spring Security 的原生注解支持为该方法定义了一组属性。这些属性将传递给各种方法拦截器,如 AuthorizationManagerBeforeReactiveMethodInterceptor
,以便它做出实际的决策。
- 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 评估引擎调用。
@bean
指的是你定义的自定义组件,其中 apply
可以返回 Boolean
或 Mono<Boolean>
来表示授权决策。这样的 bean 可能看起来像这样:
- Java
@Bean
public Function<Account, Mono<Boolean>> func() {
return (account) -> Mono.defer(() -> Mono.just(account.getId().equals(12)));
}
方法授权是方法前和方法后的授权的结合。
方法前授权在方法被调用之前执行。如果该授权拒绝访问,则不会调用该方法,并抛出 AccessDeniedException
。方法后授权在方法被调用之后但在方法返回给调用者之前执行。如果该授权拒绝访问,则不会返回值,并抛出 AccessDeniedException
要重新创建添加 @EnableReactiveMethodSecurity(useAuthorizationManager=true)
默认所做的操作,你需要发布以下配置:
- 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 构建的。
自定义授权
Spring Security 的 @PreAuthorize
、@PostAuthorize
、@PreFilter
和 @PostFilter
提供了丰富的基于表达式的支持。
此外,对于基于角色的授权,Spring Security 会添加一个默认的 ROLE_
前缀,该前缀在评估像 hasRole
这样的表达式时使用。你可以通过暴露一个 GrantedAuthorityDefaults
bean 来配置授权规则以使用不同的前缀,如下所示:
- Java
@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 表达式来指定复杂的授权规则。
有多种方法可以让你的逻辑基于Java而不是基于SpEL。这使我们可以使用整个Java语言,从而提高可测试性和流程控制。
在 SpEL 中使用自定义 Bean
程序化授权方法的第一种方式是一个两步过程。
首先,声明一个具有如下方法的 bean,该方法接收一个 MethodSecurityExpressionOperations
实例:
- Java
- Kotlin
@Component("authz")
public class AuthorizationLogic {
public decide(MethodSecurityExpressionOperations operations): Mono<Boolean> {
// ... authorization logic
}
}
@Component("authz")
open class AuthorizationLogic {
fun decide(val operations: MethodSecurityExpressionOperations): Mono<Boolean> {
// ... authorization logic
}
}
然后,在注解中以如下方式引用该 bean:
- Java
- Kotlin
@Controller
public class MyController {
@PreAuthorize("@authz.decide(#root)")
@GetMapping("/endpoint")
public Mono<String> endpoint() {
// ...
}
}
@Controller
open class MyController {
@PreAuthorize("@authz.decide(#root)")
@GetMapping("/endpoint")
fun endpoint(): Mono<String> {
// ...
}
}
Spring Security 将在每次方法调用时调用该 bean 上的给定方法。
这样做的好处是,所有的授权逻辑都在一个独立的类中,可以独立进行单元测试并验证其正确性。它还可以访问完整的 Java 语言。
除了返回 Mono<Boolean>
,您还可以返回 Mono.empty()
来表示代码不做出决定。
如果你想要包含更多关于决策性质的信息,你可以返回一个自定义的 AuthorizationDecision
,如下所示:
- Java
- Kotlin
@Component("authz")
public class AuthorizationLogic {
public Mono<AuthorizationDecision> decide(MethodSecurityExpressionOperations operations) {
// ... authorization logic
return Mono.just(new MyAuthorizationDecision(false, details));
}
}
@Component("authz")
open class AuthorizationLogic {
fun decide(val operations: MethodSecurityExpressionOperations): Mono<AuthorizationDecision> {
// ... authorization logic
return Mono.just(MyAuthorizationDecision(false, details))
}
}
或者抛出一个自定义的 AuthorizationDeniedException
实例。不过,请注意,返回一个对象是更可取的做法,因为这样不会产生生成堆栈跟踪的开销。
然后,您可以在自定义如何处理授权结果时访问自定义详细信息。
使用自定义授权管理器
第二种以编程方式授权方法的方式是创建自定义的 AuthorizationManager。
首先,声明一个授权管理器实例,可能像这样:
- Java
- Kotlin
@Component
public class MyPreAuthorizeAuthorizationManager implements ReactiveAuthorizationManager<MethodInvocation> {
@Override
public Mono<AuthorizationDecision> check(Supplier<Authentication> authentication, MethodInvocation invocation) {
// ... authorization logic
}
}
@Component
class MyPreAuthorizeAuthorizationManager : ReactiveAuthorizationManager<MethodInvocation> {
override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocation): Mono<AuthorizationDecision> {
// ... authorization logic
}
}
然后,使用与你希望 ReactiveAuthorizationManager
运行时相对应的切入点发布方法拦截器。例如,你可以像这样替换 @PreAuthorize
和 @PostAuthorize
的工作方式:
- Java
- Kotlin
@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);
}
}
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun preAuthorize(val manager: MyPreAuthorizeAuthorizationManager) : Advisor {
return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(manager)
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun postAuthorize(val manager: MyPostAuthorizeAuthorizationManager) : Advisor {
return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(manager)
}
}
你可以使用 AuthorizationInterceptorsOrder
中指定的顺序常量,将拦截器放置在 Spring Security 方法拦截器之间。
自定义表达式处理
或者,第三种方法,你可以自定义每个SpEL表达式的处理方式。为此,你可以暴露一个自定义的 MethodSecurityExpressionHandler
,如下所示:
- Java
- Kotlin
@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy);
return handler;
}
companion object {
@Bean
fun methodSecurityExpressionHandler(val roleHierarchy: RoleHierarchy) : MethodSecurityExpressionHandler {
val handler = DefaultMethodSecurityExpressionHandler()
handler.setRoleHierarchy(roleHierarchy)
return handler
}
}
我们通过一个 static
方法暴露 MethodSecurityExpressionHandler
,以确保 Spring 在初始化 Spring Security 的方法安全 @Configuration
类之前发布它。
你也可以继承 DefaultMessageSecurityExpressionHandler 以添加自定义的授权表达式,而不仅仅是默认的。
EnableReactiveMethodSecurity
- 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
定义如下:
- Java
- Kotlin
Mono<String> findMessageByUsername(String username) {
return Mono.just("Hi " + username);
}
fun findMessageByUsername(username: String): Mono<String> {
return Mono.just("Hi $username")
}
以下是最小化的方法安全配置,用于在响应式应用中配置方法安全:
- 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)
}
}
考虑以下类:
- 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 协程:
- Kotlin
@Component
class HelloWorldMessageService {
@PreAuthorize("hasRole('ADMIN')")
suspend fun findMessage(): String {
delay(10)
return "Hello World!"
}
}
结合上面的配置,@PreAuthorize("hasRole('ADMIN')")
确保只有具有 ADMIN
角色的用户才能调用 findByMessage
。请注意,标准方法安全中的任何表达式都适用于 @EnableReactiveMethodSecurity
。然而,目前我们仅支持表达式的返回类型为 Boolean
或 boolean
。这意味着表达式不能阻塞。
当与WebFlux 安全集成时,Reactor Context 会根据已认证的用户由 Spring Security 自动建立:
- 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)
}
}
你可以在 hellowebflux-method 中找到一个完整的示例。