--- 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
如果没有 → 直接发 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 { 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]]