--- tags: [计算机网络, JWT, OAuth2, OIDC, mTLS, TLS安全, CipherSuite, OCSP] create time: 2026-05-18 05:10 --- # JWT 认证与 TLS 安全实践 ## 概述 本章覆盖两个关键领域:Web API 最常用的认证机制 JWT 的安全性陷阱,以及 HTTPS/TLS 在生产环境中的正确配置方式。这两者是保护 API 安全的左右手。 ## JWT 安全深度剖析 ### JWT 结构回顾 ``` eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. ← Header (base64url) eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE1MTYyMzkwMjJ9. ← Payload (base64url) SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signature (HMACSHA256) ``` ```json // Header {"alg":"HS256","typ":"JWT"} // Payload {"sub":"1234567890","name":"John","iat":1516239022,"exp":1516242622} ``` ### JWT 六大常见漏洞 | # | 漏洞 | 攻击手法 | 修复方案 | |---|------|---------|---------| | 1 | **alg: none** | 篡改 header 为 `{"alg":"none"}`, 签名变为空字符串即可通过验证 | 服务端严格白名单验签算法 | | 2 | **无 exp 字段** | Token 永远有效,一旦泄露无法撤销 | 设置合理的 TTL(通常 ≤ 1h)+ refresh token | | 3 | **弱密钥** | HS256 用短/简单密钥可暴力破解 | 使用 ≥ 32 字节的随机密钥,或改用 RS256/ES256 | | 4 | **密钥复用** | 多个服务用同一个 secretKey | 每个服务独立密钥,或使用 JWK Set 动态获取公钥 | | 5 | **时钟偏差** | 服务器时间不同步导致 exp 判断异常 | NTP 同步 + leeway 容忍窗口 | | 6 | **敏感数据明文** | base64 ≠ 加密,payload 中放了密码/身份证 | payload 只能放非敏感声明 (standard claims) | ### 安全的 JWT 实现 ```go import ( "github.com/golang-jwt/jwt/v5" "crypto/rand" "time" ) // 生成足够强度的密钥(至少 32 bytes) func generateSecretKey() []byte { key := make([]byte, 32) rand.Read(key) return key } // ✅ 安全的 JWT 签发 const jwtExpiry = 15 * time.Minute // access token 有效期短 func GenerateJWT(userID string) (string, error) { claims := jwt.MapClaims{ "sub": userID, // subject: 用户 ID "iat": time.Now().Unix(), // issued at "exp": time.Now().Add(jwtExpiry).Unix(), // ⚠️ 必须有 exp! "nbf": time.Now().Unix(), // not before (防止提前使用) "jti": randomUUID(), // JWT ID: 唯一标识,用于黑名单撤销 } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(secretKey) } // ✅ 安全的 JWT 验证 func VerifyJWT(tokenStr string) (*jwt.Token, error) { token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) { // 1. 强制校验算法(防止 alg: none 攻击) if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return secretKey, nil }) if err != nil || !token.Valid { return nil, fmt.Errorf("invalid token") } // 2. 提取并检查标准声明 claims, ok := token.Claims.(jwt.MapClaims) if !ok { return nil, fmt.Errorf("invalid claims format") } // 3. 检查 jti 是否在黑名单中(手动撤销场景) jti, _ := claims["jti"].(string) if blacklist.Check(jti) { return nil, fmt.Errorf("token revoked") } return token, nil } ``` ### Refresh Token 模式 ``` access token: 有效期 15 分钟,用于每次 API 请求 refresh token: 有效期 7 天,用于换取新的 access token ─────────────────────────────────────────────────── ┌─────────────┐ ┌──────────────┐ │ Client │ │ Auth Server │ └──────┬──────┘ └──────┬───────┘ │ │ │ 1. Login → access + refresh │ │─────────────────────────────────→│ │ │ │ 2. API request with access token │ │─────────────────────────────────→│ │ (15min later...) │ │ │ │ 3. access expired → 401 │ │←─────────────────────────────────│ │ │ │ 4. exchange refresh for new │ │─────────────────────────────────→│ │ new access token │ │←─────────────────────────────────│ │ │ │ 5. Continue with new access token│ │─────────────────────────────────→│ ``` ```go // Go 中间件: 拦截 401 并自动刷新 func authMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { token := extractBearerToken(r) claims, err := VerifyJWT(token) if err != nil { // token 无效 → 尝试用 refresh token 换新 token rt := extractRefreshToken(r) newAccessToken, err := RefreshAccessToken(rt) if err != nil { http.Error(w, "unauthorized", 401) return } r.Header.Set("Authorization", "Bearer "+newAccessToken) } next(w, r) } } ``` ## OAuth 2.0 vs OpenID Connect (OIDC) | 协议 | 定位 | 典型场景 | 返回内容 | |------|------|---------|---------| | **OAuth 2.0** | Authorization Framework | 第三方代用户操作(微信登录、GitHub 授权) | access_token(有时有 refresh_token)| | **OIDC** | Authentication Overlay on OAuth 2.0 | 身份验证(确认"你是谁") | id_token (JWT) + user info | ```go // OAuth 2.0 Authorization Code Flow 简化版 func oauthLoginHandler(w http.ResponseWriter, r *http.Request) { state := randomState() url := fmt.Sprintf( "https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&state=%s", clientID, redirectURI, state, ) http.Redirect(w, r, url, http.StatusFound) } func oauthCallbackHandler(w http.ResponseWriter, r *http.Request) { // 1. 验证 state 防 CSRF // 2. code exchange → access_token token, _ := client.Exchange(ctx, r.URL.Query().Get("code")) // 3. use token to fetch user info user, _ := client.UserInfos.Get(token) } ``` ## TLS 安全最佳实践 ### TLS 版本与 Cipher Suite 选择 ```go import "crypto/tls" // ✅ Go 中生产环境 TLS 配置 tlsConfig := &tls.Config{ MinVersion: tls.VersionTLS12, // 最低 TLS 1.2(TLS 1.3 优先) MaxVersion: tls.VersionTLS13, // 限制在 1.3(更安全、更快) // 首选 Elliptic Curve CurvePreferences: []tls.CurveID{ tls.X25519, // 首选 Ed25519 曲线(最快最安全) tls.CurveP256, // NIST P-256(广泛兼容) }, // ALPN 协商(告诉客户端用什么应用层协议) NextProtos: []string{"h2", "http/1.1"}, // 服务器优先选择 cipher suite PreferServerCipherSuites: true, // RC4 必须禁用(已知弱点) // Go 1.15+ 已移除所有 RC4 cipher suite } ``` ### 推荐与禁用的 Cipher Suite 速查 ``` ✅ 推荐使用 (TLS 1.3): ├── TLS_AES_256_GCM_SHA384 (首选) ├── TLS_CHACHA20_POLY1305_SHA256 (移动端友好) └── TLS_AES_128_GCM_SHA256 (备选) ⚠️ 勉强可用 (TLS 1.2): ├── ECDHE-RSA-AES256-GCM-SHA384 ├── ECDHE-RSA-CHACHA20-POLY1305 └── ECDHE-ECDSA-... (如果用的是 EC 证书) ❌ 必须禁用: ├── RSA 密钥交换 (无前向保密 PFS) ├── CBC 模式 (BEAST/Sweet32 攻击) ├── SHA-1 哈希 (碰撞攻击) ├── RC4 (严重泄漏漏洞) ├── 3DES (Sweet32) └── 出口级加密 (export ciphers) ``` ### OCSP Stapling —— 证书吊销检查的优化 ``` 传统 OCSP (慢 + 隐私泄露): OCSP Stapling (快 + 隐私保护): Browser → CA: Is cert valid? Server → CA: Fetch status + sign CA → Browser: Valid/Invalid Server caches stapled response Server → Browser: cert + stale Server sends: cert + signed assertion checking happens in browser Browser validates CA-signed assertion directly from server 问题: 优势: • 每次都要连 CA • 无需浏览器直连 CA • 暴露浏览习惯给 CA • 速度快 (缓存有效期内无需额外查询) • 有些 CA 响应慢 • 隐私好 (服务器知道你在访问谁) ``` ```nginx # Nginx 启用 OCSP Stapling ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s; # 确保完整的证书链(包括 intermediate CA) ssl_trusted_certificate /etc/ssl/certs/fullchain.pem; ``` ### HSTS —— 强制 HTTPS ```bash # 告诉浏览器"此后只能用 HTTPS" $ curl -I https://example.com Strict-Transport-Security: max-age=31536000; includeSubDomains; preload # 参数解读: # max-age=31536000 → 一年内所有请求自动转 HTTPS(60 天起步,建议一年) # includeSubDomains → 子域也适用 # preload → 提交到浏览器预加载列表 ``` ```bash # 提交到 HSTS Preload List $ curl https://hstspreload.org/api/v2/domain/example.com # 检查状态: pending / preloaded / opt-out # Chrome/HSTS Preload 内置列表 ≈ 50000+ 域名 # 一旦被 preload,即使第一次访问也不会发 HTTP 请求! ``` ## mTLS(双向 TLS 认证) ```mermaid sequenceDiagram participant C as Client
(持客户端证书) participant S as Server
(持服务端证书) C->>S: ClientHello + Client Certificate S->>C: ServerHello + Server Certificate Note over S,C: 双方互相验证书! S->>S: 验证客户端证书 CN/ SAN C->>C: 验证服务端证书 (常规) alt 双方证书都有效 Note over S,C: ✅ 建立 mTLS 连接 else 客户端证书无效/过期 S-->>C: Alert: bad_certificate 🔴 end ``` ```bash # 生成客户端证书 openssl req -newkey rsa:2048 -nodes -keyout client.key -out client.csr -subj "/CN=user1" openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out client.crt -days 365 -extfile client.ext # Nginx 启用 mTLS ssl_certificate /etc/ssl/server.crt; ssl_certificate_key /etc/ssl/server.key; ssl_client_certificate /etc/ssl/ca.crt; # CA 根证书 ssl_verify_client on; # on / optional / off ``` ## 关联笔记 - [[hhs/NETWORK/TLS安全实践]] — OCSP Stapling / HSTS / Cipher Suite 的详细展开 - [[hhs/NETWORK/DdoS与MITM防御]] — DDoS MitM 的互补安全知识 - [[hhs/NETWORK/Web应用攻击面]] — CSRF/XSS/XXE 的 Web 应用防护 - [[hhs/OAuth2/01-OAuth2基础]] — OAuth 2.0 的完整流程详解(如已创建)