啟用 ReactiveMethodSecurity

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

為了使此示例正常工作,方法的返回型別必須是 org.reactivestreams.Publisher(即 MonoFlux)。這是與 Reactor 的 Context 整合所必需的。

帶有 AuthorizationManager 的 EnableReactiveMethodSecurity

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

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

方法前授權在方法被呼叫之前執行。如果授權拒絕訪問,則方法不會被呼叫,並丟擲 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 來配置授權規則以使用不同的字首,如下所示

自定義 GrantedAuthorityDefaults
  • 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,該 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<AuthorizationResult> authorize(Supplier<Authentication> authentication, MethodInvocation invocation) {
        // ... authorization logic
    }

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

}

然後,釋出帶有切入點的方法攔截器,該切入點對應於您希望 ReactiveAuthorizationManager 執行的時間。例如,您可以像這樣替換 @PreAuthorize@PostAuthorize 的工作方式

僅 @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,如下所示

自定義 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')") 確保 findByMessage 僅由具有 ADMIN 角色的使用者呼叫。請注意,標準方法安全性中的任何表示式都適用於 @EnableReactiveMethodSecurity。但是,目前,我們只支援表示式的返回型別為 Booleanboolean。這意味著表示式不能阻塞。

當與 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((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);
	}
}
@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 中找到完整的示例。

© . This site is unofficial and not affiliated with VMware.