This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/NETWORK/08-网络安全/04-JWT认证与TLS安全.md
T
2026-05-17 22:27:07 +08:00

11 KiB
Raw Blame History

tags, create time
tags create time
计算机网络
JWT
OAuth2
OIDC
mTLS
TLS安全
CipherSuite
OCSP
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)
// 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 实现

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 中间件: 拦截 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
// 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 选择

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 启用 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

# 告诉浏览器"此后只能用 HTTPS"
$ curl -I https://example.com
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

# 参数解读:
# max-age=31536000  → 一年内所有请求自动转 HTTPS(60 天起步,建议一年)
# includeSubDomains → 子域也适用
# preload           → 提交到浏览器预加载列表
# 提交到 HSTS Preload List
$ curl https://hstspreload.org/api/v2/domain/example.com
# 检查状态: pending / preloaded / opt-out

# Chrome/HSTS Preload 内置列表 ≈ 50000+ 域名
# 一旦被 preload,即使第一次访问也不会发 HTTP 请求!

mTLS(双向 TLS 认证)

sequenceDiagram
    participant C as Client<br/>(持客户端证书)
    participant S as Server<br/>(持服务端证书)
    
    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
# 生成客户端证书
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

关联笔记