4. JWT令牌深度实践
在API网关中,JWT(JSON Web Token)通常作为无状态身份凭证使用。网关拿到令牌后,需要完成三件事:验证它确实由可信签发方签发、确认它当前仍然有效、并在必要时提前让它失效。这三件事分别对应签名校验、过期处理与黑名单机制,而密钥轮换则是让签名校验在长期运行中保持安全的关键运维能力。本节围绕这四条主线展开,给出可落地的实现思路与参数示例。
一、签名校验:先验结构,再验签名
JWT由三部分组成:头部(Header)、载荷(Payload)和签名(Signature),以点号分隔。头部声明算法,例如 {"alg":"RS256","kid":"k-2024-07"};载荷携带声明,例如 sub、iss、aud、exp、jti;签名是对前两段拼接结果的密码学保护。网关校验顺序建议为:解析结构 → 校验头部算法是否在白名单内 → 根据 kid 选择公钥 → 验证签名 → 校验标准声明。
算法白名单非常重要。若网关允许 alg 为 none,攻击者可去掉签名直接伪造令牌;若同时接受HS256与RS256,还可能被“算法混淆”攻击利用,用公钥当HMAC密钥重签。因此生产网关应显式限定为 RS256 或 ES256,并拒绝 none。下面是一段典型的校验伪代码:
claims = parse_jwt(token)
assert claims.header.alg in ["RS256", "ES256"]
assert claims.header.kid in key_store
pubkey = key_store.get(claims.header.kid)
assert verify_signature(token, pubkey) == true
assert claims.payload.iss == "https://auth.example.com"
assert claims.payload.aud == "api-gateway"
assert now < claims.payload.exp
assert now >= claims.payload.nbf - leeway
其中 leeway 是时钟偏移容忍量,示例取60秒,用于吸收网关与签发方之间的时间差。校验失败时应返回401,并在响应头或日志中记录失败原因码,例如 invalid_signature、token_expired、unknown_kid,便于排障。
二、过期处理:exp、nbf与刷新边界
JWT的过期由 exp 声明控制,网关只需比较当前时间与 exp。但实际系统还要处理 nbf(生效时间)和 iat(签发时间)。常见策略是:访问令牌有效期设为15分钟,刷新令牌设为7天。网关只校验访问令牌,刷新动作由认证服务完成。这样既保证无状态校验的高效,又通过短有效期降低泄露风险。
过期处理的关键是“边界一致性”。若网关使用UTC时间,签发方也必须使用UTC;若网关允许60秒偏移,刷新逻辑也应允许相同偏移,否则会出现“网关认为已过期、认证服务认为仍可刷新”的窗口。建议在网关配置中集中定义:
jwt:
clock_skew_seconds: 60
access_token_ttl: 900
refresh_token_ttl: 604800
require_exp: true
require_nbf: false
当令牌过期时,网关返回401并附带 WWW-Authenticate: Bearer error="invalid_token", error_description="token_expired",客户端据此触发刷新流程,而不是直接跳转登录。
三、密钥轮换:kid与JWKS的协作
密钥轮换的目标是:在不中断现有令牌校验的前提下,平滑切换到新密钥。实现依赖两个机制:头部中的 kid 和JWKS端点。签发方发布JWKS文档,包含当前公钥集合;网关定期拉取并缓存。轮换时,签发方先生成新密钥对,将新公钥加入JWKS,再用新私钥签发新令牌;旧公钥保留一段时间,直到所有旧令牌过期后再移除。
轮换窗口应大于访问令牌TTL。例如访问令牌15分钟,旧公钥至少保留30分钟。网关侧需要处理 unknown_kid:若遇到未知 kid,先触发一次JWKS强制刷新,再重试校验;仍失败才拒绝。这样可避免因缓存延迟导致误判。JWKS缓存建议设置 max-age=300,并支持主动刷新。
四、黑名单机制:让无状态令牌可控失效
JWT本身无状态,签发后无法直接撤销。黑名单机制用于处理“令牌未过期但必须失效”的场景,例如用户登出、密码修改、权限降级或检测到异常登录。实现方式通常有两种:一是按 jti 精确拉黑单个令牌;二是按 sub 或 tenant 拉黑某个主体在某个时间点之前签发的所有令牌。
网关侧的黑名单校验应尽量轻量。推荐使用Redis存储,键设计为 jwt:blacklist:jti:{jti},值为失效原因,TTL设置为令牌剩余有效期,避免无限增长。对于按主体拉黑,可存储 jwt:blacklist:sub:{sub},值为“生效时间戳”,校验时比较令牌的 iat 是否早于该时间戳。示例:
SET jwt:blacklist:jti:abc123 "logout" EX 900
SET jwt:blacklist:sub:user-42 "1720000000" EX 604800
校验逻辑为:先查 jti 是否命中,再查 sub 是否命中且 iat 早于生效时间。命中则返回401,错误码 token_revoked。为降低Redis压力,可在网关本地缓存黑名单查询结果1~5秒,但登出等强一致场景应关闭本地缓存或缩短TTL。
五、组合策略与排障要点
把四条主线组合起来,网关的JWT处理顺序应为:解析 → 算法白名单 → kid取公钥 → 签名验证 → exp/nbf校验 → 黑名单校验 → 注入身份信息。任何一步失败都短路返回,并记录结构化日志,字段包括 kid、jti、sub、失败原因和耗时。
常见排障场景包括:unknown_kid 多为JWKS缓存未刷新;invalid_signature 多为密钥不匹配或算法混淆;token_expired 需检查时钟偏移与TTL配置;token_revoked 需确认黑名单键是否误写或TTL过短。建议在网关暴露 /health/jwks 与 /metrics/jwt 两个端点,分别用于检查公钥缓存状态和统计各类失败计数,使轮换与黑名单问题可观测、可定位。