如何在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的授权逻辑,更贴合框架设计:
- 创建自定义授权管理器,负责提取请求体
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)); } }
- 修改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过滤器单独处理:
- 实现自定义过滤器:
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 { // 可添加配置参数,比如指定需要校验的路径 } }
- 在路由配置中启用该过滤器:
spring: cloud: gateway: routes: - id: fhir_route uri: ${FHIR_SERVICE_URL} predicates: - Path=/api/fhir/** filters: - RequestUserMatchFilter
注意事项
- 读取请求体后必须重新写入,否则后续过滤器或服务无法获取请求内容;
- 需处理请求体解析失败(如非JSON格式)的情况,直接返回403;
- 如果是GET等无请求体的请求,可根据业务需求添加跳过逻辑或返回错误。
内容的提问来源于stack exchange,提问作者pmenze
相关产品推荐
相关产品推荐

