EnableReactiveMethodSecurity

Spring Security 透過使用 Reactor 的 Context 來支援方法安全性,Context 由 ReactiveSecurityContextHolder 設置。以下範例示範如何檢索目前登入使用者的訊息

為了使此範例運作,方法的回傳型別必須是 org.reactivestreams.Publisher (即 MonoFlux)。這對於與 Reactor 的 Context 整合是必要的。

使用 AuthorizationManager 啟用 ReactiveMethodSecurity

在 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 註解

方法安全性組態
  • 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 可以回傳 BooleanMono<Boolean> 以指示授權決策。這樣的 Bean 可能看起來像這樣

方法安全性反應式布林運算式
  • Java

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

自訂授權

Spring Security 的 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 隨附豐富的基於運算式的支援。

此外,對於基於角色的授權,Spring Security 新增了預設的 ROLE_ 字首,在評估 hasRole 等運算式時使用。您可以透過公開 GrantedAuthorityDefaults Bean 來組態授權規則以使用不同的字首,如下所示

自訂 MethodSecurityExpressionHandler
  • 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)。

自訂授權管理器

方法授權是方法前授權和方法後授權的組合。

方法前授權在調用方法之前執行。如果該授權拒絕存取,則不會調用該方法,並且會拋出 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 建構的。因此,攔截器會根據指定的順序調用。這可以透過在攔截器實例上調用 setOrder 來自訂,如下所示

發布自訂 Advisor
  • Java

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

您可能只想在您的應用程式中支援 @PreAuthorize,在這種情況下,您可以執行以下操作

僅限 @PreAuthorize 組態
  • Java

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

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

或者,您可能有一個自訂的方法前 ReactiveAuthorizationManager,您想要新增至清單中。

在這種情況下,您需要告訴 Spring Security ReactiveAuthorizationManager 以及您的授權管理器適用於哪些方法和類別。

因此,您可以組態 Spring Security 在 @PreAuthorize@PostAuthorize 之間調用您的 ReactiveAuthorizationManager,如下所示

自訂前置 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 方法攔截器之間。

方法後授權也可以執行相同的操作。方法後授權通常關注分析回傳值以驗證存取權。

例如,您可能有一個方法確認請求的帳戶實際上屬於已登入的使用者,如下所示

@PostAuthorize 範例
  • Java

public interface BankService {

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

您可以提供自己的 AuthorizationMethodInterceptor 來客製化如何評估對回傳值的存取權。

例如,如果您有自己的自訂註解,您可以像這樣組態它

自訂後置 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 攔截器之後調用。

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。但是,目前,我們僅支援運算式回傳型別為 Booleanboolean。這表示運算式不得封鎖。

當與 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 中找到完整的範例。