ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

统一网关登录流程总结:TaoToken 统一 Key/API 通道下的 JWT 鉴权配置骨架

统一网关登录流程总结:TaoToken 统一 Key/API 通道下的 JWT 鉴权配置骨架 1. 微服务登录鉴权为什么总在网关这层翻车统一网关登录流程这件事说穿了就是一句话让所有请求在进入内网之前先在一个固定入口完成身份确认再把「你是谁」这件事安全地传给后面的业务服务。API Gateway 在这里扮演的是门卫加传令兵的角色JWT 则是那张盖了章、还能自证真伪的通行证。适合谁看正在搭微服务、被「每个服务都写一遍登录校验」折磨过的后端同学以及想把鉴权链路收敛到一处的团队。我见过太多项目登录接口散落在用户服务里订单服务自己又验一遍 token支付服务再抄一份 JWT 解析代码。结果是密钥轮换时漏改一个服务或者某个内部接口忘了加拦截器直接裸奔。更麻烦的是排查问题用户说登录失败你得挨个服务翻日志根本不知道是签发环节错了还是校验环节把合法 token 拒了。把这条链路收到 API Gateway 之后结构会清爽很多。客户端只跟网关打交道登录请求由网关转发给认证中心认证中心签发 JWT 返回后续业务请求带着Authorization: Bearer token到网关网关统一校验签名和过期时间再把用户身份写进请求头透传给下游。下游服务不再关心 token 怎么来的只读X-User-Id这类头做权限判断。这篇要交付的是一套可复制的网关鉴权配置骨架路由白名单怎么划、JWT 校验中间件怎么写、Header 透传规则怎么定以及本地怎么发请求验证整条链路通不通。接入层我用 TaoToken 的统一 Key/API 通道来承接模型侧调用这样网关既要管业务鉴权也能顺带把 AI 能力的入口统一收口不用再为每个模型单独配一套密钥。2. TaoToken 统一 Key/API 通道在鉴权链路里的位置先把 TaoToken 是什么讲清楚它是一个统一网关式的 API 接入层把多家模型的调用收敛到一个 Key、一条 API 通道上。对微服务架构来说这意味着你的网关后面挂的不只是业务服务还有一类「模型调用服务」而这类服务的凭证管理可以交给 TaoToken 统一处理不必在每个业务服务里散落一堆模型 Key。官网入口在这里https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 注意 API 地址不带 UTM 参数配置时别把跟踪参数拼进去。它和本篇 JWT 鉴权的关系是这样的业务用户的身份用 JWT 在网关层校验而网关或下游的模型调用服务访问 TaoToken 时用的是统一 Key。两套凭证各管各的——JWT 管「这个请求代表哪个用户」TaoToken Key 管「这个服务有没有权限调模型」。把这两件事分开安全边界才清晰不会出现把模型 Key 塞进前端 token 里的低级错误。实际操作上你需要先去控制台拿 Key。打开 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 页面创建一把 Key页面地址是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。这把 Key 只放在网关或后端服务的环境变量里绝对不要下发到客户端。如果你后面要接 Claude Code 这类编码工具做长期开发可以了解下 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。想先验证模型通不通用模型对话页最直接https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。接入细节看文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。注意TaoToken 是合规的 API 接入通道配置时只填官方给的 API 基址不要自行拼接来路不明的地址。3. 可复制的网关鉴权配置骨架下面这套骨架以 Spring Cloud Gateway 为例其他网关Kong、APISIX、NginxLua思路一致换的是语法不是逻辑。整体分三块路由与白名单、JWT 校验全局过滤器、Header 透传规则。3.1 路由白名单与基础配置先在application.yml里定义路由和放行路径。白名单的原则是「只放必须匿名访问的」登录、注册、健康检查、公开文档这几类其余一律走校验。spring: cloud: gateway: routes: - id: auth-service uri: http://auth-center:8081 predicates: - Path/auth/** - id: business-service uri: http://business:8082 predicates: - Path/api/** default-filters: - AddResponseHeaderX-Gateway, tao-gateway gateway: auth: # 白名单命中即跳过 JWT 校验 public-paths: - /auth/login - /auth/register - /auth/refresh - /actuator/health - /doc/** # JWT 校验相关 jwt: issuer: tao-gateway # 生产环境从环境变量注入禁止硬编码 secret: ${JWT_SECRET} clock-skew-seconds: 30这里有个容易踩的点/auth/**整段路由到认证中心但白名单只放/auth/login、/auth/register、/auth/refresh。也就是说/auth/logout这类接口仍然要带合法 token 才能访问别图省事把整个/auth/**都放行。3.2 JWT 校验全局过滤器过滤器要做四件事判断是否白名单、提取 token、校验签名与过期、把用户信息写进请求头。顺序上给最高优先级保证在业务逻辑之前执行。Component Order(-100) public class JwtAuthFilter implements GlobalFilter { Value(${gateway.auth.jwt.secret}) private String secret; private final ListString publicPaths; private final JWTVerifier verifier; public JwtAuthFilter(GatewayAuthProperties props) { this.publicPaths props.getPublicPaths(); this.verifier JWT.require(Algorithm.HMAC256(props.getJwt().getSecret())) .withIssuer(props.getJwt().getIssuer()) .acceptLeeway(props.getJwt().getClockSkewSeconds()) .build(); } Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path exchange.getRequest().getPath().value(); // 1. 白名单直接放行 if (isPublic(path)) { return chain.filter(exchange); } // 2. 提取 Bearer Token String token extractToken(exchange.getRequest()); if (token null) { return reject(exchange, 401, missing_token); } // 3. 校验签名、过期、签发者 DecodedJWT jwt; try { jwt verifier.verify(token); } catch (TokenExpiredException e) { return reject(exchange, 401, token_expired); } catch (JWTVerificationException e) { return reject(exchange, 401, token_invalid); } // 4. 透传用户身份覆盖客户端可能伪造的同名头 ServerHttpRequest mutated exchange.getRequest().mutate() .header(X-User-Id, jwt.getSubject()) .header(X-User-Name, safeClaim(jwt, username)) .header(X-User-Roles, safeClaim(jwt, roles)) .header(X-Token-Id, jwt.getId()) .build(); return chain.filter(exchange.mutate().request(mutated).build()); } private boolean isPublic(String path) { return publicPaths.stream().anyMatch(p - path.startsWith(p.replace(/**, ))); } private String extractToken(ServerHttpRequest request) { String header request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); if (header ! null header.startsWith(Bearer )) { return header.substring(7); } return null; } private String safeClaim(DecodedJWT jwt, String name) { Claim claim jwt.getClaim(name); return claim.isNull() ? : claim.asString(); } private MonoVoid reject(ServerWebExchange exchange, int status, String code) { exchange.getResponse().setStatusCode(HttpStatus.valueOf(status)); exchange.getResponse().getHeaders().add(Content-Type, application/json); byte[] body ({\error\:\ code \}).getBytes(StandardCharsets.UTF_8); return exchange.getResponse().writeWith(Mono.just( exchange.getResponse().bufferFactory().wrap(body))); } }关键点在于第 4 步mutate().header(...)是覆盖而不是追加。如果客户端自己塞了一个X-User-Id: 999网关会用 JWT 里的真实 subject 把它盖掉下游拿到的永远是可信值。这一步不做等于把权限判断的钥匙交给了调用方。3.3 Header 透传规则与下游约定透传规则要写成文档让所有下游服务遵守同一套约定否则又会出现「这个服务读 X-Uid那个服务读 X-User-Id」的混乱。Header 名来源含义下游是否可信任X-User-IdJWT sub用户唯一标识是网关已覆盖X-User-NameJWT username用户名是X-User-RolesJWT roles角色列表逗号分隔是X-Token-IdJWT jtiToken 唯一 ID用于黑名单是Authorization客户端原样原始 token否下游不应再解析注意下游服务必须只信任网关写入的X-User-*头并且要在部署层面禁止外部流量绕过网关直连服务端口。否则攻击者直接打服务端口自己伪造X-User-Id就绕过了整条鉴权链。3.4 登录签发与刷新接口骨架认证中心负责签发这里给一个最小可用的签发逻辑重点是把jti、sub、roles、exp都带上。public String issueAccessToken(User user) { Instant now Instant.now(); return JWT.create() .withIssuer(tao-gateway) .withSubject(String.valueOf(user.getId())) .withJWTId(UUID.randomUUID().toString()) .withClaim(username, user.getName()) .withClaim(roles, String.join(,, user.getRoles())) .withIssuedAt(Date.from(now)) .withExpiresAt(Date.from(now.plus(30, ChronoUnit.MINUTES))) .sign(Algorithm.HMAC256(secret)); }Access Token 给 30 分钟Refresh Token 单独用不透明随机串存 Redis有效期 7 天只用于换新 token不参与业务请求。这样即使 Access Token 泄露窗口期也有限。4. 本地验证请求与成功结果配置写完别急着上环境先在本地把链路跑通。分三步起服务、拿 token、带 token 访问受保护接口。第一步用环境变量注入密钥再启动网关避免密钥进代码库。export JWT_SECRETlocal-dev-secret-change-me mvn spring-boot:run -pl gateway第二步调登录接口拿 token。假设认证中心在 8081网关在 8080。curl -s -X POST http://localhost:8080/auth/login \ -H Content-Type: application/json \ -d {username:alice,password:correct-password}成功时返回类似{ accessToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..., refreshToken: rft_9f2c..., expiresIn: 1800 }第三步带 token 访问受保护接口同时故意伪造一个X-User-Id验证网关是否覆盖。TOKENeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... curl -s http://localhost:8080/api/profile \ -H Authorization: Bearer $TOKEN \ -H X-User-Id: 999在下游服务里打印收到的头你应该看到X-User-Id是 alice 的真实 ID而不是 999。这一步验证通过说明透传规则生效了。再验证拒绝路径不带 token 访问/api/profile应返回 401 和missing_token带一个过期 token应返回token_expired。curl -i http://localhost:8080/api/profile # HTTP/1.1 401 Unauthorized # {error:missing_token}如果网关后面挂了模型调用服务顺手验证一下 TaoToken 通道。用统一 Key 发一次请求确认 API 基址和鉴权头都对curl -s https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_KEY \ -H Content-Type: application/json \ -d {model:gpt-4o-mini,messages:[{role:user,content:ping}]}返回正常内容就说明模型侧通道通了。想更直观地看模型响应可以直接在模型对话页试https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。5. 本篇常见错误排查5.1 401 一直返回 token_invalid但 token 明明没过期九成是密钥不一致。签发用的 secret 和网关校验用的 secret 不是同一个或者签发时用了withIssuer(auth-center)校验时却写withIssuer(tao-gateway)。issuer 不匹配会直接抛JWTVerificationException。排查方法把 token 贴到 jwt.io 看 payload 里的iss再对照网关配置。5.2 白名单不生效登录接口也被拦看isPublic的匹配逻辑。上面示例里用path.startsWith(p.replace(/**, ))如果白名单写的是/auth/login而实际请求路径带了 context-path 变成/gateway/auth/login就匹配不上。要么统一去掉 context-path要么白名单里写全路径。另外注意startsWith的边界问题/auth/login会匹配到/auth/login-extra严格场景应该用精确匹配或正则。5.3 下游拿到的 X-User-Id 是客户端伪造的值检查过滤器里是不是用了header(name, value)的追加语义。Spring 的mutate().header()是覆盖但如果你用的是headers(h - h.add(...))那就是追加客户端伪造的头会排在前面下游getFirst拿到的就是假值。统一用覆盖写法并在下游约定只读第一个值。5.4 时钟偏移导致 token 刚签发就过期容器时间不同步时签发方和校验方差几十秒短有效期 token 会频繁报过期。配置里加acceptLeeway(30)容忍 30 秒偏移同时确保所有节点接 NTP。别把 leeway 调太大那等于变相延长了 token 有效期。5.5 刷新接口被自己的鉴权拦死/auth/refresh在白名单里但它需要读 Refresh Token。如果 Refresh Token 放在请求头X-Refresh-Token里白名单放行后认证中心自己校验即可不要让它走 JWT 过滤器。如果误把 refresh 接口放进受保护路径用户 token 一过期就永远刷不了只能重新登录。5.6 TaoToken 调用返回 401先确认 Key 是从控制台复制的完整串没有多余空格再确认请求头是Authorization: Bearer key不是自定义头名。API 基址用 https://taotoken.net/api 不要带 UTM 参数。如果还是不通去 API Keys 页面重新生成一把再试https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。6. 把鉴权链路收口到网关之后整套骨架跑通后你会发现新增一个业务服务变得很轻不用再写登录校验只要约定好读X-User-*头注册到网关路由即可。密钥轮换、黑名单、限流这些横切关注点全都在网关一处维护。如果你还在用散落各处的模型 Key建议顺手把模型调用也收到统一通道上。TaoToken 的接入文档里有各语言的调用示例照着改环境变量就行https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。长期做编码和 Agent 开发的Coding Plan 那条线也值得看一眼https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。最后留一个我踩过的坑网关的 JWT 过滤器一定要在压测环境验证一遍并发表现JWT 解析是 CPU 操作QPS 高的时候会成为瓶颈。加一层 Caffeine 缓存解析结果key 用 token 的 jtiTTL 设短于 token 有效期能明显降 CPU。但缓存别存太久否则黑名单生效会有延迟。
返回列表