You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Spring Cloud Gateway中通过Spring Security自定义授权校验HTTP POST请求Payload

问题:Spring Cloud Gateway路由的请求体与JWT声明匹配校验实现

我正尝试保护Spring Cloud Gateway的部分路由,具体要求如下:

  • 用户必须通过OAuth2认证才能访问这些路由(未认证则返回HTTP 401);
  • JWT访问令牌的"scp"声明必须包含特定值(我的场景中为"2fa",不满足则返回HTTP 403);
  • JSON请求体中的"user"属性必须与JWT访问令牌的"sub"声明值一致(不满足则返回HTTP 403)。

前两项要求已实现,但第三项的实现资料极少,求可运行的示例。


现有配置

application.yaml(生产环境)

...
spring:
  profiles: production

  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: ${AUTH_URL}/oidc
          jwk-set-uri: ${AUTH_URL}/oidc/jwks.json
...

SecurityWebFilterChain配置

...
 @Bean
  @Order(Ordered.HIGHEST_PRECEDENCE - 3)
  public SecurityWebFilterChain secondFactorScopeApiHttpSecurity(ServerHttpSecurity http) {
    final ServerWebExchangeMatcher baseScopeEndpointsMatcher = new OrServerWebExchangeMatcher(
        new PathPatternParserServerWebExchangeMatcher("/api/fhir"),
        new PathPatternParserServerWebExchangeMatcher("/api/fhir/List**"),
        new PathPatternParserServerWebExchangeMatcher("/api/fhir/Observation**")
    );

    http.securityMatcher(baseScopeEndpointsMatcher)
        .authorizeExchange(exchanges -> exchanges.anyExchange().hasAuthority("SCOPE_2fa"))
        .oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt);
    return http.build();
  }
...

实现请求体与JWT sub声明匹配校验的方案

方案1:自定义ReactiveAuthorizationManager(推荐,集成Spring Security授权流程)

这种方案直接融入Spring Security的授权逻辑,更贴合框架设计:

  1. 创建自定义授权管理器,负责提取请求体user属性并与JWT的sub声明对比:
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class RequestBodyUserMatchAuthorizationManager implements ReactiveAuthorizationManager<ServerWebExchange> {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, ServerWebExchange exchange) {
        // 提取JWT中的sub声明
        Mono<String> jwtSub = authentication
                .filter(auth -> auth.getPrincipal() instanceof Jwt)
                .map(auth -> ((Jwt) auth.getPrincipal()).getClaim("sub"));

        // 读取请求体并提取user属性
        Mono<String> requestUser = exchange.getRequest().getBody()
                .collectList()
                .map(dataBuffers -> {
                    try {
                        byte[] bytes = new byte[dataBuffers.stream().mapToInt(dataBuffer -> dataBuffer.readableByteCount()).sum()];
                        for (var buffer : dataBuffers) {
                            buffer.read(bytes);
                        }
                        JsonNode root = objectMapper.readTree(bytes);
                        return root.get("user").asText();
                    } catch (Exception e) {
                        return null;
                    }
                });

        // 对比两者是否一致,一致则授权通过
        return Mono.zip(jwtSub, requestUser)
                .map(tuple -> tuple.getT1().equals(tuple.getT2()))
                .map(AuthorizationDecision::new)
                .defaultIfEmpty(new AuthorizationDecision(false));
    }
}
  1. 修改SecurityWebFilterChain配置,替换原有授权规则:
...
 @Bean
  @Order(Ordered.HIGHEST_PRECEDENCE - 3)
  public SecurityWebFilterChain secondFactorScopeApiHttpSecurity(ServerHttpSecurity http) {
    final ServerWebExchangeMatcher baseScopeEndpointsMatcher = new OrServerWebExchangeMatcher(
        new PathPatternParserServerWebExchangeMatcher("/api/fhir"),
        new PathPatternParserServerWebExchangeMatcher("/api/fhir/List**"),
        new PathPatternParserServerWebExchangeMatcher("/api/fhir/Observation**")
    );

    http.securityMatcher(baseScopeEndpointsMatcher)
        .authorizeExchange(exchanges -> exchanges
            .anyExchange()
            .access(new RequestBodyUserMatchAuthorizationManager())) // 使用自定义授权管理器
        .oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt);
    return http.build();
  }
...

方案2:自定义ServerFilter(灵活独立,适合单独校验场景)

如果不想修改Spring Security的授权流程,可以添加一个Gateway过滤器单独处理:

  1. 实现自定义过滤器:
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpStatus;

@Component
public class RequestUserMatchFilter extends AbstractGatewayFilterFactory<RequestUserMatchFilter.Config> {

    private final ObjectMapper objectMapper = new ObjectMapper();

    public RequestUserMatchFilter() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            // 获取JWT的sub声明
            Mono<String> jwtSub = ReactiveSecurityContextHolder.getContext()
                    .map(ctx -> ctx.getAuthentication())
                    .filter(auth -> auth.getPrincipal() instanceof Jwt)
                    .map(auth -> ((Jwt) auth.getPrincipal()).getClaim("sub"));

            // 读取请求体并提取user属性,注意读取后要重新写入请求体
            Mono<String> requestUser = exchange.getRequest().getBody()
                    .collectList()
                    .map(dataBuffers -> {
                        try {
                            byte[] bytes = new byte[dataBuffers.stream().mapToInt(dataBuffer -> dataBuffer.readableByteCount()).sum()];
                            for (var buffer : dataBuffers) {
                                buffer.read(bytes);
                            }
                            // 重新包装请求体,避免后续组件无法读取
                            exchange.getRequest().mutate().body(Mono.just(bytes)).build();
                            JsonNode root = objectMapper.readTree(bytes);
                            return root.get("user").asText();
                        } catch (Exception e) {
                            return null;
                        }
                    });

            // 校验匹配结果
            return Mono.zip(jwtSub, requestUser)
                    .flatMap(tuple -> {
                        if (tuple.getT1().equals(tuple.getT2())) {
                            return chain.filter(exchange);
                        } else {
                            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
                            return exchange.getResponse().setComplete();
                        }
                    })
                    .onErrorResume(e -> {
                        exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
                        return exchange.getResponse().setComplete();
                    });
        };
    }

    public static class Config {
        // 可添加配置参数,比如指定需要校验的路径
    }
}
  1. 在路由配置中启用该过滤器:
spring:
  cloud:
    gateway:
      routes:
        - id: fhir_route
          uri: ${FHIR_SERVICE_URL}
          predicates:
            - Path=/api/fhir/**
          filters:
            - RequestUserMatchFilter

注意事项

  • 读取请求体后必须重新写入,否则后续过滤器或服务无法获取请求内容;
  • 需处理请求体解析失败(如非JSON格式)的情况,直接返回403;
  • 如果是GET等无请求体的请求,可根据业务需求添加跳过逻辑或返回错误。

内容的提问来源于stack exchange,提问作者pmenze

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 03:30:53