启用响应式方法安全

Spring Security 通过使用Reactor 的 Context 支持方法安全,这是由ReactiveSecurityContextHolder 设置的。 以下示例展示了如何检索当前已登录用户的消息:spring-doc.cadn.net.cn

要使此示例正常工作,方法的返回类型必须是org.reactivestreams.Publisher(即一个MonoFlux)。 这有助于与Reactor的Context集成。spring-doc.cadn.net.cn

使用 AuthorizationManager 启用响应式方法安全

在 Spring Security 5.8 中,我们可以通过在任意 @Configuration 实例上使用 @EnableReactiveMethodSecurity(useAuthorizationManager=true) 注解来启用基于注解的安全性。spring-doc.cadn.net.cn

这在多个方面改进了@EnableReactiveMethodSecurity@EnableReactiveMethodSecurity(useAuthorizationManager=true)spring-doc.cadn.net.cn

  1. 使用简化的 AuthorizationManager API,取代元数据源、配置属性、决策管理器和投票器。 这简化了重用和自定义。spring-doc.cadn.net.cn

  2. 支持响应式返回类型,包括Kotlin协程。spring-doc.cadn.net.cn

  3. 基于原生 Spring AOP 构建,移除了抽象层,使您可以使用 Spring AOP 的构建模块进行自定义spring-doc.cadn.net.cn

  4. 检查是否存在冲突的注解,以确保安全配置明确无歧义spring-doc.cadn.net.cn

  5. 符合 JSR-250 规范spring-doc.cadn.net.cn

对于较早版本,请参考使用@EnableReactiveMethodSecurity 的类似支持。spring-doc.cadn.net.cn

例如,以下代码将启用Spring Security的@PreAuthorize注解:spring-doc.cadn.net.cn

方法安全配置
@EnableReactiveMethodSecurity(useAuthorizationManager=true)
public class MethodSecurityConfig {
	// ...
}

将注解添加到方法(类或接口上)会相应地限制对该方法的访问。 Spring Security 的原生注解支持定义了一组属性用于该方法。 这些属性会被传递给各种方法拦截器,例如 AuthorizationManagerBeforeReactiveMethodInterceptor,由其做出实际决策:spring-doc.cadn.net.cn

方法安全注解用法
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 指的是由 SpEL 评估引擎调用的 SecurityExpressionRoot 中的方法。spring-doc.cadn.net.cn

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

方法安全 响应式布尔表达式
@Bean
public Function<Account, Mono<Boolean>> func() {
    return (account) -> Mono.defer(() -> Mono.just(account.getId().equals(12)));
}

方法授权是前置和后置方法授权的组合。spring-doc.cadn.net.cn

在方法调用之前进行前置方法授权。如果该授权拒绝访问,则不会调用该方法,并抛出AccessDeniedException异常。 在方法调用之后但在返回给调用者之前进行后置方法授权。如果该授权拒绝访问,则不会返回值,并抛出AccessDeniedException异常spring-doc.cadn.net.cn

要重现已添加@EnableReactiveMethodSecurity(useAuthorizationManager=true)的效果,默认情况下,您需要发布以下配置:spring-doc.cadn.net.cn

全预后方法安全配置
@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-doc.cadn.net.cn

自定义授权

Spring Security 的 @PreAuthorize, @PostAuthorize, @PreFilter, 和 @PostFilter 配备了丰富的基于表达式的支持。spring-doc.cadn.net.cn

此外,对于基于角色的授权,Spring Security 添加了一个默认的ROLE_前缀,在评估如hasRole这样的表达式时会使用该前缀。 您可以通过暴露一个GrantedAuthorityDefaults bean 来配置授权规则以使用不同的前缀,例如:spring-doc.cadn.net.cn

自定义 GrantedAuthorityDefaults
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
	return new GrantedAuthorityDefaults("MYPREFIX_");
}

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

以编程方式授权方法

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

有多种方式可以让你的逻辑基于 Java 而非 SpEL。 这使你可以利用完整的 Java 语言,从而提升可测试性和流程控制能力。spring-doc.cadn.net.cn

在 SpEL 中使用自定义 Bean

以编程方式授权方法的第一种方式是一个两步过程。spring-doc.cadn.net.cn

首先,声明一个包含方法的 Bean,该方法接收一个 MethodSecurityExpressionOperations 实例,如下所示:spring-doc.cadn.net.cn

@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:spring-doc.cadn.net.cn

@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 上指定的方法。spring-doc.cadn.net.cn

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

除了返回 Mono<Boolean> 之外,你还可以返回 Mono.empty() 来表示该代码放弃做出决定。

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

@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 实例。 不过请注意,返回一个对象是更推荐的做法,因为这样不会产生生成堆栈跟踪(stacktrace)的开销。spring-doc.cadn.net.cn

然后,您可以在自定义处理授权结果的方式时访问这些自定义详细信息。 自定义处理授权结果的方式spring-doc.cadn.net.cn

使用自定义授权管理器

第二种以编程方式授权方法的方式是创建一个自定义 AuthorizationManagerspring-doc.cadn.net.cn

首先,声明一个授权管理器实例,或许像下面这样:spring-doc.cadn.net.cn

@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的工作方式:spring-doc.cadn.net.cn

仅 @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);
	}
}
@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 方法拦截器之间。spring-doc.cadn.net.cn

自定义表达式处理

或者,第三种方式是自定义每个SpEL表达式的处理方式。 为此,你可以暴露一个自定义的MethodSecurityExpressionHandler,例如:spring-doc.cadn.net.cn

自定义 MethodSecurityExpressionHandler
@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
	}
}

我们通过一个 MethodSecurityExpressionHandler 方法暴露 static,以确保 Spring 在初始化 Spring Security 的方法安全 @Configuration 类之前就将其发布。spring-doc.cadn.net.cn

您也可以继承 DefaultMessageSecurityExpressionHandler 类,以添加默认表达式之外的自定义授权表达式。spring-doc.cadn.net.cn

启用响应式方法安全

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 定义为:spring-doc.cadn.net.cn

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

以下是最小的方法安全配置,在响应式应用中启用方法安全:spring-doc.cadn.net.cn

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

考虑以下类:spring-doc.cadn.net.cn

@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协程:spring-doc.cadn.net.cn

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

结合我们上面的配置,@PreAuthorize("hasRole('ADMIN')") 确保只有具有 findByMessage 角色的用户才能调用 ADMIN。 请注意,在标准方法安全表达式中适用的所有表达式同样适用于 @EnableReactiveMethodSecurity。 然而,此时我们仅支持表达式的返回类型为 Booleanboolean。 这意味着表达式必须不能阻塞。spring-doc.cadn.net.cn

当与WebFlux 安全性集成时,Spring Security 根据已认证的用户自动建立 Reactor 上下文:spring-doc.cadn.net.cn

@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中找到完整的示例。spring-doc.cadn.net.cn