Files
cs-note/hhs/DEV/鉴权策略/OAuth2与MFA/OAuth2-and-MFA.md
T
2026-05-24 11:42:38 +08:00

737 lines
25 KiB
Markdown
Raw 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:
- OAuth2
- MFA
- Authentication
- Authorization
- Security
- TOTP
- OpenID-Connect
- 2FA
create time: 2026-05-17 20:00
---
# OAuth2 + MFA
## 概述
本文档系统梳理 **OAuth 2.0**(授权框架)与 **多因素认证(MFA)** 的核心原理、设计模式和工程实践。涵盖 OAuth2 的四种授权流程、MFA 的多因素分类、两者在真实系统中的集成方式(包括 MFA-challenged 场景),以及 Go 后端和 React/TypeScript 前端的完整示例。旨在让读者能够从零搭建一个支持 MFA 的企业级认证体系。
> [!question] 思考题
> OAuth2 本质上是**授权协议**而非认证协议。那为什么业界常用它来做登录?又如何在 OAuth2 之上叠加 MFA 能力?带着这两个问题开始阅读。
---
## 一、OAuth2 核心概念
### 1.1 四个角色
```mermaid
graph LR
A["用户 (Resource Owner)"] -->|"授予权限"| B["客户端 (Client)"]
B -->|"请求令牌"| C["授权服务器 (AS)"]
C -->|"发放 Access Token"| B
B -->|"携带 Token"| D["资源服务器 (RS)"]
D -->|"返回受保护资源"| B
style A fill:#e1f5fe
style B fill:#fff3e0
style C fill:#e8f5e9
style D fill:#fce4ec
```
| 角色 | 职责 | 常见身份 |
|------|------|---------|
| Resource Owner | 资源拥有者 | 终端用户 |
| Client | 发起请求的应用 | Web App / Mobile App / SPA |
| Authorization Server | 验证用户并发放令牌 | Keycloak / Auth0 / 自研 |
| Resource Server | 托管受保护资源的 API | 后端微服务 |
> [!tip] 关键认知
> 很多系统中 AS 和 RS 部署在同一域名下(如 `auth.example.com` 和 `api.example.com`),对外表现为一个统一平台。但它们在架构上是解耦的。
### 1.2 四种授权流程(Grant Types)
| Grant Type | 适用场景 | 安全性 |
|-----------|---------|-------|
| **Authorization Code** | 服务端渲染的后端应用(SPA + Backend) | ★★★★★ |
| **Authorization Code + PKCE** | SPA、移动端等无法保密 Client Secret 的场景 | ★★★★★ |
| **Implicit Flow** | ~~已废弃~~ 不推荐使用 | ★★★ |
| **Client Credentials** | 机器对机器通信(Service-to-Service) | ★★★★☆ |
| **Device Code** | 无浏览器设备(IoT、电视) | ★★★★ |
#### Authorization Code + PKCE(推荐方案)
这是当前最佳实践,尤其适合前后端分离架构:
```mermaid
sequenceDiagram
participant U as 用户
participant C as Client(SPA)
participant AS as Auth Server
participant RS as Resource Server
U->>C: 1. 点击"登录"
C->>C: 2. 生成 code_verifier & code_challenge
C->>AS: 3. 重定向到 /authorize?code_challenge=S256
AS->>U: 4. 展示登录页 + MFA 提示
U->>AS: 5. 输入密码 + TOTP 验证码
AS->>C: 6. 回调 /callback?code=AUTH_CODE
C->>AS: 7. 用 code + code_verifier 换 token
AS->>C: 8. 返回 {access_token, refresh_token, id_token}
C->>RS: 9. 携带 access_token 请求 API
RS->>RS: 10. 验证签名并返回资源
```
> [!question] 为什么需要 PKCE?
> 传统的 Authorization Code 要求客户端持有 `client_secret`。但对于浏览器端或移动端的 SPA 应用,Secret 必然暴露在前端代码中——任何拦截都能窃取。PKCE 通过每次请求动态生成 `code_challenge`(基于 `code_verifier`),确保只有发起请求的客户端能用该 authorization code 换取 token,即使 code 被截获也无法利用。
### 1.3 Token 类型
```typescript
// Access Token — 短期有效,用于访问 API
interface AccessToken {
sub: string; // 用户唯一标识
iss: string; // 签发者
aud: string[]; // 目标受众(资源服务器)
exp: number; // 过期时间(通常 15min ~ 1hr)
iat: number; // 签发时间
scopes: string[]; // 授权范围
mfa_verified?: boolean; // MFA 是否已验证
session_id: string; // 关联会话
}
// Refresh Token — 长期有效,用于获取新的 Access Token
// ⚠️ 绝对不应该出现在前端浏览器中
interface RefreshToken {
sub: string;
jti: string; // Token ID(用于吊销)
exp: number; // 过期时间(通常 7 ~ 30 days)
device_fingerprint?: string;
}
```
> [!warning] 安全陷阱
> Access Token 存 `localStorage` 还是 `memory`?答案:**永远放内存**。`localStorage` 和 `sessionStorage` 可被任意 JavaScript 读取,是 XSS 攻击的天然蜜罐。HTTP-only Cookie 适合后端渲染,但不适用于纯 SPA。
---
## 二、多因素认证(MFA)基础
### 2.1 三大因素分类
```mermaid
quadrantChart
title "MFA 因素对比"
x-axis "低用户体验" --> "高用户体验"
y-axis "技术成熟度高" --> "技术成熟度低"
"短信验证码": [0.4, 0.6]
"邮箱验证码": [0.5, 0.75]
"TOTP (Google Authenticator)": [0.75, 0.9]
"生物识别 (指纹/人脸)": [0.85, 0.85]
"硬件密钥 (YubiKey)": [0.6, 0.7]
"Push Notification": [0.9, 0.8]
```
| 因素 | 例子 | 优势 | 劣势 |
|------|------|------|------|
| **Knowledge**(所知) | 密码、PIN | 零成本,用户熟悉 | SIM 卡劫持、钓鱼可绕过 |
| **Possession**(所有) | 手机 app、硬件密钥 | 独立于密码 | 依赖设备可用性 |
| **Inherence**(所是) | 指纹、面部识别 | 无缝体验 | 隐私争议、误识率 |
### 2.2 TOTP 算法详解(RFC 6238)
TOTP(Time-based One-Time Password)是目前最常用的 Possession 因素实现。
#### TOTP 算法流程
```
输入:共享密钥 K + 当前时间 T
输出:6~8 位数字验证码
步骤:
1. T = floor(当前 Unix Timestamp / 30) // 每 30 秒一个窗口
2. Counter = big-endian byte representation of T
3. HMAC-SHA256(K, Counter) → 20 字节结果
4. Dynamic Truncation → 提取 4 字节
5. Modulo 10^digits → 6 或 8 位数字
```
> [!example] 为什么是 30 秒?
> 30 秒是安全窗口和安全性的折衷——太短用户来不及操作,太长增加了被盗用的风险。配合 ±1 个窗口的漂移容忍(允许前后两个值),实际容错范围是 90 秒。
#### Go 实现 TOTP 验证
```go
package totp
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"fmt"
"math"
"time"
)
// Verify 校验 TOTP 码,tolerance 表示允许的时间窗口偏移量
func Verify(secret, code string, digits int, tolerance int, now time.Time) bool {
window := now.Unix() / 30
for offset := -tolerance; offset <= tolerance; offset++ {
if verifyAtWindow(secret, code, int(window+offset), digits) {
return true
}
}
return false
}
func verifyAtWindow(secret, code string, window, digits int) bool {
mac := hmac.New(sha256.New, []byte(secret))
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, uint64(window))
mac.Write(buf)
hash := mac.Sum(nil)
offset := hash[len(hash)-1] & 0x0F
codeBytes := binary.BigEndian.Uint32(hash[offset : offset+4]) & 0x7FFFFFFF
computed := int(codeBytes) % int(math.Pow10(digits))
expected := fmt.Sprintf("%06d", computed)
// constant-time comparison, 防止 timing attack
return hmac.Equal([]byte(expected), []byte(code))
}
```
> [!note] 关键点解释
> - `secret` 是服务器与客户端共享的基础密钥(Base32 编码),通过 QR 码首次绑定
> - `tolerance=1` 是最常用的配置,提供 ±30s 的容差
> - 比较时使用 `hmac.Equal` 进行 constant-time comparison,防止 timing attack
### 2.3 WebAuthn / FIDO2(下一代 MFA)
WebAuthn 由 W3C 定义,取代传统 TOTP,将"possessing a device"变为"possessing a cryptographic key bound to a domain"。
```
流程:
注册:
1. 客户端请求凭证创建 → 服务端生成 challenge
2. 浏览器调用 navigator.credentials.create()
3. 用户通过生物识别/PIN 授权
4. 硬件安全模块(TPM/Safe Area)生成公私钥对
5. 公钥 + attestation 返回服务端,服务端持久化
认证:
1. 登录时服务端发送 challenge
2. 浏览器调用 navigator.credentials.get()
3. 硬件验证身份后使用私钥签名
4. 服务端用已存储的公钥验证签名
```
> [!tip] 为什么 WebAuthn 比 TOTP 更安全?
> - **抗钓鱼**:密钥绑定特定 origin,攻击者无法诱骗用户使用其网站的密钥
> - **私钥不出设备**:私钥永远不会离开安全硬件
> - **无需共享密钥**:消除了二维码泄露导致的全局风险
---
## 三、OAuth2 + MFA 的深度集成
### 3.1 MFA-Challenged 模式
这是企业级认证中最关键的集成点。当用户认证成功但尚未完成 MFA 验证时,授权服务器应返回 `mfa_required` 错误,而非直接发放 token。
```
┌───────────── 第一次请求 ─────────────┐
│ Client → /authorize │
│ 用户输入用户名 + 密码 │
│ ✅ 密码正确 │
│ ❓ MFA 状态? │
│ → 需要 MFA │
│ │
│ Client ← { │
│ "error": "mfa_required", │
│ "challenge_id": "uuid-xxxx", │
│ "supported_factors": ["totp","webauthn"] │
│ } │
└──────────────────────────────────────┘
┌───────────── 第二次请求(补充 MFA)───┐
│ Client → /token │
│ headers: │
│ X-MFA-Challenge-ID: uuid-xxxx │
│ X-MFA-Factor: totp │
│ body: │
│ mfa_code: "123456" │
│ │
│ ✅ MFA 验证成功 │
│ │
│ Client ← { │
│ "access_token": "xxx", │
│ "refresh_token": "yyy", │
│ "mfa_verified": true │
│ } │
└───────────────────────────────────────┘
```
#### Go 后端处理 MFA Challenge
```go
package auth
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
)
type AuthHandler struct {
rdb *redis.Client
tokenGenerator *TokenGenerator
ctx context.Context
}
type MFACreateChallengeRequest struct {
UserID string `json:"user_id"`
}
type MFAResponse struct {
ChallengeID string `json:"challenge_id"`
SupportedFactors []string `json:"supported_factors"`
ExpiresIn int `json:"expires_in"` // seconds
}
// MFAChallengeResponse MFA 验证通过后返回的 token 对
type MFAChallengeResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
MFAVerified bool `json:"mfa_verified"`
TokenType string `json:"token_type"` // "Bearer"
}
// CreateMFAChallenge 在密码验证通过后调用,生成一个限时 challenge
func (h *AuthHandler) CreateMFAChallenge(w http.ResponseWriter, r *http.Request) {
var req MFACreateChallengeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
challengeID := uuid.New().String()
expiry := time.Now().Add(5 * time.Minute)
// 将 challenge 存入 Redis,5 分钟过期
key := fmt.Sprintf("mfa:challenge:%s", challengeID)
h.rdb.Set(h.ctx, key, fmt.Sprintf(`{"user_id":"%s","exp":%d}`, req.UserID, expiry.Unix()), 5*time.Minute)
json.NewEncoder(w).Encode(MFAResponse{
ChallengeID: challengeID,
SupportedFactors: []string{"totp", "webauthn"},
ExpiresIn: 300,
})
}
// SubmitMFAVerify 提交 MFA 码进行验证
func (h *AuthHandler) SubmitMFAVerify(w http.ResponseWriter, r *http.Request) {
var req struct {
ChallengeID string `json:"challenge_id"`
Factor string `json:"factor"` // "totp" or "webauthn"
Code string `json:"code"`
}
json.NewDecoder(r.Body).Decode(&req)
// 1. 校验 challenge 存在且未过期
userID, err := h.validateChallenge(req.ChallengeID)
if err != nil {
http.Error(w, "invalid or expired challenge", http.StatusUnauthorized)
return
}
// 2. 根据 factor 执行验证逻辑
switch req.Factor {
case "totp":
if !totp.Verify(userID, req.Code, 6, 1, time.Now()) {
http.Error(w, "invalid totp code", http.StatusUnauthorized)
return
}
case "webauthn":
// WebAuthn assertion verification...
}
// 3. 验证成功,发放 token
tokenPair, err := h.tokenGenerator.Generate(userID, map[string]interface{}{
"mfa_verified": true,
"session_id": req.ChallengeID,
})
if err != nil {
http.Error(w, "token generation failed", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(MFAChallengeResponse{
AccessToken: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken,
MFAVerified: true,
TokenType: "Bearer",
})
}
```
### 3.2 MFA-Verified Scope(精细粒度控制)
不同 API 对安全级别的请求不同。高敏感操作需要 `mfa_verified=true` 的 token。
```typescript
// 前端:根据操作敏感度判断是否需要触发 MFA
// decodeBase64URL JWT payload 解码 helper
function decodeBase64URL(base64url: string): string {
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
return atob(base64);
}
async function sensitiveAction(apiPath: string) {
const needsMFA = apiPath.match(/\/transfer|\/withdraw|\/settings-change/);
if (needsMFA) {
// 检查当前 token 是否已有 MFA 标记
const token = getToken();
const decoded = JSON.parse(decodeBase64URL(token.split('.')[1]));
if (!decoded.mfa_verified) {
// 先走 MFA flow,获取新 token
const freshToken = await requestMFAFlow();
setToken(freshToken);
}
}
return fetch(apiPath, {
headers: { Authorization: `Bearer ${getToken()}` },
});
}
```
```go
// 后端中间件:校验 Token 中的 MFA 级别
func MFAMiddleware(requiredLevel string) gin.HandlerFunc {
return func(c *gin.Context) {
tokenStr, _ := extractToken(c)
claims, err := validateJWT(tokenStr)
if err != nil {
c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"})
return
}
if requiredLevel == "high" && claims.MFAVerified != true {
c.AbortWithStatusJSON(403, gin.H{
"error": "mfa_verification_required",
"required_level": "high",
})
return
}
c.Set("claims", claims)
c.Next()
}
}
// 使用示例
router.POST("/api/transfer", middleware.MFAMiddleware("high"), transferHandler)
router.GET("/api/profile", middleware.MFAMiddleware("low"), profileHandler)
```
### 3.3 Backup Codes(备用码)机制
当用户丢失手机或 TOTP app 无法使用时,Backup Codes 是最后的保底手段。
**设计要点:**
1. **一次性使用** — 每个码只能用一次,验证后立即标记已用
2. **预生成 + 安全存储** — 启用 TOTP 时同时生成 10 个码
3. **SHA-256 哈希存储** — 数据库中只存 hash,不存明文
4. **唯一显示给用户** — 用户需截图或抄写保存(离线场景)
```go
func GenerateBackupCodes(userID string) ([]string, []string) {
codes := make([]string, 10)
hashes := make([]string, 10)
for i := 0; i < 10; i++ {
b := make([]byte, 8)
rand.Read(b)
code := fmt.Sprintf("%08d", binary.BigEndian.Uint64(b) % 100000000)
codes[i] = code
hashes[i] = sha256.Sum256([]byte(code))
}
// hashes 存入数据库(一次性展示给用户)
return codes, hashes
}
func VerifyBackupCode(userID, code string) bool {
hash := sha256.Sum256([]byte(code))
ok := db.QueryRow("SELECT id FROM backup_codes WHERE user_id=? AND hash=?", userID, hash).Scan(...)
if ok {
// 立即删除,确保一次性
db.Exec("DELETE FROM backup_codes WHERE user_id=? AND hash=?", userID, hash)
}
return ok
}
```
> [!warning] 安全注意
> - Backup Codes 的强度等同于 8 位数字(1 亿种组合),**必须一次性使用**后销毁
> - 剩余可用码数量应反馈给前端("还有 3 个备用码可用"),让用户心中有数
> - 使用 Backup Code 登录后,建议强制重新绑定 TOTP(视为设备变更)
> - 审计日志中单独标注 "auth_method: backup_code"
---
### 3.4 Session 管理与 Remember-Me
```mermaid
stateDiagram-v2
[*] --> Unauthenticated
Unauthenticated --> PasswordValidated: 输入用户名 + 密码
PasswordValidated --> MFAChallenged: 需要 MFA
PasswordValidated --> Authorized: MFA skip (trusted device)
MFAChallenged --> Authorized: MFA verified
MFAChallenged --> PasswordValidated: MFA timeout
MFAChallenged --> Unauthenticated: cancel
Authorized --> SessionActive: 发放 session
SessionActive --> Authorized: token refresh
SessionActive --> Remembered: 勾选"记住我"
Remembered --> Authorized: 自动续期 (extended)
Remembered --> Unauthenticated: remember token expired
Authorized --> Unauthenticated: logout / revoke
```
| 策略 | 典型 TTL | 适用场景 |
|------|---------|---------|
| 标准 Session | 24h(配合 refresh token 滚动) | 日常办公 |
| Remember-Me | 7~30 天 | 个人账号,低敏感度 |
| No-Retain | 关闭 remember-me | 金融/医疗等高敏场景 |
> [!warning] Refresh Token 轮换(Rotation)
> 每次使用 refresh token 换 access token 时,都应同步颁发新的 refresh token,使旧的那个失效。这样可以:
> - 检测 replay 攻击(旧 refresh token 再次出现即意味着泄露)
> - 限制单个刷新令牌的总生命周期
> - Go 实现中使用 Redis Set NX 保证原子性
---
## 四、端到端实战流程
### 4.1 完整登录链路(含 MFA)
```mermaid
sequenceDiagram
actor U as User
participant FE as Frontend (React)
participant BE as Backend (Go)
participant AS as Auth Server
participant Redis as Redis Store
U->>FE: 点击"登录"
FE->>FE: generate code_verifier + code_challenge
FE->>AS: 重定向到 auth server
AS->>U: 展示登录表单
U->>AS: 输入 username + password
AS->>AS: 验证密码 ✓
AS->>AS: 查询 MFA 状态
Note over AS: 如果用户有 TOTP → 需要 MFA<br/>如果没有 → 直接发 token
alt MFA Required
AS->>FE: redirect back with error=mfa_required
FE->>BE: POST /api/auth/challenge (with user info)
BE->>Redis: store challenge record
BE->>FE: return challenge_id
FE->>U: 显示 TOTP 输入框
U->>FE: 输入 6 位验证码
FE->>BE: POST /api/auth/mfa-verify
BE->>AS: 带 X-MFA-Challenge-ID + code 调 /token
AS->>AS: 验证 TOTP + challenge
AS->>BE: {access_token, refresh_token, mfa_verified:true}
BE->>FE: token pair
FE->>FE: save token in memory state
else MFA Not Required
AS->>FE: redirect back with authorization code
FE->>AS: exchange code + verifier for tokens
AS->>FE: {access_token, refresh_token}
FE->>FE: save token in memory state
end
U->>FE: 进入应用主页
```
### 4.2 设备信任机制
对于可信设备,用户可以跳过 MFA。这是用户体验和安全的关键平衡点。
```go
// 设备指纹生成与验证
func DeviceFingerprint(userAgent string, screenRes string, timezone string) string {
seed := userAgent + screenRes + timezone
h := sha256.Sum256([]byte(seed))
return hex.EncodeToString(h[:16]) // 128-bit fingerprint
}
// 保存信任记录
func TrustDevice(userID, deviceFP string) error {
key := fmt.Sprintf("device:trust:%s:%s", userID, deviceFP)
return rdb.Set(ctx, key, "trusted", 30*24*time.Hour).Err() // 30 天
}
// 验证时跳过 MFA
func SkipMFAIfTrusted(userID, deviceFP string) bool {
key := fmt.Sprintf("device:trust:%s:%s", userID, deviceFP)
val, _ := rdb.Get(ctx, key).Result()
return val == "trusted"
}
```
```typescript
// 前端:生成设备指纹
async function getDeviceFingerprint(): Promise<string> {
const data = [
navigator.userAgent,
`${screen.width}x${screen.height}`,
Intl.DateTimeFormat().resolvedOptions().timeZone,
new Date().getTimezoneOffset(),
].join('|');
const encoder = new TextEncoder();
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(data));
const hashArray = Array.from(new Uint8Array(hashBuffer)).slice(0, 16);
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
```
> [!tip] 设备信任的安全性边界
> - 设备指纹**不是**加密证明,仅作为用户体验优化
> - 核心保障依然在于 access token 的短期生命周期
> - 敏感操作(转账、修改密码)**不应**因设备信任而跳过 MFA
> - 建议定期重新验证(如每隔 7 天清除 trust 记录)
---
## 五、OpenID Connect(OIDC)扩展
OIDC 是在 OAuth2 之上的认证层,提供了标准化的身份令牌(Identity Token)。
```
OAuth2: Client → AS → "Here's an access token for reading resources"
OIDC: Client → AS → "Here's an access token AND here's who you are"
```
### 5.1 ID Token(JWT 格式)
```json
{
"iss": "https://auth.example.com",
"sub": "user_abc123",
"aud": ["client_id_xyz"],
"exp": 1715970000,
"iat": 1715966400,
"auth_time": 1715966350,
"amr": ["pwd", "otp"], // Authentication Methods References
"azp": "client_id_xyz",
"nonce": "random-nonce-value"
}
```
> [!note] amr (Authentication Methods References)
> `amr` 字段记录了认证方法。**这就是 MFA 信息在 OIDC 层面的体现**:
> - `["pwd"]` → 仅有密码认证
> - `["pwd", "otp"]` → 密码 + OTP(双因素完成)
> - `["pwd", "puk"]` → 密码 + 硬件密钥
>
> 前端解析 `id_token.amr` 即可知道用户当前的认证强度。
### 5.2 Auth Server 选型推荐
自建还是用托管方案?以下是主流选型对比:
| 方案 | 适合场景 | MFA 支持 | 开发成本 | 维护成本 |
|------|---------|---------|---------|---------|
| **Keycloak (开源)** | 企业内部、数据不出域 | TOTP / WebAuthn / Email OTP | 中 | 中(需运维) |
| **Auth0 (SaaS)** | 快速上线、预算充足 | 全量支持 + adaptive MFA | 低 | 极低 |
| **Cloudflare Access** | 边缘侧认证、API 网关 | 邮件 OTP / WebAuthn | 低 | 极低 |
| **自研 (Go + go-oauth2/oauth2)** | 高度定制化需求 | 完全自控 | 高 | 高 |
| **Supabase Auth** | 初创产品、BFF 架构 | TOTP (v2) | 低 | 极低 |
```mermaid
graph TD
A{"需要快速上线?"} -->|是| B["Auth0 / Supabase"]
A -->|否| C{"数据合规要求?"}
C -->|必须私有部署| D["Keycloak"]
C -->|无限制| E["自建方案"]
style B fill:#d4edda
style D fill:#fff3cd
style E fill:#f8d7da
```
> [!tip] 决策建议
> - **MVP / 创业公司**:直接用 Auth0,节省数周开发时间
> - **企业内网工具**:Keycloak + LDAP/AD 集成,一次投入长期受益
> - **高度敏感行业**:考虑自建(可控审计流程),但务必复用成熟开源库而非从零造轮子
---
## 七、安全最佳实践清单
> [!checklist] 生产环境 Checklist
### OAuth2 侧
- [ ] **必须使用 HTTPS**(TLS 1.2+),否则 token 在网络中转易被截取
- [ ] Authorization Code + **PKCE** 是所有公开客户端的标配
- [ ] Access Token 放在**内存**(不存 localStorage/sessionStorage)
- [ ] Refresh Token 使用 **HTTP-only, Secure, SameSite=Strict** Cookie 仅在后端
- [ ] 实现 **Refresh Token Rotation**(每次旋转,旧 token 立即作废)
- [ ] Token 设置合理的 TTL(Access: 15min~1hr,Refresh: 7~30 days)
- [ ] JWT 使用 **RS256/ES256**(非对称签名),避免 HS256 密钥泄漏风险
- [ ] 严格校验 `issuer`、`audience`、`expiry`、`nonce`
### MFA 侧
- [ ] TOTP 使用 **RFC 6238** 兼容库,不要自己实现底层密码学
- [ ] rate limiting:每个 IP 每 10 次失败锁定 5 分钟
- [ ] 考虑支持 **Backup Codes**(一次性备用码,最多 10 个)
- [ ] 优先推荐 **WebAuthn/FIDO2** 替代 SMS/TOTP(更抗钓鱼)
- [ ] SMS 验证码有效期不超过 **10 分钟**
- [ ] MFA 相关接口必须限制频率(不要让用户能爆破 6 位数字)
- [ ] 支持 **MFA enrollment revocation**(用户可随时禁用并重新配置)
### 架构侧
- [ ] Token 中**不存储敏感数据**(密码、MFA secret),只存元数据
- [ ] 审计日志:记录每次 MFA 验证尝试(成功/失败/IP/device)
- [ ] 敏感操作强制二次 MFA(即使 token 带有 `mfa_verified: true`)
- [ ] 实现 Token Revocation List(黑屏/注销时立即失效)
---
## 八、常见威胁模型分析
| 攻击类型 | 防御手段 |
|---------|---------|
| **Phishing(钓鱼)** | WebAuthn(origin-binding 天然免疫)、DNS-over-HTTPS |
| **Replay Attack(重放)** | PKCE、Nonce 校验、Refresh Token Rotation |
| **Token Theft(窃听/存储)** | Short-lived Access Token、HTTPS-only、SameSite Cookie |
| **Brute Force(爆破 MFA)** | Rate Limiting、Account Lockout |
| **Session Hijacking** | HttpOnly Cookie、CSRF Token、Device Binding |
| **SIM Swap(短信劫持)** | 优先 TOTP/WebAuthn、SMS 仅作为 fallback |
---
## 关联笔记
- [[hhs/DEV/Security/Basics/RSA-and-ECC]]
- [[hhs/DEV/Security/JWT-Deep-Dive]]
- [[hhs/DEV/Go/Backend/Redis-Patterns]]
- [[hhs/DEV/React/frontend-state-management]]