Java 程序员第 45 阶段08:网关统一路由大模型接口,配合 Nacos 配置治理,网关层统一鉴权:JWT/OAuth2 Token 校验与下游转发实战
前 3 篇我们让网关路由动态、实例动态但开放的大门谁都能进。大模型接口有两个特点让鉴权变得格外重要**一是调用按 token 计费、成本高**二是**模型能力是核心资产、必须按租户/权限隔离**。如果鉴权分散在每个大模型微服务里各自实现不仅重复劳动还会出现某服务漏校验、被刷爆额度的木桶短板。本篇要做的是在网关层**统一**完成 Token 校验JWT 解析、OAuth2 校验、权限判断全在网关一处做完校验通过后把合法的身份信息用户、租户、权限范围透传给下游微服务下游无需再验一次。这就是统一鉴权 下游转发的核心思想。为什么鉴权要放在网关层JWT 与 OAuth2 的核心概念Gateway 全局过滤器鉴权原理实战JWT 校验 GlobalFilter实战Token 透传下游Header 转发OAuth2 资源服务器模式最佳实践与踩坑点本篇小结1. 为什么鉴权要放在网关层把鉴权下沉到每个微服务会带来三个问题**重复实现**10 个大模型微服务10 份验签代码升级算法要改 10 处**安全水位不一**某个新服务忘了校验就成了突破口**无法统一限权**按租户、按模型、按额度的访问控制分散难以集中策略。网关作为所有流量的唯一入口天然适合做**边界安全Perimeter Security**所有请求先过网关这一道合法才放行。这正好呼应我们系列主题——网关统一路由大模型接口统一路由的同时也统一鉴权。维度各服务各自鉴权网关统一鉴权---------实现成本每服务一份网关一处安全一致性易有短板统一水位策略变更改 N 处、重启 N 次改网关 Nacos 治理下游负担每跳都验只网关验下游信任透传注意网关统一鉴权不等于下游完全不校验。零信任架构下内部服务间仍可保留轻量校验mTLS/内部 token但**面向外部的第一次校验一定在网关**避免无效请求穿透到昂贵的推理资源。2. JWT 与 OAuth2 的核心概念**JWTJSON Web Token** 是一种自包含的令牌payload 里带着 sub用户、tenant租户、scope权限范围、exp过期时间等声明并用签名保证不可篡改。网关拿到 JWT 后**本地验签**即可无需每次查库——这对高并发大模型网关非常关键。**OAuth2** 是授权框架。常见做法是客户端先用 client_id/secret 或密码换到 access_tokenJWT 形式之后每个请求带 Authorization: Bearer token。网关作为**资源服务器Resource Server**校验这个 token 的合法性与 scope。典型大模型调用链路客户端 - 授权服务器换 token - 带 token 访问网关网关验签 查 scope(是否含 llm:chat) 是否过期通过 - 把 sub/tenant 写入 Header - 转发下游微服务下游读取 Header 中的身份信息直接信任不再验签对大模型场景JWT 的 payload 通常至少包含声明含义用途---------sub用户 ID计费、审计tenant租户 ID多租户隔离scope权限范围是否允许调用 chat/embeddingsmodel_quota模型额度额度控制结合第09篇限流exp过期时间防重放3. Gateway 全局过滤器鉴权原理Gateway 的 GlobalFilter 会作用于**所有**路由正是做统一鉴权的绝佳位置。GlobalFilter 通过 ServerWebExchange 拿到请求校验不通过就直接 setComplete返回 401/403通过则放行并在 exchange 上附加信息。public interface GlobalFilter {MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain);}过滤器的执行顺序由 Ordered 接口getOrder()决定。**鉴权过滤器必须排在最前面**order 取最小负值如 Ordered.HIGHEST_PRECEDENCE确保它还早于路由匹配、负载均衡等后续过滤器——非法请求在进路由前就被拦下不浪费任何推理资源。与第 06 篇动态路由、第 07 篇服务发现的关系鉴权发生在 Route 被选中、转发下游之前因此它可以读取路由 metadata如某路由要求的 scope做路由级权限判断——这正是 Nacos 治理的延伸在 Nacos 路由配置里加 requiredScope 字段网关按路由校验 scope。4. 实战JWT 校验 GlobalFilter下面给一个可直接用的 JWT 校验全局过滤器支持从 Nacos 动态读取密钥配合配置治理。ComponentOrder(Ordered.HIGHEST_PRECEDENCE) // 最先执行public class JwtAuthGlobalFilter implements GlobalFilter {// 公钥/密钥可由 Nacos 配置热更新结合 RefreshScope 或监听Value(${gateway.jwt.secret:})private String jwtSecret;private final ObjectMapper objectMapper new ObjectMapper();// 免鉴权白名单如 /actuator/health、OAuth2 的 /oauth/tokenprivate static final SetString WHITE_LIST Set.of(/oauth/token, /actuator/health, /_gateway/admin/login);Overridepublic MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) {String path exchange.getRequest().getURI().getPath();if (WHITE_LIST.stream().anyMatch(path::startsWith)) {return chain.filter(exchange);}String auth exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);if (!StringUtils.hasText(auth) || !auth.startsWith(Bearer )) {return unauthorized(exchange, 缺少 Authorization Bearer Token);}String token auth.substring(7);try {// 本地验签 解析使用 JJWT 或 auth0 库Claims claims Jwts.parser().setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes())).parseClaimsJws(token).getBody();// 过期校验库会自动抛 ExpiredJwtExceptionString tenant claims.get(tenant, String.class);String sub claims.getSubject();// 把身份信息写入 Header供下游使用ServerHttpRequest mutated exchange.getRequest().mutate().header(X-User-Id, sub).header(X-Tenant-Id, tenant).header(X-Scopes, String.join(,, claims.get(scope, List.class))).build();return chain.filter(exchange.mutate().request(mutated).build());} catch (ExpiredJwtException e) {return unauthorized(exchange, Token 已过期);} catch (JwtException e) {return unauthorized(exchange, Token 非法);}}private MonoVoid unauthorized(ServerWebExchange exchange, String msg) {exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);exchange.getResponse().getHeaders().add(Content-Type, application/json);byte[] bytes ({\error\:\ msg \}).getBytes(StandardCharsets.UTF_8);DataBuffer buffer exchange.getResponse().bufferFactory().wrap(bytes);return exchange.getResponse().writeWith(Mono.just(buffer));}}要点**验签在本地完成**不依赖外部授权服务网关自身不成为瓶颈通过 request.mutate().header(...) 把身份信息注入下游 Header下游直接读 X-Tenant-Id 即可失败直接 401**请求在最前端就被挡下**保护昂贵的大模型算力。5. 实战Token 透传下游Header 转发上一节我们把身份信息写进了 X-User-Id 等内部 Header。但有时下游服务尤其是第三方大模型代理要求**原始 Bearer Token 继续携带**比如网关后面的代理服务要代用户去调 OpenAI需要用用户自己的 key。这时有两种策略**策略 A透传原始 Token**。在白名单/特定路由上把 Authorization 原样转发// 在转发过滤器前保留 Authorization默认 Gateway 不会剥离它// 仅需注意不要在日志里打印该 Header**策略 B替换为内部服务 Token**。更安全的做法是网关校验完用户 Token 后**换成内部服务间 token** 再转发避免用户原始 token 扩散到内网ComponentOrder(Ordered.HIGHEST_PRECEDENCE 10)public class InternalTokenFilter implements GlobalFilter {Value(${gateway.internal.token})private String internalToken;Overridepublic MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest req exchange.getRequest().mutate().header(Authorization, Internal internalToken) // 覆盖为内部 token.build();return chain.filter(exchange.mutate().request(req).build());}}配合 Nacos 配置治理把 gateway.internal.token、gateway.jwt.secret 放进 Nacos密钥轮换时只改 Nacos全网关热更新——这正是系列主题配合 Nacos 配置治理在安全侧的落地。6. OAuth2 资源服务器模式如果团队已有 OAuth2 授权服务器如 Keycloak、Authing可以让网关以**资源服务器**方式校验 JWT省去手写验签。Spring Security 提供 spring-security-oauth2-resource-server配合 Gateway 的 TokenRelay 过滤器还能把 token 自动转发。spring:security:oauth2:resourceserver:jwt:issuer-uri: https://auth.example.com/realms/llm# 或指定 jwk-set-uri 拉取公钥jwk-set-uri: https://auth.example.com/realms/llm/protocol/openid-connect/certs// 启用 TokenRelay自动把 Bearer token 透传给下游Beanpublic RouteLocator routes(RouteLocatorBuilder b) {return b.routes().route(llm, r - r.path(/v1/llm/**).filters(f - f.tokenRelay()) // 透传原始 token.uri(lb://llm-openai-proxy)).build();}TokenRelay 与我们的自定义 JwtAuthGlobalFilter 并不冲突全局过滤器负责拒非法的tokenRelay() 负责放行的请求带上原 token。但若你已用全局过滤器做了完整校验并改用内部 token就不再需要 tokenRelay。**二选一避免双重透传造成混淆**。7. 最佳实践与踩坑点踩坑点现象正确做法---------鉴权过滤器顺序靠后非法请求已进入路由、打到推理服务才被拒order 设最小负值早于路由匹配公钥写死代码密钥轮换要重启密钥/公钥放 Nacos热更新白名单过宽内部接口被外网打白名单精确到路径禁用 ** 通配根透传 token 进日志token 泄露到日志系统日志脱敏禁止打印 Authorization不校验 scope低权限用户调高权限模型按路由 requiredScope 校验 scopeJWT 不验 aud/iss跨系统 token 被复用校验 aud受众与 iss签发者下游完全信任内网横向越权内部仍保留轻量 mTLS/内部 token工程建议**路由级权限**在 Nacos 路由 JSON 的 metadata 里加 requiredScope: llm:chat网关取路由 metadata 校验实现不同大模型接口不同权限治理粒度到路由。**多租户隔离**把 tenant 写进 Header下游按 tenant 做数据/额度隔离防止 A 租户刷 B 租户额度。**失败快速返回**401/403 不要走完整过滤器链直接 setComplete省算力。**审计日志**放行时在 Nacos 治理的审计通道记录 sub/tenant/path/time便于大模型调用计费与溯源。8. 本篇小结本篇我们把网关统一路由大模型接口 Nacos 配置治理补上安全一环——**网关层统一鉴权**鉴权放在网关入口所有大模型请求先过统一校验非法请求不触及推理资源用 GlobalFilter最高优先级做 JWT 本地验签 scope 校验失败即 401校验通过后把 sub/tenant/scope 写入内部 Header 透传下游下游直接信任密钥、内部 token、路由级 requiredScope 全部由 Nacos 配置治理热更新、可审计已有 OAuth2 体系可用资源服务器 tokenRelay 模式与自定义过滤器二选一。至此网关已动态路由 动态实例 统一鉴权。下一篇第 09 篇我们给大模型接口加保险丝——**网关限流熔断Sentinel 与 Gateway 整合保护大模型接口**防止单点洪峰打垮昂贵算力。