EnableReactiveMethodSecurity
Spring Security 透過使用 Reactor 的 Context 來支援方法安全,該 Context 由 ReactiveSecurityContextHolder 設定。以下示例展示瞭如何檢索當前登入使用者的訊息
要使此示例工作,方法的返回型別必須是 |
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)));
}
方法授權是方法前授權和方法後授權的組合。
方法前授權在方法呼叫之前執行。如果該授權拒絕訪問,則不呼叫該方法,並丟擲 |
要重現新增 @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
帶有豐富的基於表示式的支援。
-
Java
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("MYPREFIX_");
}
我們使用 |
以程式設計方式授權方法
如您所見,有幾種方法可以使用方法安全 SpEL 表示式指定非平凡的授權規則。
有多種方法可以使您的邏輯基於 Java 而不是 SpEL。這使您可以使用整個 Java 語言來提高可測試性和控制流。
在 SpEL 中使用自定義 Bean
以程式設計方式授權方法的第一種方法是一個兩步過程。
首先,宣告一個具有接受 MethodSecurityExpressionOperations
例項的方法的 bean,如下所示
-
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
例項。但請注意,首選返回一個物件,因為這不會產生生成堆疊跟蹤的開銷。
然後,當您定製如何處理授權結果時,可以訪問自定義詳細資訊。
使用自定義 Authorization Manager
以程式設計方式授權方法的第二種方法是建立一個自定義的 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)
}
}
您可以使用 |
定製表示式處理
或者,第三種方法是定製每個 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
}
}
我們使用 |
您還可以繼承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')")
確保 findByMessage
僅由具有 ADMIN
角色的使用者呼叫。請注意,標準方法安全中的任何表示式都適用於 @EnableReactiveMethodSecurity
。但是,目前我們只支援表示式的返回型別為 Boolean
或 boolean
。這意味著表示式不能阻塞。
與WebFlux Security 整合時,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 中找到一個完整的示例。