Files
cs-note/hhs/NETWORK/08-网络安全/04-JWT认证与TLS安全.md
2026-05-24 11:42:38 +08:00

320 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<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
```
```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 的完整流程详解(如已创建)