320 lines
11 KiB
Markdown
320 lines
11 KiB
Markdown
---
|
||
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 的完整流程详解(如已创建)
|