11 KiB
11 KiB
tags, create time
| tags | 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)
// 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
关联笔记
- 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 的完整流程详解(如已创建)