自定义 Delegating ReactiveAuthorizationManager 以验证所有匹配规则,而不是放弃第一个匹配的规则

Customize DelegatingReactiveAuthorizationManager to verify all matching rules, instead of giving up on the first that matches

我有一个授权模型相当复杂的应用程序。 在数据库中,我可以拥有指定特定端点所需权限的条目,并在应用程序启动时加载它们。

示例(方法、模式、权限):

  1. GET - /api/service1/someEntity - GET_SERVICE1_SOME_ENTITY_AUTHORITY
  2. GET - /api/service2/someEntity - GET_SERVICE2_SOME_ENTITY_AUTHORITY
  3. GET - /api/service1/** - GET_SERVICE1_ANYTHING_AUTHORITY
  4. null - /** - DO_ANYTHING (管理员)

在我加载这些规则之前,我将它们从最具体到最一般排序,因此它们最终的顺序与上面完全相同。我这样加载它们(对于每个排序顺序):

http.authorizeExchange()
    .pathMatchers(permission.getMethod(), permission.getPattern())
    .hasAuthority(permission.getAuthority());

现在的问题是,当我有用户:ADMIN 和权限 DO_ANYTHING 时,由于 spring 安全的工作方式,它实际上无法做任何事情.

假设 admin 想要访问 /api/service1/someEntity。 Spring 将匹配第 1 条规则并检查 admin 是否具有权限 GET_SERVICE1_SOME_ENTITY_AUTHORITY 并且结果证明他没有。此时,Spring 将拒绝访问。但我想让它做的是检查其他匹配规则(数字 3 和 4)。

我找到了 the code responsible for this:

public final class DelegatingReactiveAuthorizationManager implements ReactiveAuthorizationManager<ServerWebExchange> {
// ...
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, ServerWebExchange exchange) {
        return Flux.fromIterable(this.mappings).concatMap((mapping) -> mapping.getMatcher().matches(exchange)
                .filter(MatchResult::isMatch).map(MatchResult::getVariables).flatMap((variables) -> {
                    logger.debug(LogMessage.of(() -> "Checking authorization on '"
                            + exchange.getRequest().getPath().pathWithinApplication() + "' using "
                            + mapping.getEntry()));
                    return mapping.getEntry().check(authentication, new AuthorizationContext(exchange, variables));
                })).next().defaultIfEmpty(new AuthorizationDecision(false));
    }
// ...
}

不幸的是,我不知道如何用我的替换这个实现。

tldr: 我希望 spring 安全验证与请求路径匹配的所有安全规则,而不是拒绝对第一个匹配的规则的访问。

好的,我想出了一个解决方案,虽然有点老套,但它满足了我的需要。

我已经创建了自己的 ReactiveAuthorizationManager<ServerWebExchange> 实现,它与 Spring 的 DelegatingReactiveAuthorizationManager 几乎相同,但有一个额外的过滤器,可以完成这项工作:

public class MyAuthorizationManager implements ReactiveAuthorizationManager<ServerWebExchange> {

    private final List<ServerWebExchangeMatcherEntry<ReactiveAuthorizationManager<AuthorizationContext>>> mappings;

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, ServerWebExchange exchange) {
        return Flux.fromIterable(this.mappings)
                   .concatMap((mapping) -> mapping.getMatcher()
                                                  .matches(exchange)
                                                  .filter(ServerWebExchangeMatcher.MatchResult::isMatch)
                                                  .map(ServerWebExchangeMatcher.MatchResult::getVariables)
                                                  .flatMap((variables) -> {
                                                      log.debug(LogMessage.of(() -> "Checking authorization on '"
                                                              + exchange.getRequest()
                                                                        .getPath()
                                                                        .pathWithinApplication() + "' using "
                                                              + mapping.getEntry()).toString());
                                                      return mapping.getEntry()
                                                                    .check(authentication, new AuthorizationContext(exchange, variables));
                                                  }).filter(AuthorizationDecision::isGranted) /* <- this is what I needed */)
                   .next()
                   .defaultIfEmpty(new AuthorizationDecision(false));
    }
}

我是这样注册的:

 @Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        
        List<Permission> permissions = repo.getPermissions()

        MyAuthorizationManager.Builder authBuilder = MyAuthorizationManager.builder();

        permissions.forEach(permission -> {
            // I create the mappings here, so matcher + auth manager for that matcher
            authBuilder.add(
                    ServerWebExchangeMatchers.pathMatchers(permission.getMethod(), permission.getPattern()), 
                    AuthorityReactiveAuthorizationManager.hasAuthority(permission.getAuthority())
            );
        });
        
        http.authorizeExchange().anyExchange().permitAll()
            .and()
            .httpBasic()
            .and()
            .formLogin().disable()
            .anonymous().disable();

        // it will be before the default authorization filter, bu actually it doesn't really matter
        http.addFilterBefore(new AuthorizationWebFilter(authBuilder.build()), SecurityWebFiltersOrder.AUTHORIZATION);

        return http.build();
}

我已经离开 .authorizeExchange().anyExchange().permitAll() 因为它设置了一些默认的东西,比如入口点、异常 Web 过滤器,我不想复制更多代码并完全放弃默认管理器。