JWT 认证与安全边界¶
要点卡¶
30 秒回答
JWT 是 claims 的令牌格式,不是登录协议;OAuth 2.0 是授权框架,OIDC 才在其上定义身份层。
常见三段 compact JWT 是签名 JWS,payload 仅 Base64URL 编码并不保密。资源服务必须固定允许的
算法和 token profile,校验签名、iss/aud/exp/nbf、必要的 typ 与可信 kid;
claims 只表达签发时事实,实时权限和高风险操作仍需服务端授权。
3 分钟展开
- Access token 短期化并绑定目标资源 audience/scope;refresh token 只交给授权服务器,并按能力使用 rotation/reuse detection。
- JWKS 从受信 issuer 配置获取并缓存,未知
kid可受控刷新但要防攻击者制造刷新风暴;轮换期允许新旧 key 重叠。 - 不同 token 类型使用互斥校验规则,避免把 ID token、access token 或其他 issuer 的 JWT 互换使用。
- 浏览器优先 Authorization Code + PKCE/BFF 等当前安全模式;不把 refresh token 暴露给无保护存储,Cookie 场景另做 CSRF 防护。
| 记忆槽 | 内容 |
|---|---|
| 三个不变量 | JWT 不是协议;payload 不保密;验签成功不等于当前有业务权限 |
| 手画图 | AS signs → client presents access token → resource server validates profile → authz |
| 项目落点 | Agent Platform 讲 tenant/audience/scope;内部身份 header 必须由可信网关重建并保护服务间这一跳 |
| 一个取舍 | 本地验签低延迟、少依赖,但即时吊销和实时权限需短 TTL、状态检查或不透明 token/introspection |
错误表达
- ❌ “能解码 JWT 就认证成功;OAuth 就是 JWT;微服务统一用 RS256 一定最安全。”
- ✅ “算法与 token profile 由部署明确固定;验签后仍校验 issuer、audience、时效、类型与业务授权。”
自测追问:为什么 ID token 不能直接当 access token?未知 kid 到来时如何安全刷新 JWKS?
10 分钟版(原理 + 图示)¶
结构
| 部分 | 内容 |
|---|---|
| Header | alg、kid、可选 typ 等;只能按服务端受信策略解释 |
| Payload | sub, exp, roles, tenant_id |
| Signature | HMAC/RSA 签名 |
sequenceDiagram
Client->>Auth: login
Auth->>Client: access JWT + refresh
Client->>API: Authorization Bearer JWT
API->>API: 验签 exp iss aud
API->>Svc: 内部 user_id header / metadata
Note over Client,API: 泄露 JWT = 在 exp 前等同账号
安全边界
- 不该做:在未加密 JWT 中存密码、银行卡或不必要 PII;无 exp;把 token 中的
alg/jku/x5u当可信配置;密钥硬编码仓库。 - 该做:算法/key/token-type 白名单 + 完整签名 + exp/nbf/iss/aud 校验;设置有限 clock skew;
密钥放 KMS/HSM 或受控 key service 并支持重叠轮换;TLS。Cookie 型凭证除
HttpOnly/Secure/SameSite外,还要按场景校验 Origin 或使用 CSRF token。 - 与 Session:Session 可服务端立即失效;JWT 需补充机制(短 TTL、refresh rotation、token family 检测重放)。
生产场景¶
- 微服务:授权服务器按固定 profile 签发,各资源服务从受信 issuer/JWKS 获取验证 key,
校验 audience/scope;
tenant_id只是授权输入,仍要验证 membership 与资源所有权。 - 强制下线:用户改密后
token_version++,JWT 带verclaim,不匹配拒绝。 - BFF:浏览器 HttpOnly refresh,SPA 内存持 access,减少 XSS 窃取窗口。
排查与工具¶
| 工具 | 用途 |
|---|---|
| jwt.io | 解码调试(勿贴生产 token) |
| golang-jwt/jwt | 解析验证 |
| OWASP ZAP | 认证测试 |
| 审计日志 | 异常 iss/alg |
路径:401 突发 → 是否密钥轮换不同步 → exp 时钟漂移 → 中间件是否校验 alg 白名单。
架构取舍¶
| 方案 | 适用 | 不适用 |
|---|---|---|
| 本地验证 JWT | 多实例、低延迟资源服务 | 需即时全量吊销或每次读取实时权限 |
| Session + Redis | 可控失效 | 扩展/redis 依赖 |
| OAuth2/OIDC | 第三方登录 | 纯内部简单场景 |
| mTLS 服务间 | 零信任内网 | 移动端 |
| API Key | 机器对机器 | 用户登录 |
深挖问答¶
- JWT 和 Session 区别? → JWT 客户端持票自证;Session 服务端存状态。
- 如何吊销? → 短 TTL、黑名单、refresh rotation、ver claim。
- HS256 vs 非对称 JWS? → HMAC 的签发与验证方共享 secret,验证方也有伪造能力; 非对称方案可只分发公钥。具体算法按协议 profile、库支持和密钥治理选择,不能只背 RS256。
- XSS 偷 token? → HttpOnly refresh + CSP;access 放内存缩短窗口。
- Go 怎么验? →
jwt.ParseWithClaims同时固定算法、issuer、audience、expiration 等规则, 再做 token type、subject 与业务授权校验;不能只调用Valid()。 kid怎么用? → 只在本地受信 key set/JWKS 中查找;不要按 token 中任意jku/x5uURL 下载密钥。
反模式与事故¶
- 接受
alg: none——伪造任意用户(库需jwt.WithValidMethods)。 - Payload 存
isAdmin: true可客户端改——有签名改不了,但误用未验签的中间层。 - Refresh token 无限续期无 rotation——被盗永久有效。
- 日志打印完整 Authorization header——token 泄露进 ELK。
代码示例¶
import "github.com/golang-jwt/jwt/v5"
type Claims struct {
UserID uint64 `json:"uid"`
TenantID uint64 `json:"tid"`
jwt.RegisteredClaims
}
func parseToken(tokenStr string, key *rsa.PublicKey) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(
tokenStr,
claims,
func(t *jwt.Token) (any, error) { return key, nil },
jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()}),
jwt.WithIssuer("https://auth.example.com"),
jwt.WithAudience("orders-api"),
jwt.WithExpirationRequired(),
jwt.WithLeeway(30*time.Second),
)
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
Gin 中间件在 c.Set("claims", claims) 后 c.Next(),失败则 AbortWithStatusJSON(401, ...)。