--- tags: [JWT, token, authentication, security, backend] create time: 2026-05-02 16:30 --- # JWT — JSON Web Token ## 概述 JWT(JSON Web Token, RFC 7519)是一种**开放标准**,用于在各方之间安全地传输声明(claims)。在 Web 开发中,它最常被用作无状态身份认证的载体——服务器签发一个签名的令牌,客户端在后续请求中携带该令牌来证明自己的身份。 > [!tip] 为什么选 JWT 而不是 Session? > Session 需要服务端存储用户状态,在分布式或微服务架构中必须借助 Redis 等共享存储;JWT 将所有信息打包在令牌本身,服务端只需验签即可,天然适合横向扩展的无状态场景。 > [!question] 思考 > 如果 JWT 把所有信息(包括敏感数据)都打包在令牌里,那和明文 Cookie 有什么区别? > —— 关键差异在于 **签名机制**。JWT 附带数字签名,服务端可以验证令牌是否被篡改。但注意:默认情况下 Payload 只是 Base64 编码,并非加密! ## 核心结构 JWT 由三段 Base64Url 编码的字符串用 `.` 连接而成: ``` eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. header (头部) eyJpc3MiOiJleGFtcGxlLmNvbSIsInVzZXJJZCI6NDJ9. payload (载荷) SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c signature (签名) ``` ### Header — 元信息 声明签名算法和令牌类型: ```json { "alg": "HS256", "typ": "JWT" } ``` 常用算法对比: | 算法 | 底层机制 | 密钥类型 | 典型场景 | | ----------------- | ------------------ | -------------- | -------------- | | `HS256` / `HS512` | HMAC + SHA-256/512 | 对称(同一密钥签名+验签) | 小型项目、快速原型 | | `RS256` | RSA + SHA-256 | 非对称(私钥签名,公钥验签) | 生产环境、多服务验签 | | `ES256` | ECDSA + P-256 | 非对称(密钥更短,性能更好) | 移动端、IoT 资源受限场景 | ### Payload — 声明数据 承载要传递的信息,分为三类标准字段和自定义字段: ```json { "iss": "auth.example.com", // Issuer — 签发者 "iat": 1714627200, // Issued At — 签发时间(Unix 时间戳) "exp": 1714713600, // Expiration — 过期时间 "sub": "user_42", // Subject — 主题(通常是用户 ID) "role": "admin", // 自定义字段 "permissions": ["read", "write"] // 自定义字段 } ``` > [!warning] 安全红线 > **Payload 绝对不要存放密码、身份证号等敏感信息!** Base64 编码不等于加密,任何人拿到令牌都可以解码看到内容。如需保密,应使用 JWE(JWT 加密格式)或在服务端查库获取。 ### Signature — 签名验证 签名确保令牌在传输过程中未被篡改: ``` HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret ) ``` 对于非对称算法(RS256),则使用私钥签名、公钥验签。这也是微服务架构中的推荐方案——只有鉴权服务持有私钥,其他服务用公钥验签即可。 ```mermaid flowchart LR subgraph Sign["签名阶段"] A[Header + Payload] --> B["HMAC(Raw string, Secret)"] B --> C[Signature] A -.-> D["最终令牌 = Header.Payload.Signature"] end subgraph Verify["验签阶段"] E["收到的令牌"] --> F["提取前两段拼接"] F --> G["HMAC(Raw string, Secret)"] G --> H["本地签名"] I["原始签名段"] --> J{"一致?"} H --> J J -->|"是"| K["令牌合法"] J -->|"否"| L["被篡改"] end Sign --> |"下发完整令牌"| Client["Client"] Client --> |"每次请求携带"| Verify ``` ## 认证流程 典型的 JWT 认证过程涉及四个角色间的交互: ```mermaid sequenceDiagram participant C as Client participant S as Server participant DB as Database C->>S: 1. POST /login (username + password) S->>DB: 2. 校验凭据 DB-->>S: 3. 用户信息 alt 凭据正确 S->>S: 4. 生成 JWT (带 exp, iss, sub) S-->>C: 5. 返回 { accessToken, expiresIn } C->>S: 6. 后续请求带 Authorization: Bearer S->>S: 7. 验签 + 检查 exp alt 令牌有效 S-->>C: 8. 业务数据 else 令牌无效 S-->>C: 9. 401 Unauthorized end else 凭据错误 S-->>C: 10. 401 Unauthorized end ``` > [!note] Access Token vs Refresh Token > 实践中通常采用双令牌策略: > - **Access Token**:生命周期短(如 15 分钟),放在请求头中,用于访问 API > - **Refresh Token**:生命周期长(如 7 天),存储在 HTTP-only Cookie 中,用于换取新的 Access Token > > 这样即使 Access Token 泄露,影响范围也有限;而 Refresh Token 存放在 Cookie 中不受 XSS 直接窃取。 ## 代码示例 ### Go:签发与验证 JWT 使用 `golang-jwt/jwt/v5`: ```go import "github.com/golang-jwt/jwt/v5" // 签发令牌 func GenerateToken(userID uint, role string) (string, error) { claims := jwt.MapClaims{ "sub": userID, "role": role, "iat": time.Now().Unix(), "exp": time.Now().Add(15 * time.Minute).Unix(), // 15 分钟过期 } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte("your-secret-key")) } // 验证令牌 func ValidateToken(tokenString string) (*jwt.Token, error) { return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { // 确保算法符合预期,防止算法替换攻击 if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte("your-secret-key"), nil }) } ``` ### Gin 中间件:JWT 认证 结合之前 `[[hzh/GIN/3-middleware/jwt-auth-qa]]` 讨论的模式: ```go func JWTAuthRequired() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing token"}) return } // 提取 Bearer prefix parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Bearer" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid format"}) return } token, err := ValidateToken(parts[1]) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) return } // 存入 Context,供下游 handler 使用 c.Set("userID", token.Claims.(jwt.MapClaims)["sub"]) c.Next() } } ``` ### React:前端使用 JWT ```tsx import axios from 'axios'; const api = axios.create({ baseURL: '/api', }); // 自动在请求头注入 Token api.interceptors.request.use((config) => { const token = localStorage.getItem('accessToken'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // 统一处理 401,触发刷新或登出 api.interceptors.response.use( (res) => res, async (err) => { if (err.response?.status === 401) { // 尝试刷新 Token const newToken = await refreshAccessToken(); if (newToken) { localStorage.setItem('accessToken', newToken); err.config.headers.Authorization = `Bearer ${newToken}`; return api.request(err.config); } window.location.href = '/login'; // 重定向到登录页 } return Promise.reject(err); }, ); ``` > [!example] Token 存储方式对比 > > | 存储位置 | XSS 防护 | CSRF 防护 | 后端可读 | 适用场景 | > |----------|----------|-----------|----------|----------| > | **localStorage** | ❌ 易被 XSS 读取 | ✅ 不自动发送 | ❌ 需手动设置 | SPA + 有 XSRF 防护 | > | **Memory** | ✅ 不被 XSS 直接读 | ❌ — | ❌ 需手动设置 | 高安全要求(刷新即丢失) | > | **HTTP-only Cookie** | ✅ 不被 JS 访问 | ⚠️ 需额外防护 | ✅ 自动发送 | 服务端渲染 / 传统 Web | ## 常见安全问题与防御 ### 1. 算法降级攻击(Alg: None) 攻击者将 `alg` 改为 `none`,试图绕过验签: ```json {"alg": "none", "typ": "JWT"} ``` **防御**:始终在白名单中指定允许的算法: ```go if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method") } ``` ### 2. 密钥泄露与弱密钥 使用硬编码或弱密钥会让攻击者轻易伪造令牌。 **防御**: - 密钥长度 ≥ 256 bit(HS256 要求至少 32 字节) - 通过环境变量或密钥管理服务(Vault、AWS KMS)注入 - 定期轮换密钥 ### 3. 过期时间设计不合理 过长的 `exp` 增加令牌泄露后的风险窗口;过短的 `exp` 又导致频繁刷新。 **建议**: - Access Token:5 ~ 30 分钟 - Refresh Token:7 ~ 30 天 - 配合黑名单机制实现主动注销(见下文) ### 4. 重放攻击 攻击者截取合法的 JWT 令牌后反复使用。 **防御**: - 短生命周期 Access Token - 添加 `jti`(JWT ID)唯一标识,配合黑名单做一次性校验 - 在高价值操作中引入二次确认(如修改密码、支付) ## 进阶:Token 黑名单与撤销 JWT 的一个固有缺陷:**一旦签发,无法主动作废**(除非查询数据库)。在生产环境中,这意味著用户登出或密码修改后,已泄露的令牌仍可在过期前使用。 ```mermaid flowchart TD subgraph Issue["签发"] A["登录成功"] --> B["生成 JWT jti + exp + signed"] B --> C["返回给客户端"] end subgraph Use["正常使用"] D["请求携带 JWT"] --> E["验签 + 查黑名单"] E -->|"未在册"| F["放行"] E -->|"已在册"| G["拒绝"] end subgraph Revoke["撤销操作"] H["用户登出或改密"] --> I["将jti写入黑名单Redis"] I --> J["Set TTL = exp - iat"] end C --> D H --> I ``` Go 实现黑名单校验的关键逻辑: ```go func Blacklisted(ctx context.Context, jti string) bool { key := "jwt:blacklist:" + jti exists, _ := redis.Get(ctx, key).Result() return exists == "true" } func InvalidateToken(ctx context.Context, jti string, ttl time.Duration) { redis.Set(ctx, "jwt:blacklist:"+jti, "true", ttl) } ``` > [!tip] 替代方案:短期 Token + 版本控制 > 为简化实现,可以给每个用户分配一个 `tokenVersion`(存在数据库中)。签发令牌时将版本号写入 `sub` 或自定义字段。用户登出时只递增版本号,所有旧版令牌在验签通过后对比数据库版本号即可识别为失效。无需维护黑名单集合。 ## Session vs JWT:选型决策 | 维度 | Session | JWT | |------|---------|-----| | 服务端存储 | 需要(内存/Redis) | 不需要(仅验签) | | 实时注销 | ✅ 立即可效 | ❌ 需黑名单或短轮询 | | 跨域支持 | 依赖 Cookie,需处理 CORS | 任意头,天然跨域 | | 可扩展性 | 受限于共享存储 | 天然无状态,易于水平扩展 | | 令牌体积 | 极小(仅 sessionId) | 较大(含 payload) | | 客户端操控 | ❌ 不可见 | ✅ 可解码(但不可篡改) | > [!info] 如何选型? > > - **单体应用 / 传统 MVC** → Session(简单可靠,原生支持好) > - **微服务 / 前后端分离 / 移动端** → JWT(无状态,多服务共享密钥即可) > - **混合模式** → JWT 作为内部服务间通信凭证,外部入口仍可搭配 Session ## 关联笔记 - [[hzh/GIN/3-middleware/jwt-auth-qa]] — JWT 中间件中 Context 数据安全分析 - [[hzh/GIN/3-middleware]] — Gin 中间件完整机制 - [[hzh/DEV/跨域问题调试]] — CORS 配置与跨域场景