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