EnableReactiveMethodSecurity

本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

Spring Security 通过使用 Reactor的Context 支持方法安全(method security),Context 是由 ReactiveSecurityContextHolder 设置的。下面的例子显示了如何检索当前登录用户的信息。

为了让这个例子生效,方法的返回类型必须是 org.reactivestreams.Publisher(也就是 MonoFlux)。这对于与Reactor的 Context 整合是必要的。

使用 AuthorizationManager 的 EnableReactiveMethodSecurity

在 Spring Security 5.8 中,我们可以在任何 @Configuration 实例上使用 @EnableReactiveMethodSecurity(useAuthorizationManager=true) 注解来启用基于注解的security。

这在很多方面改进了 @EnableReactiveMethodSecurity

@EnableReactiveMethodSecurity(useAuthorizationManager=true)

  1. 使用简化的 AuthorizationManager API,而不是元数据源、配置属性、决策管理器和投票器。这简化了重复使用和定制。

  2. 支持响应式返回类型。请注意,我们正在等待 Spring框架提供更多的协程支持,然后再添加协程支持。

  3. 使用原生的Spring AOP构建,去除抽象,允许你使用Spring AOP构建模块来定制。

  4. 检查冲突的注解以确保security配置的明确性

  5. 符合JSR-250标准

对于早期版本,请阅读 @EnableReactiveMethodSecurity 的类似支持。

例如,下面将启用 Spring Security 的 @PreAuthorize 注解。

Method Security Configuration
  • Java

@EnableReactiveMethodSecurity(useAuthorizationManager=true)
public class MethodSecurityConfig {
	// ...
}

给一个方法(在类或接口上)添加注解,就会相应地限制对该方法的访问。Spring Security的原生注解支持为方法定义一组属性。这些将被传递给各种方法拦截器,比如 AuthorizationManagerBeforeReactiveMethodInterceptor,以便它做出实际决定。

Method Security Annotation Usage
  • 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评估引擎(SpEL evaluation engine)调用的方法。

@bean 指的是你定义的一个自定义组件,其中 apply 可以返回 BooleanMono<Boolean> 来表示授权决定。一个这样的 bean 可能看起来像这样。

Method Security Reactive Boolean Expression
  • 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来配置授权规则以使用不同的前缀,就像这样。

Custom MethodSecurityExpressionHandler
  • Java

@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
	return new GrantedAuthorityDefaults("MYPREFIX_");
}

我们使用静态方法暴露 GrantedAuthorityDefaults,以确保Spring在初始化Spring Security 的方法安全 @Configuration 类之前发布它。

自定义授权管理器(Authorization Manager)

方法授权是方法前和方法后授权的结合。

在方法被调用之前会进行方法前(Before-method)授权。如果该授权拒绝访问,该方法就不会被调用,并且会抛出一个 AccessDeniedException。方法后(After-method)授权是在方法被调用后,但在方法返回给调用者之前进行的。如果该授权拒绝了访问,该值不会被返回,并且会抛出一个 AccessDeniedException

为了重现添加 @EnableReactiveMethodSecurity(useAuthorizationManager=true) 的默认操作,你将发布以下配置。

Full Pre-post Method Security Configuration
  • 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 来定制,就像这样。

Publish Custom Advisor
  • Java

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

你可能想在你的应用程序中只支持 @PreAuthorize,在这种情况下,你可以做以下工作。

Only @PreAuthorize Configuration
  • Java

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

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

或者,你可能有一个自定义的前方法(before-method) ReactiveAuthorizationManager,你想添加到列表中。

在这种情况下,你需要告诉Spring Security,ReactiveAuthorizationManager 以及你的授权管理器适用于哪些方法和类。

因此,你可以这样配置Spring Security,在 @PreAuthorize@PostAuthorize 之间调用你的 ReactiveAuthorizationManager

Custom Before 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方法拦截器之间。

同样的情况也可以用在方法后(after-method)授权上。方法之后的授权通常涉及分析返回值以验证访问。

例如,你可能有一个方法来确认所请求的账户确实属于登录的用户,像这样。

@PostAuthorize example
  • Java

public interface BankService {

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

你可以提供你自己的 AuthorizationMethodInterceptor 来定制如何评估对返回值的访问。

例如,如果你有自己的自定义注解,你可以这样配置它。

Custom After 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

@EnableReactiveMethodSecurity 也支持 Kotlin 的协程(coroutine),尽管只是在有限的程度上。当拦截协程(coroutine)时,只有第一个拦截器参与其中。如果有任何其他拦截器存在,并且在Spring Security的方法安全拦截器之后, 它们将被跳过

  • 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")
}

下面的最小方法安全(method security)配置了响应式应用程序中的方法安全。

  • 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 协程(coroutine)。

  • Kotlin

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

结合我们上面的配置,@PreAuthorize("hasRole('ADMIN')") 确保 findByMessage 只被拥有 ADMIN 角色的用户调用。请注意,标准方法安全中的任何表达式都对 @EnableReactiveMethodSecurity 有效。然而,在这个时候,我们只支持表达式的返回类型为 Booleanboolean。这意味着该表达式不能阻塞。

当与 with WebFlux Security 集成时,Spring Security 会根据认证的用户自动建立Reactor Context。

  • 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 中找到一个完整的样本。