diff --git a/hhs/DEV/OAuth2与MFA/OAuth2-and-MFA.md b/hhs/DEV/OAuth2与MFA/OAuth2-and-MFA.md new file mode 100644 index 0000000..e0f97f0 --- /dev/null +++ b/hhs/DEV/OAuth2与MFA/OAuth2-and-MFA.md @@ -0,0 +1,728 @@ +--- +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]] diff --git a/hzh/MS/02-服务治理/01-API网关.md b/hzh/MS/02-服务治理/01-API网关.md index 1c88bcd..d914bc8 100644 --- a/hzh/MS/02-服务治理/01-API网关.md +++ b/hzh/MS/02-服务治理/01-API网关.md @@ -1,80 +1,115 @@ --- -tags: [microservice, api-gateway, kong, apisix, spring-cloud-gateway] -create time: 2026-05-05 +tags: [microservice, api-gateway, kong, apisix, spring-cloud-gateway, nginx] +create time: 2026-05-05 12:00 --- # API 网关 ## 概述 -API Gateway 是所有外部请求的统一入口,承担以下职责: +API Gateway 是所有外部请求的统一入口,相当于微服务架构的 **"大门"**——所有来自客户端的请求必须先经过它,由它完成鉴权、限流、路由、转换等公共职责后,再转发给后端具体业务服务。 -```mermaid -graph LR - Client["客户端 App / Web"] --> GW["API Gateway"] - GW --> Auth["鉴权 & 限流"] - GW --> Route["路由转发"] - GW --> Transform["协议转换"] - GW --> Log["日志 & 监控"] - GW -.-> CB[(配置中心)] -``` +> [!question] 为什么要网关? +> 假设你手上有 20 个微服务,每个都有独立的 URL。如果让客户端直接调用这些服务: +> - 每次调用都要处理鉴权和签名验证? +> - 前端要维护 20 个 CORS 配置? +> - HTTPS 证书要在每台服务器上部署? +> +> 有没有一种方式让这些重复工作只处理一次? -常见实现:**Kong、APISIX、Spring Cloud Gateway、Nginx + Lua**。 - -## 网关应该做什么? - -> [!summary] 网关职责清单 - -| ✅ 适合放 | ❌ 不应该放 | -|---------|-----------| -| 鉴权与认证 | 业务逻辑(如订单创建) | -| 限流熔断 | 复杂的数据聚合查询 | -| HTTPS 终结 | 大量 CPU 密集型计算 | -| 请求/响应转换 | 涉及数据库写操作 | -| 路由分发 | 跨服务事务管理 | -| 日志 & 监控 | 邮件/短信发送等异步任务 | - -> [!tip] 核心原则 -> **网关保持瘦**——它是 traffic cop(交通警察),不是 warehouse manager(仓库管理员)。重逻辑应下沉到业务服务。 - -### 分层鉴权模型 - -> [!question] 权衡题 -> 有人说:"鉴权放到网关统一做,避免每个服务重复写。"但如果某个内部服务不需要鉴权呢?或者不同团队要求不同的鉴权方式呢? - -**分层鉴权架构**: +答案就是 **网关做共性、服务做个性**。 ```mermaid graph TB - External["外部用户"] --> GW["API Gateway
JWT 校验 + OAuth2"] - GW --> S1[公开接口
无需额外鉴权] - GW --> S2[内部接口
Service Token] - - S1 --> UserSvc[[User Service]] - S2 --> OrderSvc[[Order Service]] - S2 --> PaySvc[[Payment Service]] - - OrderSvc -->|"mTLS + JWT"| DB[(DB)] - PaySvc -->|"mTLS + JWT"| DB + subgraph External["外部世界"] + App["移动端 App"] + Web["Web 浏览器"] + ThirdParty["第三方 Partner"] + end + + subgraph Gateway["API Gateway 集群"] + direction TB + LB["负载均衡器
Nginx / CLB"] + GW1["Gateway Node A"] + GW2["Gateway Node B"] + end + + subgraph Backend["微服务层"] + USvc[[User Service]] + OSvc[[Order Service]] + PSvc[[Payment Service]] + ISvc[[Inventory Service]] + end + + subgraph Infra["基础设施"] + Config[(配置中心)] + Monitor[(Prometheus/Grafana)] + Log[(ELK / Loki)] + end + + App --> LB + Web --> LB + ThirdParty --> LB + LB --> GW1 + LB --> GW2 + GW1 --> USvc + GW1 --> OSvc + GW2 --> OSvc + GW2 --> PSvc + GW1 -.-> Config + GW2 -.-> Config + GW1 -.-> Monitor + GW2 -.-> Monitor ``` -- **第一层(网关)**:校验外部用户的 JWT/OAuth Token,拦截非法请求 -- **第二层(服务间)**:通过 Service Token 或 mTLS 保证调用方身份可信 -- **第三层(数据层)**:RBAC / ABAC 细粒度权限控制 +常见实现方案: -## 路由策略 +| 方案 | 语言 | 类型 | 适用场景 | +|------|------|------|---------| +| **Kong** | C (OpenResty/Lua) | 独立二进制 | 高性能、插件生态丰富 | +| **APISIX** | Lua (OpenResty) | 独立二进制 | 国内广泛使用、动态路由 | +| **Spring Cloud Gateway** | Java (Reactor) | 嵌入式 SDK | Spring 技术栈项目 | +| **Envoy** | C++ | 代理 + SDK | Service Mesh 底层代理 | +| **Nginx + Lua** | C/Lua | 手动组合 | 极致可控、团队有运维能力 | -### 基础路由规则 +## 网关应该做什么? + +> [!summary] 职责边界 +> +> | ✅ 适合放网关 | ❌ 不应该放网关 | +> |---------|-----------| +> | 鉴权与认证 | 业务逻辑(如订单创建) | +> | 限流熔断 | 复杂的数据聚合查询 | +> | HTTPS 终结 | 大量 CPU 密集型计算 | +> | 请求/响应格式转换 | 涉及数据库写操作 | +> | 路由分发 | 跨服务事务管理 | +> | 日志 & 指标采集 | 邮件/短信发送等异步任务 | +> | 协议转换(HTTP→gRPC) | 数据加工和报表生成 | + +> [!tip] 核心原则 +> **网关保持瘦**——它是 traffic cop(交通警察),不是 warehouse manager(仓库管理员)。重逻辑应下沉到业务服务。 +> +> > [!note] 思考 +> > 如果你在网关里发现需要写一个超过 30 行的 if-else 分支来处理"特殊业务规则",停下来问自己:这真的是公共关注点,还是某个服务的私有需求被错误地推到了上层? + +### 鉴权职责的分界 + +网关层的鉴权只负责 **"你是谁"**——校验 JWT / Token 合法性、拦截非法请求。 +更细致的权限判断留给下游服务自行处理。详细方案见 → [[02-服务治理/09-网关鉴权策略]] + +## 路由配置 + +### 基础路由示例 ```yaml -# APISIX 路由示例 +# APISIX 路由配置 routes: - uri: /api/orders/* upstream: nodes: "order-service:8080": 1 type: roundrobin - + - uri: /api/payments/* upstream: nodes: @@ -82,16 +117,31 @@ routes: type: roundrobin ``` -### 高级路由策略 +```go +// Go 中的路由注册(以 Gin + Gateway 为例) +r := gin.Default() -| 策略 | 场景 | 示例 | -|------|------|------| -| **路径匹配** | 按 URL 前缀路由 | `/api/v1/*` → v1 版本服务 | -| **Header 匹配** | A/B 测试、灰度发布 | `x-canary: true` → 新版本 | -| **权重路由** | 金丝雀发布 | 90% 流量 → v1, 10% → v2 | -| **正则匹配** | 复杂 URL 模式 | `/api/user/(?P\d+)/*` | +r.Use(gateway.AuthMiddleware()) // 全局鉴权 +r.Use(gateway.RateLimit(100)) // 全局限流 -## 网关插件体系 +api := r.Group("/api") +{ + api.POST("/orders", order.CreateHandler) + api.GET("/orders/:id", order.GetHandler) + api.POST("/payments", payment.ChargeHandler) +} + +_ = r.ListenAndServe() +``` + +> [!note] 解释 +> `AuthMiddleware` 和 `RateLimit` 是挂载在路由树最上层的中间件,会拦截所有通过 `/api` 前缀的请求。这意味着无需在每个 handler 里重复写鉴权逻辑——这正是网关集中式处理的优势。 + +### 灰度发布与高级路由 + +流量权重分配、灰度策略、A/B 测试等内容已移至 → [[02-服务治理/08-流量治理]] + +## 插件体系 网关的核心价值在于 **可插拔的中间件链**,类似 Express/Koa 的 middleware 概念。 @@ -99,55 +149,301 @@ routes: graph LR Req["Request"] -->|Plugin 1| RateLimiter["限流"] RateLimiter -->|Plugin 2| Auth["鉴权"] - Auth -->|Plugin 3| CBR["熔断"] - CBR -->|Plugin 4| Transform["协议转换"] - Transform -->|Plugin 5| Logger["日志"] - Logger --> Backend["Backend Service"] + Auth -->|Plugin 3| Router["路由匹配"] + Router -->|Plugin 4| Transform["协议转换"] + Transform -->|Plugin 5| Logger["日志记录"] + Logger --> Resp["Response"] ``` -### 常用插件列表 +### Go 实现的插件框架 + +```go +// Plugin 接口定义 — 每个插件实现此接口即可接入网关流水线 +type Plugin interface { + Name() string + Priority() int // 数字越小越先执行 + OnRequest(ctx *Context) bool // 返回 false 则中断流水线 + OnResponse(ctx *Context) // 响应阶段钩子 +} + +// 网关执行管线 +func (gw *Gateway) ExecutePlugins(ctx *Context, plugins []Plugin) { + sort.Slice(plugins, func(i, j int) bool { + return plugins[i].Priority() < plugins[j].Priority() + }) + + for _, p := range plugins { + ctx.Set("plugin", p.Name()) + if !p.OnRequest(ctx) { + ctx.AbortWithJSON(ctx.StatusCode(), gateway.ErrResp(ctx.ErrorCode())) + return + } + } + + ctx.Next() // 转发到后端服务 + + for _, p := range plugins { + p.OnResponse(ctx) + } +} +``` + +> [!note] 解释 +> 这段代码展示了网关插件系统的核心设计: +> - 每个插件通过 `Priority()` 控制执行顺序(例如限流必须在鉴权之前) +> - `OnRequest` 返回 `false` 时立即截断请求,不会到达后端 +> - `OnResponse` 在所有请求结束后执行,常用于埋点和日志记录 + +### 常用插件速查 | 插件 | 作用 | 推荐算法 | |------|------|---------| | **Rate Limiting** | 防刷限流 | 令牌桶 / 漏桶 | | **CORS** | 跨域处理 | 预检缓存 | | **IP 黑白名单** | 访问控制 | Redis Bloom Filter | -| **Response Rewrite** | 修改响应体 | JSON Patch | | **Request Transformation** | Header/Body 改写 | Map-based | +| **Protocol Conversion** | HTTP↔gRPC 互转 | protobuf mapping | | **Prometheus Exporter** | 指标采集 | 自动埋点 | -| **Fault Injection** | 混沌测试注入延迟/错误 | 按比例注入 | +| **Fault Injection** | 混沌测试 | 按比例注入 | -## 网关的高可用设计 +## 错误处理 -```mermaid -graph TB - DNS["DNS / CLB"] --> GW1["Gateway Node 1"] - DNS --> GW2["Gateway Node 2"] - - subgraph GW_Cluster["网关集群"] - GW1 - GW2 - end - - GW1 --> B1["backend-pool-v1"] - GW2 --> B2["backend-pool-v1"] - - B1 --> Svc1[[order-service]] - B1 --> Svc2[[user-service]] - B2 --> Svc1 - B2 --> Svc2 - - GW1 -.-> Config["配置中心 (同步)"] - GW2 -.-> Config +网关处于请求链路的最外层,它的错误处理质量直接影响用户体验。 + +### 统一错误码体系 + +```go +// 自定义网关级错误码 +const ( + ErrUnauthorized = 1001 // 未认证或 Token 过期 + ErrForbidden = 1002 // 认证通过但无权访问 + ErrRateLimit = 1003 // 触发限流 + ErrBackendUnavail = 1004 // 后端服务不可用(熔断中) + ErrGatewayTimeout = 1005 // 后端超时 + ErrMalformedReq = 1006 // 请求格式错误 + ErrServiceOverload = 1007 // 服务过载(上游返回 503) +) + +func ErrResp(code int) map[string]interface{} { + return map[string]interface{}{ + "code": code, + "message": errorMessages[code], + "request": currentRequestID, + } +} ``` -**关键点**: -- 网关无状态设计,可横向扩展 -- 配置通过注册中心实时同步,无需重启 -- 多节点前接负载均衡器(CLB/Nginx) +> [!note] 关键决策:网关 4xx vs 5xx +> +> | 场景 | 网关应返回 | 原因 | +> |------|-----------|------| +> | Token 无效 | **401** | 问题出在客户端凭证 | +> | 后端服务宕机 | **502** | 网关正确转发了但收到坏响应 | +> | 后端超时 | **504** | 网关等待超时而非业务错误 | +> | 触发限流 | **429** | 语义明确,客户端可据此退避 | +> | 参数错误 | **400** | 无论前后端,都是客户端输入有误 | +> +> > [!warning] 反模式 +> > 不要把后端 500 原封不动返给客户端。网关应该将其转换为统一的 "内部错误" 提示,并附带唯一的 request ID 用于排查。避免在公网暴露堆栈信息。 + +### 短路保护 —— 熔断机制 + +```go +func (gw *Gateway) forward(ctx *Context) error { + svc := gw.routeTo(ctx.Path) + cb := gw.CircuitBreaker(svc) + + // 熔断开启时直接短路,不再发请求 + if cb.State() == CircuitOpen { + return ctx.Error(ErrBackendUnavail, "%s is circuit-breaker open", svc) + } + + resp, err := cb.Do(func() (*http.Response, error) { + return http.DefaultClient.Do(ctx.Request) + }) + // 成功率低于阈值 → 打开熔断 + return nil +} +``` + +熔断状态机:**Closed**(正常转发)→ **Open**(全部短路)→ **Half-Open**(放行少量探测请求)。详见 → [[02-服务治理/06-容错模式]] + +## 可观测性 + +在生产环境中,网关是一站式的观测入口——所有流量的元数据都在这一层汇聚。 + +```mermaid +flowchart LR + Client --> GW["API Gateway"] + GW -- "access.log + trace_id" --> ELK["ELK / Loki"] + GW -- "QPS / latency / errors" --> PM["Prometheus"] + PM --> Grafana["Grafana Dashboard"] + GW -- "trace span" --> Jager["Jaeger / Zipkin"] + Jager --> Grafana +``` + +### 三 pillars 实践 + +| Pillar | 做什么 | 关键指标 | +|--------|--------|---------| +| **结构化日志** | 每条请求写入 trace_id、source IP、耗时、上游地址 | `method path status latency upstream` | +| **指标采集** | 按路由 / 状态码分桶暴露 Prometheus Metrics | `http_requests_total`, `http_request_duration_seconds` | +| **分布式追踪** | 传递 `X-Trace-ID` 给下游,构建完整调用链 | Trace ID 透传率 ≥ 99% | + +### 告警基线参考 + +| 指标 | 阈值 | 动作 | +|------|------|------| +| P99 延迟 | > 500ms 持续 5min | PagerDuty 告警 | +| 5xx 比例 | > 1% | 立即通知值班 | +| 熔断器打开数 | ≥ 3 个同时打开 | 检查下游健康 | + +## 性能优化 + +网关作为所有流量的必经之路,自身性能瓶颈会成为整个系统的天花板。 + +### 连接池复用 + +每个网关节点都会频繁向后端发起 HTTP 连接,为减少 TCP 握手开销: + +```go +// Go net/http 连接池配置 +transport := &http.Transport{ + MaxIdleConnsPerHost: 100, // 同一后端的空闲连接数 + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 5 * time.Second, + ResponseHeaderTimeout: 10 * time.Second, +} + +client := &http.Client{Transport: transport} +``` + +> [!note] 为什么重要 +> 假设 QPS = 5000,平均响应时间 = 50ms,每个请求新建 TCP 连接的 overhead 约 3ms(含 TLS handshake 可能达 20ms)。节省下的 15~20ms 可以直接转化为吞吐量提升。对于网关这种每毫秒都计较的场景,连接池复用是最简单的性能手段。 + +### DNS 预解析 + +避免每次请求都做 DNS lookup: + +```lua +-- OpenResty / Nginx 中的 dns_resolver 配置 +resolver 10.0.0.2 valid=30s; # 30s 缓存 DNS 结果 +resolver_timeout 2s; +``` + +### 其他优化技巧 + +| 技巧 | 收益 | 复杂度 | +|------|------|--------| +| 静态资源本地缓存 | 消除对上游的冗余请求 | 低 | +| gzip/brotli 压缩响应体 | 带宽降低 60~80% | 低 | +| Keep-Alive 复用连接 | 减少 TCP/TLS 握手次数 | 低 | +| 异步日志写入 | 不阻塞请求主线 | 中 | +| 多进程 Worker 模型 | 利用多核 CPU | 低 | + +## 实际案例 + +### 场景一:HTTP 转 gRPC + +后端团队用 gRPC 对外提供服务,但前端只能消费 HTTP/JSON。网关承担协议转换: + +```typescript +// 前端看到的仍然是 RESTful JSON +POST /api/v1/users +{ "name": "Alice", "email": "alice@example.com" } + +// 网关内部将 JSON body 转为 Protobuf 后发给 gRPC 后端 +// UserCreateRequest { name: "Alice", email: "alice@example.com" } +// → POST grpc:///user-service/UserService/Create +``` + +Go 中可使用 [`grpc-ecosystem/grpc-gateway`](https://github.com/grpc-ecosystem/grpc-gateway) 自动生成反向代理,或通过 [`go-grpc-middleware`](https://github.com/go-grpc-middleware) 编写自定义转换器。 + +### 场景二:静态页面兜底 + +当所有后端服务全部不可用时,提供友好的降级页面: + +```nginx +upstream backend { + server app-1:8080; + server app-2:8080; + server app-3:8080; + fail_timeout=30s max_fails=3; # 连续失败 3 次标记为 down +} + +server { + location @fallback { + root /usr/share/nginx/html; + try_files /maintain.html =503; + } + + location / { + proxy_pass http://backend; + proxy_next_upstream error timeout http_502 http_503; + error_page 503 @fallback; # 所有后端挂掉时展示维护页 + } +} +``` + +### 场景三:大文件上传加速 + +用户头像/文档上传场景,网关层直接限速容易被打满,可以绕过网关直连存储: + +```mermaid +sequenceDiagram + participant C as Client + participant G as Gateway + participant S3 as S3/OSS + participant App as App Service + + C->>G: GET /upload-token + G->>App: validate user + App-->>G: { token, upload_url } + G-->>C: presigned URL + C->>S3: PUT (direct, bypass gateway) + S3-->>C: 200 OK + C->>G: notify-complete { token } + G->>App: process uploaded file +``` + +> [!tip] 思路 +> 上传/下载大文件这类 I/O 密集型操作,消耗的是网关的连接数和带宽。可以考虑网关只负责签发临时凭证,数据传输直连对象存储,从而释放网关的并发容量。 + +## 反模式警示 + +> [!warning] 这些做法看起来合理,但实际上会带来问题 + +### 反模式 1:网关成为"万能胶水" + +某些团队把越来越多的业务逻辑往网关塞——拼接多个后端接口、汇总数据、做数据清洗……最终网关变成了一张巨型 spider web。 + +**后果**:网关变慢 → 全系统响应变慢 → 运维越来越痛苦。 + +**对策**:如果需要跨服务聚合数据,创建一个专门的 **BFF(Backend For Frontend)** 层,放在网关之后。 + +### 反模式 2:没有健康检查 + +网关不知道后端什么时候挂掉了,继续把流量打过去直到超时耗尽用户耐心。 + +**对策**:启用 upstream health check,配合熔断器(参见 → [[02-服务治理/06-容错模式]])。 + +### 反模式 3:日志没带 trace_id + +用户在后台看到报错,去翻日志却无从定位——成千上万条日志里没有唯一标识串联整条链路。 + +**对策**:在网关入口处生成 trace_id,通过 `X-Trace-ID` header 透传到所有下游,并在 access log 中固定包含该字段。 + +### 反模式 4:忽略请求大小限制 + +允许任意大小的请求体进入,攻击者可以轻松用超大 payload 打爆网关内存。 + +**对策**:设置 `client_max_body_size`(Nginx)或等效配置,对上传接口单独放宽,普通接口默认限制在几 MB 以内。 ## 关联笔记 -- [[02-服务治理/04-服务发现]] — 网关需要订阅服务实例列表 -- [[02-服务治理/08-流量治理]] — 高级路由和灰度发布的延伸 +- [[02-服务治理/09-网关鉴权策略]] — 网关鉴权的分层设计与差异化方案 +- [[02-服务治理/08-流量治理]] — 灰度发布、权重路由、蓝绿部署 +- [[02-服务治理/06-容错模式]] — 熔断、重试、降级 +- [[02-服务治理/04-服务发现]] — 网关如何获取后端实例列表 - [[hzh/MS/API 设计原则]] — API Gateway 的设计与 REST/gRPC 选型 +- [[04-可观测性]] — 日志、指标、链路追踪的完整体系 diff --git a/hzh/MS/02-服务治理/02-安全机制.md b/hzh/MS/02-服务治理/02-安全机制.md index 4bc12ea..675a84a 100644 --- a/hzh/MS/02-服务治理/02-安全机制.md +++ b/hzh/MS/02-服务治理/02-安全机制.md @@ -1,6 +1,6 @@ --- tags: [microservice, security, mTLS, JWT, OAuth2, zero-trust] -create time: 2026-05-05 +create time: 2026-05-05 10:00 --- # 安全机制 @@ -11,57 +11,118 @@ create time: 2026-05-05 ```mermaid graph TB - subgraph "防御层" - L1["L1: 网络隔离
VPC / 安全组"] - L2["L2: 传输加密
mTLS / TLS"] - L3["L3: 身份认证
JWT / Service Token"] - L4["L4: 授权控制
RBAC / ABAC"] - L5["L5: 审计追踪
操作日志"] + subgraph "纵深防御体系" + L1["网络隔离
VPC / 安全组"] + L2["传输加密
mTLS / TLS"] + L3["身份认证
JWT / SPIFFE"] + L4["授权控制
RBAC / ABAC"] + L5["审计追踪
操作日志"] end - + L1 -.-> L2 -.-> L3 -.-> L4 -.-> L5 ``` > [!tip] Zero Trust 原则 -> **永不信任,始终验证**。每个服务调用都要经过认证和授权,无论请求来自内网还是外网。 +> **永不信任,始终验证**。无论请求来自内网还是外网,每个服务调用都要经过认证和授权。传统"内网即安全"的假设是微服务安全的最大盲区——一旦某个服务被攻破,攻击者可以横向移动到其他服务。 ## 服务间认证 ### mTLS (双向 TLS) +mTLS 是零信任架构的基石——建立加密通道前,双方必须互相验证证书身份。 + ```mermaid sequenceDiagram - participant Client as 调用方 Service - participant CA as Certificate Authority - participant Svc as 被调方 Service - - Client->>CA: 申请证书 (SPIFFE ID) - Svc->>CA: 申请证书 (SPIFFE ID) - - Client->>Svc: TLS Handshake + ClientCert - Svc->>Svc: 验证书 + SPIFFE ID - Svc-->>Client: ✅ 认证成功 - - note over Client,Svc: 建立加密通道 + participant C as 调用方 Service + participant CA as CA / SPIFFE + participant S as 被调方 Service + + C->>CA: 申请证书 (SPIFFE ID) + S->>CA: 申请证书 (SPIFFE ID) + CA-->>C: 签发客户端证书 + CA-->>S: 签发服务端证书 + + C->>S: ClientHello + ClientCert + S->>S: 验证书链 + SPIFFE ID + alt 验证通过 + S-->>C: ServerHello + ServerCert + note over C,S: 建立加密通道 + else 验证失败 + S->>C: TLS Alert (handshake failure) + end ``` +> [!question] 思考 +> 为什么内网服务间通信也需要加密?如果攻击者已经进入了内网网络,明文 gRPC 调用会发生什么? + +> [!answer] 答案 +> 传统安全模型假设"内网即安全",但这个前提是整个内网都不可入侵——这在实际中几乎不成立。以下场景说明为什么内网也必须加密: +> +> **1. 攻击者进入内网的途径很多** +> - 某个前端服务存在远程代码执行漏洞 → 获得容器权限 → 嗅探同 VPC 其他 Pod 流量 +> - 开发者电脑中毒 / CI/CD 供应链被投毒 → 从合法节点发起内网横向访问 +> - 第三方插件或依赖库被植恶意代码 → 在构建产物中留下后门 +> +> **2. 明文 gRPC 被窃听后的具体后果** +> +> | 攻击方式 | 可获取的内容 | 影响 | +> |---------|------------|------| +> | **流量嗅探** | 全部请求/响应体、gRPC metadata | 用户隐私数据、业务逻辑泄露 | +> | **Metadata 劫持** | JWT Token、追踪 ID、认证头 | 直接伪造身份调用下游服务 | +> | **中间人篡改** | 修改请求参数、响应 payload | 注入脏数据、绕过业务校验 | +> +> ```mermaid +> graph LR +> A["A: order-service
(发送请求)"] -->|"明文 gRPC"| D["D: attacker
(嗅探 + 注入)"] +> A --> B["交换机 / router"] +> B --> C["C: payment-service
(接收请求)"] +> D -.->|"伪造成 order-service"| C +> +> style D fill:#f99,stroke:#c00 +> ``` +> +> **关键理解**:即使没有"主动攻击"能力,仅靠二层/三层抓包(ARP spoofing、镜像端口、VLAN hop),攻击者就能完整回放 orca 一个 gRPC stream——包括其中传递的 JWT token、数据库查询条件等敏感信息。 +> +> **结论**:mTLS 不是"防外部黑客"的,而是让内网中的**任何一个被攻破的点**都无法读取其他服务的流量。这就是 Zero Trust 的核心思想。 + **mTLS 的核心优势**: -- 双方都持有对方可验证的证书,防止中间人攻击 -- 证书自动轮换(配合 Istio/Linkerd 等服务网格) -- 零代码侵入——Sidecar 代理处理握手 +- 双向证书验证,防止中间人攻击和非法服务接入 +- 证书自动轮换(配合 Istio / Cert-Manager 等服务网格方案) +- 零代码侵入——Sidecar 代理处理握手,业务代码无需关心 + +**生产落地要点**: + +| 要素 | 推荐方案 | 说明 | +|------|---------|------| +| **CA 体系** | SPIFFE / Istio CA | 基于 SPIFFE ID 自动签发短生命周期证书 | +| **证书管理** | cert-manager + Vault | K8s 场景下自动续期,避免人工干预 | +| **降级策略** | Peer Authentication MESH_STRICT | Istio 中设置为 STRICT 可强制所有流量走 mTLS | + +```yaml +# Istio PeerAuthentication 示例 +apiVersion: security.istio.io/v1beta1 +kind: PeerAuthentication +metadata: + name: default + namespace: prod +spec: + mtls: + mode: STRICT # 拒绝明文连接 +``` ### JWT / Service Token -适用于不具备 mTLS 基础设施的场景: +适用于不具备 mTLS 基础设施的场景,或作为跨边界调用的身份传递载体: ```go -// 生成 Service Token +// 生成 Service Token(短生命周期,≤ 1h) token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "sub": "order-service", // 服务标识 - "iss": "auth-server", // 签发者 - "aud": "payment-service", // 目标服务 - "exp": time.Now().Add(1*time.Hour).Unix(), - "iat": time.Now().Unix(), + "sub": "order-service", // 调用方服务标识 + "iss": "auth-server", // 签发者 + "aud": "payment-service", // 目标服务 + "exp": time.Now().Add(1 * time.Hour).Unix(), + "iat": time.Now().Unix(), + "jti": uuid.New().String(), // 唯一 ID,防重放 }) tokenString, _ := token.SignedString(secretKey) ``` @@ -69,11 +130,11 @@ tokenString, _ := token.SignedString(secretKey) | 方案 | 安全性 | 复杂度 | 适用场景 | |------|--------|--------|---------| | **mTLS** | ⭐⭐⭐⭐⭐ | 中(需 PKI 基础设施) | 服务网格环境、K8s 内部通信 | -| **JWT + Shared Secret** | ⭐⭐⭐⭐ | 低 | 中小规模,快速上手 | +| **JWT + Shared Secret** | ⭐⭐⭐⭐ | 低 | 中小规模、快速上手 | | **mTLS + JWT** | ⭐⭐⭐⭐⭐ | 中高 | 金融级安全要求 | > [!tip] 最佳实践 -> 在生产环境中,推荐 **mTLS + JWT 双层防护**:mTLS 保证通道安全,JWT 传递调用方的身份信息(谁在调用)。 +> 在生产环境中,推荐 **mTLS + JWT 双层防护**:mTLS 保证通道安全,JWT 传递调用方的身份信息(谁在调用)。仅用 JWT 而不加密通道的做法,一旦 TLS 被绕开(如配置错误),所有凭据将暴露于明文。 ## API 鉴权模型 @@ -98,22 +159,32 @@ flowchart LR ### ABAC (基于属性的访问控制) -适合细粒度场景: +当权限决策需要依赖多维属性(部门、地域、时间、资源类型)时,ABAC 是更自然的选择: ```go +type User struct { + ID string + Role string + Department string + Level int // 职级 +} + +type Resource struct { + ID string + OwnerID string + Department string + Sensitivity int // 1-4, 敏感度等级 +} + +// 策略即代码:灵活定义访问规则 func canAccess(user User, resource Resource, action string) bool { - // 属性比较规则 rules := []Rule{ - { - Condition: user.Department == resource.Department, // 同部门 - Action: "read", - }, - { - Condition: user.Role == "admin" || user.ID == resource.OwnerID, - Action: "write", - }, + // 同部门员工可读本部门文档 + {Condition: user.Department == resource.Department && user.Level >= resource.Sensitivity, Action: "read"}, + // 管理员或资源所有者可写 + {Condition: user.Role == "admin" || user.ID == resource.OwnerID, Action: "write"}, } - + for _, r := range rules { if r.Condition && r.Action == action { return true @@ -123,8 +194,23 @@ func canAccess(user User, resource Resource, action string) bool { } ``` +> [!tip] RBAC vs ABAC 选型指南 +> | 维度 | RBAC | ABAC | +> |------|------|------| +> | **适用规模** | 中小团队,角色边界清晰 | 大型组织,权限维度复杂 | +> | **管理方式** | 预定义角色 + 分配 | 编写动态策略规则 | +> | **扩展性** | 新增场景 → 新增角色 → 组合爆炸 | 新增场景 → 新增规则,不影响现有 | +> | **落地成本** | 低(JWT claims 中直接携带 role) | 中(需策略引擎,如 OPA / Casbin) | +> +> 实际项目中常采用 **RBAC 为主,ABAC 补充关键资源**的混合模式。 + ## 输入校验与防攻击 +微服务架构中,输入校验是最后一道防线。无论上游做了什么,每个服务都应独立校验进入边界的输入数据。 + +> [!question] 如果网关已经做了鉴权和限流,下游服务还需要校验输入吗? +> **必须校验**。网关只负责基础设施级别的防护(认证、限流),业务层面的合法性(如字段格式、长度、枚举值)应由各服务自行把关。 + ### SQL 注入防护 ```go @@ -135,13 +221,16 @@ query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", userInput) rows, err := db.Query("SELECT * FROM users WHERE name = ?", userInput) ``` -### XSS 防护 +### XSS 与 CSP 头 ```go // Go HTML template 自动转义 tpl.ExecuteTemplate(w, "page.html", data) // {{.Name}} 自动 HTML 转义 // JSON API 天然免疫(Content-Type: application/json) + +// 强制浏览器遵守 CSP 策略 +w.Header().Set("Content-Security-Policy", "default-src 'self'") ``` ### 常见攻击防护清单 @@ -152,27 +241,87 @@ tpl.ExecuteTemplate(w, "page.html", data) // {{.Name}} 自动 HTML 转义 | **XSS** | 输出转义 / CSP Header | Web 框架 / Gateway | | **CSRF** | SameSite Cookie / CSRF Token | 网关 / Session | | **DDoS** | 限流 + WAF | API Gateway | -| **重放攻击** | Nonce + Timestamp | API 签名 | +| **重放攻击** | Nonce + Timestamp | API 签名中间件 | | **凭证泄露** | HTTPS + Secure Cookie | 传输层 | ## API 签名(防重放攻击) -对高安全要求的 API,客户端需要对请求签名: +对高安全要求的 API(如支付、转账),客户端需要对请求做 HMAC 签名,服务端验证签名的完整性: -``` -Signature = HMAC-SHA256(Secret, HTTP_Method + URL + Timestamp + Body) +```mermaid +sequenceDiagram + participant C as 客户端 + participant S as 服务端 + + C->>C: 构造请求体 + timestamp + nonce + C->>C: Signature = HMAC(Secret, Method+URL+TS+Body) + C->>S: GET /api/pay?ts=...&nonce=...&sig=... + + S->>S: 1. 检查 timestamp 是否过期 + alt 已过期 + S-->>C: 401 Request Expired + else 未过期 + S->>S: 2. 检查 nonce 是否已使用 + alt nonce 已存在 + S-->>C: 409 Replay Detected + else 新 nonce + S->>S: 3. 重新计算签名并比对 + alt 一致 + S-->>C: 200 OK + else 不一致 + S-->>C: 403 Invalid Signature + end + end + end ``` -``` -GET /api/orders?userId=123×tamp=1714900000&nonce=abc123 -↓ 用 Secret 做 HMAC-SHA256 签名 -a1b2c3d4e5f6... +### Go 实现示例 + +```go +// 客户端:生成签名 +func signRequest(secret string, method, url, body string, ts int64, nonce string) string { + payload := fmt.Sprintf("%s|%s|%d|%s|%s", method, url, ts, nonce, body) + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(payload)) + return base64.StdEncoding.EncodeToString(mac.Sum(nil)) +} + +// 服务端:签名验证中间件 +func verifySignature(secret string) middleware.Handler { + return func(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ts := getParam(r, "timestamp") + nonce := getParam(r, "nonce") + sig := getParam(r, "signature") + + // 1. 时间窗口校验 (±5 min) + if time.Since(time.Unix(ts, 0)).Abs() > 5*time.Minute { + http.Error(w, "request expired", http.StatusUnauthorized) + return + } + + // 2. Nonce 去重 (Redis SETNX, TTL=300s) + key := fmt.Sprintf("nonce:%s", nonce) + if !redis.SetNX(key, "1", 300*time.Second) { + http.Error(w, "replay detected", http.StatusConflict) + return + } + + // 3. 签名比对 + expected := signRequest(secret, r.Method, r.URL.String(), r.Body, ts, nonce) + if !hmac.Equal([]byte(sig), []byte(expected)) { + http.Error(w, "invalid signature", http.StatusForbidden) + } + next(w, r) + } + } +} ``` -服务端验证步骤: -1. 检查 `timestamp` 是否在允许窗口内(如 ±5 分钟) -2. 检查 `nonce` 是否已使用(Redis SetNX,TTL 5min) -3. 用同样的算法计算签名,比对是否一致 +> [!note] Nonce 设计要点 +> - 每次请求必须携带唯一的 `nonce`(随机字符串或递增序号) +> - 服务端用 Redis `SETNX` 记录已使用的 nonce,TTL 略大于时间窗口(如 300s) +> - 对于高频调用场景,可考虑基于布隆过滤器优化存储空间 ## 关联笔记 diff --git a/hzh/MS/02-服务治理/03-分布式追踪.md b/hzh/MS/02-服务治理/03-分布式追踪.md index 518ff49..914f6ec 100644 --- a/hzh/MS/02-服务治理/03-分布式追踪.md +++ b/hzh/MS/02-服务治理/03-分布式追踪.md @@ -1,6 +1,6 @@ --- tags: [microservice, distributed-tracing, opentelemetry, jaeger, skywalking] -create time: 2026-05-05 +create time: 2026-05-05 14:30 --- # 分布式链路追踪 @@ -16,11 +16,11 @@ create time: 2026-05-05 ```mermaid flowchart TB - Req["请求 trace_id=abc123"] --> Span1["Span #1
API Gateway
5ms"] - Span1 --> Span2["Span #2
Order Service
70ms"] - Span1 --> Span3["Span #3
User Service
15ms"] - Span2 --> Span4["Span #4
Inventory DB Query
60ms"] - + Req["请求 trace_id=abc123"] --> Span1["Span #1 API Gateway 5ms"] + Span1 --> Span2["Span #2 Order Service 70ms"] + Span1 --> Span3["Span #3 User Service 15ms"] + Span2 --> Span4["Span #4 Inventory DB Query 60ms"] + style Span2 fill:#ff9999 style Span4 fill:#ffcc99 ``` @@ -75,62 +75,315 @@ flowchart TB ## 上下文传播 (Context Propagation) -`trace_id` 如何从上游服务传递到下游? +`trace_id` 如何从上游服务传递到下游?核心原则:**调用者创建上下文 → 被调者提取上下文 → 在自身链路中继续使用并透传到更下游**。 -### HTTP 场景:W3C Trace Context 标准 +### W3C Trace Context 标准 + +这是业界事实标准([W3C Recommendation](https://www.w3.org/TR/trace-context/)),被 OpenTelemetry、Jaeger、SkyWalking 全部支持。 ```json // HTTP Header 中实际传递的字段 { "traceparent": "00-abc123def456...-789ghi012jkl-01", - "tracestate": "congo=t61rcWkgMzE" + "tracestate": "congo=t61rcWkgMzE,vendor=value" } ``` -格式:`version-trace_id-span_id-flags` +| 字段 | 格式 | 说明 | +|------|------|------| +| `version` | 2 字符十六进制 | 版本号,目前固定 `00` | +| `trace_id` | 32 字符十六进制 | 128-bit 全局唯一标识,建议用 ULID / Snowflake 生成 | +| `span_id` | 16 字符十六进制 | 64-bit 本段 span 的唯一标识 | +| `flags` | 2 字符十六进制 | 目前仅 `01` 表示已采样 | + +**`tracestate`**:供厂商扩展使用,例如用于在多家 tracing 系统间同步采样决策或路由信息。多个厂商以逗号分隔。 + +### HTTP 场景:中间件自动注入与提取 ```go // OpenTelemetry SDK 自动处理 context 注入和提取 func Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // 从入站请求提取 trace context + // ① 从入站请求提取 trace context(如果请求中没有 traceparent,会生成新的 trace) ctx := propagator.Extract(r.Context(), headerReader{r.Header}) - - // 创建新的 span + + // ② 在当前服务内创建新 span ctx, span := tracer.Start(ctx, "handleOrder") defer span.End() - - // 将 trace context 注入到出站请求 + + // ③ 将 trace context 注入到出站请求 —— 关键一步 r = r.WithContext(ctx) propagator.Inject(ctx, headerWriter{r.Header}) - + next.ServeHTTP(w, r) }) } ``` -### MQ 场景:Message Header 透传 +```mermaid +sequenceDiagram + participant C as Client + participant A as Service A + participant B as Service B + + C->>A: GET /api/order\ntraceparent=00-abc... + Note over A: Extract create Span A + A->>B: POST /inventory\nInject traceparent + Note over B: Extract create Span B + B-->>A: Response + A-->>C: Response +``` + +### gRPC 场景:Metadata 透传 + +gRPC 基于 HTTP/2,传播方式类似,但使用的是 **gRPC Metadata** 而非 HTTP Headers(gRPC 库内部会将 metadata 转为 HTTP Header)。 ```go -// RocketMQ 生产者 - 自动注入 trace context -msg := rocketmq.NewMessage("order-created", payload) -msg.WithProperty("trace_id", currentTraceID) // 手动注入 -msg.WithProperty("span_id", currentSpanID) +// === 调用方:自动从 context 提取 trace 信息写入 metadata === +ctx, span := tracer.Start(parentCtx, "callUserService") +defer span.End() -// RocketMQ 消费者 - 恢复 trace context -traceID := msg.GetProperty("trace_id") -span, _ := tracer.Start(ctx, "processOrderCreated", - trace.WithSpanKind(trace.SpanKindConsumer)) +// otelgrpc.Interceptor 会自动完成 Inject,无需手动处理 +conn, err := grpc.DialContext(ctx, "user-service:9090", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithStatsHandler(otelgrpc.NewClientHandler()), // 自动注入 +) +client := userpb.NewUserServiceClient(conn) +resp, err := client.GetUser(ctx, &userpb.GetRequest{Id: "123"}) + +// === 被调方:拦截器自动完成 Extract === +server := pb.NewUserServiceServerImpl() +grpc.NewServer( + grpc.StatsHandler(otelgrpc.NewServerHandler()), // 自动提取 +).Serve(listener) +``` + +> [!note] 为什么不直接用 HTTP Headers? +> gRPC 对 metadata key 有严格的大小写规范——全部小写且不支持连字符。因此 `traceparent` 这样的 Header 名无法直接映射到 gRPC Metadata,各 tracing SDK 内部做了适配层,开发者只需关注 `context`,不需要手动操作 metadata。 + +### MQ 场景:消息属性透传 + +消息队列没有标准化的上下文传播协议,需要**在消息属性中手动携带 trace 信息**。OpenTelemetry 提供了标准的 attribute 命名约定。 + +```go +// === RocketMQ 生产者:注入 trace context === +msg := rocketmq.NewMessage("order-created", payload) + +// 标准 OTel 属性名 +msg.WithProperty(semconv.RPCSystemKey, "rocketmq") +msg.WithProperty(semconv.MessagingSystemKey, "rocketmq") +msg.WithProperty(semconv.HTTPFlavorKey, "1.1") + +// 手动注入 trace_id 和 span_id +if sc, ok := trace.SpanFromContext(ctx).SpanContext(); ok && sc.IsValid() { + msg.WithProperty("traceparent", fmt.Sprintf( + "00-%s-%s-01", sc.TraceID().String(), sc.SpanID().String(), + )) +} + +producer.SendSync(msg) + +// === RocketMQ 消费者:恢复 trace context === +consumer.Receive(func(ctx context.Context, messages ...*rocketmq.Message) error { + for _, msg := range messages { + // 从消息属性中提取 traceparent + tp := msg.GetProperty("traceparent") + if tp != "" { + ctx = propagator.Extract(ctx, textmap.Reader(func(k string) string { + return tp // 模拟 header reader + })) + } else { + // 补偿策略:如果上游没带 trace 信息,作为新 trace 起点 + ctx = trace.ContextWithRemoteSpanContext(ctx, + trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: generateFallbackTraceID(), + Remote: true, + }), + ) + } + + _, span := tracer.Start(ctx, "processOrderCreated", + trace.WithSpanKind(trace.SpanKindConsumer)) + span.SetAttributes( + semconv.MessagingSystemKey, "rocketmq", + semconv.MessagingDestinationKey, msg.Topic, + semconv.MessagingMessageIDKey, msg.MsgID, + ) + + doProcess(ctx, msg.Body) + span.End() + } + return nil +}, rocketmq.ConsumeFunc()) +``` + +> [!tip] 补偿策略:异步解耦时可能出现"上游未传 trace"的情况 +> +> 当 MQ 消息来自非追踪系统或上游漏传了 trace_id 时,消费者应作为 **新 Trace 的根 Span** 启动,而不是丢弃消息。同时通过属性标记 `messaging.message.is_remote=true` 标明这是一个远程关联点。 + +### 同步并发:Goroutine 间的 Context 传播 + +Go 的 `context.Context` 是协程安全的,子 Goroutine 直接使用父级 context 即可自动继承 trace。关键在于**不能丢失 context 引用**。 + +```go +func handleOrder(ctx context.Context, w http.ResponseWriter, r *http.Request) { + _, span := tracer.Start(ctx, "handleOrder") + defer span.End() + + // ✅ 正确:直接传入同一 context,子 goroutine 自动继承 trace + var wg sync.WaitGroup + for _, item := range order.Items { + wg.Add(1) + go func(item OrderItem) { + defer wg.Done() + processItem(ctx, item) // ctx 携带 trace context + }(item) + } + wg.Wait() + + // ❌ 错误:创建了无父 context 的新 context,trace 断裂 + // go func() { + // _, childSpan := tracer.Start(context.Background(), "processItem") + // ... + // }() +} +``` + +```mermaid +flowchart TB + subgraph Main["主 Goroutine"] + S0["Span #1 handleOrder"] + S0 --> S1["Span #2 proc Item A"] + S0 --> S2["Span #3 proc Item B"] + S0 --> S3["Span #4 proc Item C"] + end + + style S0 fill:#bbddff + style S1 fill:#ccf2ff + style S2 fill:#ccf2ff + style S3 fill:#ccf2ff +``` + +> [!warning] 常见陷阱 +> +> 1. **忘记传入 context**:用 `context.Background()` 或 `context.TODO()` 启动了 Goroutine,导致 trace 断裂。 +> 2. **超时覆盖**:子 Goroutine 中设置了独立的 `context.WithTimeout` 但没有保留父级的 deadline,导致整体超时无效。 +> 3. **recover 后丢失 context**:`defer recover()` 中如果有错误上报逻辑,仍需持有原始 context。 + +### 额外负载:Baggage 机制 + +除了 trace/span 信息,有时需要在链路间传递**业务标签**(如用户 ID、租户 ID、A/B 测试分组),这通过 **Baggage** 实现。 + +```go +// === 入口服务:设置 baggage === +baggage, _ := baggage.New(baggage.Pair("user.id", "u-12345"), + baggage.Pair("tenant", "acme-corp")) +ctx = propagator.Inject(ctx, baggageInjector{baggage}) + +// === 中间任意服务:读取 baggage === +baggage = baggage.FromContext(ctx) +userID, _ := baggage.Value("user.id") +// userID == "u-12345" + +// === 下游服务:也能读到同样的 baggage === +``` + +| 特性 | Baggage | Span Attributes | +|------|---------|----------------| +| 传播范围 | 沿整条链路传递给所有下游 | 仅记录在本地 Span 中,不传播 | +| 大小限制 | 总计 512 字节(防止 Header 膨胀) | 无限制 | +| 典型用途 | 用户 ID、租户、环境标记 | 状态码、耗时、SQL 语句等遥测数据 | +| 是否计入 Span Duration | 否 | 否 | + +> [!caution] Baggage 安全须知 +> +> Baggage 会被编码为 HTTP Header 随每个请求传播,**绝不能包含敏感信息**(密码、Token、PII)。默认 512 字节上限也意味着不适合传递大量数据。如需传递大对象,请改为通过 `span.RecordError()` 或独立日志存储。 + +### 边界的判断:何时创建新 Trace + +并非所有场景都需要延续上游 trace。以下情况应考虑开启新 trace: + +```mermaid +quadrantChart + title 跨系统调用:是否延续 Trace? + x-axis Low Coupling --> High Coupling + y-axis New Trace --> Continue Trace + "健康检查 / Scrape": [0.2, 0.15] + "定时任务 / Cron": [0.15, 0.1] + "MQ 消息异步解耦": [0.4, 0.7] + "正常 HTTP/gRPC 请求": [0.85, 0.9] + "第三方回调": [0.5, 0.25] + "消息积压重新消费": [0.3, 0.3] +``` + +| 场景 | 行为 | 说明 | +|------|------|------| +| 正常 HTTP / gRPC / MQ 请求 | 延续上游 trace_id | 标准流程,Extract → Continue | +| 健康检查 / Prometheus scrape | 新建 trace | 可标记 `tracestate` 为 healthcheck | +| 定时任务 / Cron job | 新建 trace | 无上游 context,天然无父 span | +| 消息积压重新消费(跨越数天) | 新 trace + baggage 引用原 trace_id | 保留追溯关联 | +| 第三方回调(外部系统主动推送) | 新建 trace,通过 callback_id 间接关联 | 无法 Extract,只能新建 | +| 伪造 traceparent | 验证格式合法性,非法则忽略并新建 | 记录告警,防止注入攻击 | + +**决策流程:** + +```mermaid +flowchart LR + Incoming["Incoming Request"] --> HasTrace{"是否有有效 traceparent"} + HasTrace -->|"是"| Extract["Extract 并继续"] + HasTrace -->|"否"| NewTrace["创建新 trace"] + + Extract --> Valid{"格式合法"} + Valid -->|"是"| Continue["延续 trace"] + Valid -->|"否"| LogWarn["记录告警丢弃伪造 context"] + LogWarn --> NewTrace + + NewTrace --> MarkBaggage["可选 baggage 携带原 trace_id 作引用"] + MarkBaggage --> StartRoot["启动根 Span"] ``` ## 主流方案对比 +### 方案全景图 + +```mermaid +quadrantChart + title 三大追踪方案特性对比 + x-axis Low Invasiveness --> High Flexibility + y-axis All-in-one APM --> Modular Components + "SkyWalking": [0.25, 0.75] + "OpenTelemetry": [0.75, 0.85] + "Jaeger": [0.6, 0.35] +``` + | 方案 | 协议 | 存储后端 | 侵入程度 | 特色能力 | |------|------|---------|---------|---------| | **OpenTelemetry** | OTLP | Prometheus/Jaeger/Zipkin | SDK + Auto-Instrumentation | CNCF 标准,厂商中立,未来趋势 | | **Jaeger** | Jaeger native | Cassandra/Elasticsearch | Agent / SDK | Uber 开源,UI 友好,支持业务 Tags | | **SkyWalking** | SkyWalking | MySQL/Elasticsearch/ES | 零侵入 Java Agent | 国产,中文文档完善,APM 一体 | +### 演进路线:从单体到 OTel + +```mermaid +flowchart LR + subgraph Phase1["Phase 1 无 Tracing"] + P1["各服务独立日志\n排查靠翻日志和SSH"] + end + + subgraph Phase2["Phase 2 Jaeger/SkyWalking 自研"] + P2["特定语言适配\n链路覆盖不全"] + end + + subgraph Phase3["Phase 3 OpenTelemetry 统一"] + P3["SDK 统一\nAuto-Instrumentation\nCollector 收集"] + end + + P1 ==> P2 ==> P3 + + note["推荐目标 OTel Collector 灵活后端\n不锁死任何组件按需替换"] + note -.-> P3 +``` + > [!tip] 选型建议 > 新项目优先选 **OpenTelemetry**——它是行业标准,未来会被所有工具支持。如果团队需要开箱即用的 APM,**SkyWalking** 的 Java Agent 零侵入方案是快速上手的最佳选择。 @@ -170,16 +423,79 @@ func handleOrder(w http.ResponseWriter, r *http.Request) { ## 采样策略 +> [!question] 为什么需要采样? +> 假设日活 100 万用户,每个用户产生 10 次请求 = 每天 1 千万条 Trace。如果全部存储,按每条 Span 平均 500 bytes 算,单张表一天就是 **数 GB**。采样不是偷懒,是**用可控的成本换取可观测性**。 + +### 采样算法 + +OpenTelemetry 提供了两种内置采样器: + +```go +// 静态采样:简单粗暴,适合开发环境 +alwaysOn := sdktrace.AlwaysSample() // 100% 采集 +alwaysOff := sdktrace.NeverSample() // 0% 采集(但仍在 SDK 内创建 span) + +// 动态采样:根据链路状态智能决策 ⭐ 推荐生产使用 +dynamic := sampler.TraceIDRatioBased(0.1) // 10% 随机采样 +``` + +实际项目中通常组合使用 **Parent-Based 分层采样**,确保父子链路一致性: + +```go +provider := sdktrace.NewTracerProvider( + // Parent-based: 父级已采样则子级必采样,反之亦然 + sdktrace.WithSampler( + sdktrace.ParentBased( + sdktrace.TraceIDRatioBased(0.1), // 无父 trace 时默认 10% + ), + ), + sdktrace.WithBatcherExporter(exporter), +) +``` + +### 错误优先采样 + +```go +type errorPrioritySampler struct { + baseRate float64 // 基础采样率 +} + +func (s *errorPrioritySampler) ShouldSample(params sampling.Parameters) sampling.Result { + ctx := params.SpanContext.Context + // 检查当前 span 是否已有 error status + if sc, ok := trace.SpanFromContext(ctx).SpanContext(); ok { + attrs := sc.Attributes() + for _, attr := range attrs { + if attr.Key == "error" && attr.Value.AsBool() { + return sampling.Result{ + Decision: sampling.RecordAndSample, // 100% 采样错误请求 + Attributes: []attribute.KeyValue{}, + } + } + } + } + // 非错误请求走基础采样率 + rate := rand.Float64() + decision := sampling.Drop + if rate < s.baseRate { + decision = sampling.RecordAndSample + } + return sampling.Result{Decision: decision} +} +``` + ```mermaid flowchart LR AllReq["全部请求"] --> Sampler{"采样决策"} - Sampler -->|"100%"| Core["核心链路全量采集"] - Sampler -->|"5~10%"| Normal["普通路径随机采样"] - Sampler -->|"0%"| Health["健康检查/内部心跳"] - + Sampler -->|"有error"| CheckError{"错误标记"} + CheckError -->|"是"| Core["核心链路全量采集"] + CheckError -->|"否"| Prob["概率采样"] + Sampler -->|"无parent"| Prob + Prob -->|"命中"| Core + Prob -->|"未命中"| Drop["丢弃"] + Core --> Storage["Trace 存储"] - Normal --> Storage - Health --> Drop["丢弃"] + Drop --> Log["非采样请求仅记录计数"] ``` | 环境 | 采样率 | 理由 | @@ -190,9 +506,36 @@ flowchart LR | **生产 - 错误链路** | 100% | 出错时的请求优先保留 | > [!note] 基于错误的智能采样 -> +> > 高级做法:**正常路径低采样,一旦检测到错误立即提升当前请求的采样率**。这样既省了存储,又能在出问题时有足够的数据回溯。 +### 采样对应用的影响 + +```mermaid +flowchart LR + subgraph Sampled["被采样的请求比例十百分比"] + S1["完整 Span 数据"] + S2["完整日志关联"] + S3["存储到后端"] + end + + subgraph Dropped["未被采样的请求比例九十百分比"] + D1["SDK 内仍创建 Span"] + D2["不影响业务逻辑"] + D3["上报时直接跳过 Export"] + end + + Note["采样不等于不执行\n采样只影响上报不影响业务"] + Note --> Sampled + Note --> Dropped +``` + +> [!warning] 常见误区 +> +> 1. **"采样后 span 就不创建了"** → 错!SDK 内部仍然创建和记录 span,只是在 `Exporter` 阶段跳过网络上报。 +> 2. **"采样会导致链路断裂"** → 配合 `ParentBased` 可以保证:只要链路上有一条被采样,整条链路的 span 都会被保留。 +> 3. **"统计 P99 会有偏差"** → 正确。低采样率下 P99 估计值会偏低,需用统计学方法做补偿估计。 + ## 渐进式落地路线 > [!tip] 不要试图一开始就采集全部 Span diff --git a/hzh/MS/02-服务治理/04-服务发现.md b/hzh/MS/02-服务治理/04-服务发现.md index 764d307..e5b36d5 100644 --- a/hzh/MS/02-服务治理/04-服务发现.md +++ b/hzh/MS/02-服务治理/04-服务发现.md @@ -1,6 +1,6 @@ --- tags: [microservice, service-discovery, consul, nacos, etcd, kubernetes] -create time: 2026-05-05 +create time: 2026-05-05 00:00 --- # 服务发现 @@ -20,7 +20,8 @@ graph TB A[Service A] -->|1.查询注册中心| R1[(Registry)] R1 -->|2.返回实例列表| A A -->|3.自选实例直连| B[Service B] - noteA[客户端内置负载均衡逻辑] + noteA["客户端内置负载均衡逻辑"] + B -.-> noteA end subgraph Server["服务端发现模式"] @@ -28,7 +29,8 @@ graph TB P -->|查询注册中心| R2[(Registry)] R2 -->|返回实例| P P -->|转发| D[Service D] - noteC[客户端无感知,透明代理] + noteC["客户端无感知,透明代理"] + D -.-> noteC end ``` @@ -47,7 +49,7 @@ graph TB **优势**: - 直连调用,少了一跳网络开销 - 客户端可以根据本地缓存做智能选择(如就近节点优先) -- 框架成熟,Eureka/Consul 社区案例丰富 +- 框架成熟,Eureka / Consul 社区案例丰富 **劣势**: - 每个服务实现中都需要集成 SDK,跨语言迁移成本高 @@ -73,34 +75,44 @@ graph TB 阿里开源,支持 AP(临时实例)+ CP(持久实例)双模型,同时提供配置管理功能。 ```go -// Nacos 服务注册 & 拉取 +// Nacos 服务注册与实例拉取 import ( "github.com/nacos-group/nacos-sdk-go/v2/clients" "github.com/nacos-group/nacos-sdk-go/v2/common/constant" + "github.com/nacos-group/nacos-sdk-go/v2/vo" ) -// 创建注册客户端 +// 1. 创建服务端配置(连接哪个 Nacos) sc := constant.ServerConfig{ IpAddr: "127.0.0.1", Port: 8848, } +``` -// 注册服务实例 +> 上面仅展示了 serverConfig,实际创建时还需传入 `ClientConfig`(命名空间、超时等)。为简洁起见此处省略。 + +```go +// 2. 创建 NamingClient client, _ := clients.NewNamingClient( - value_map.NewValueMap(map[string]any{"serverConfig": sc}), + map[string]any{"serverConfigs": []constant.ServerConfig{sc}}, ) -client.RegisterInstance(naming_param.RegisterInstanceParam{ +// 3. 注册服务实例(默认临时实例,走心跳保活) +_ = client.RegisterInstance(vo.RegisterInstanceParam{ Ip: "10.0.0.1", Port: 8080, ServiceName: "order-service", ClusterName: "DEFAULT", }) -// 拉取某服务的可用实例列表 -instances, _ := client.SelectInstances(naming_param.SelectInstanceParam{ +// 4. 拉取某服务的可用实例列表(同步快照,含健康过滤) +instances, _, _ := client.SelectInstances(vo.SelectInstanceParam{ ServiceName: "order-service", + HealthyOnly: true, // 只返回健康实例 }) +for _, inst := range instances { + // inst.Ip: inst.Port — 拿到后可直接发起 HTTP / gRPC 调用 +} ``` ### Consul @@ -112,6 +124,18 @@ HashiCorp 出品,基于 Raft 共识协议(CP),天然支持健康检查 - 支持 multi-datacenter 部署 - DNS 接口:`order-service.service.consul` 可直接解析 IP +**与 Nacos 选型对比**: + +| 维度 | Consul | Nacos | +|------|--------|-------| +| 一致性模型 | CP(Raft) | AP + CP 可切换 | +| 部署复杂度 | 需独立部署 Agent(Sidecar 模式) | Go SDK 内置,无额外代理 | +| 生态集成 | Terraform / Vault 深度绑定 | Spring Cloud / Dubbo 原生支持 | +| 适用场景 | 多云 / 混合云、基础设施层 | Java 技术栈、业务服务层 | + +> [!tip] Consul Sidecar 模式 +> Consul Agent 通常以 DaemonSet 方式跑在每台节点上,应用通过 localhost 向本地 Agent 注册。这种 **Sidecar 模式**天然适配服务端发现架构——应用把 Consul Agent 当作"本地注册中心"即可。 + ### etcd + K8s Service 纯容器环境下最自然的选择。etcd 存数据,K8s 提供完整的发现和负载均衡体系。 @@ -186,17 +210,25 @@ sequenceDiagram 4. 进程退出 > [!tip] K8s Pod 优雅终止 +> 以 sidecar 方式集成 Consul Agent 时,Pod 配置如下: > ```yaml > spec: -> terminationGracePeriodSeconds: 30 # 最多等 30 秒再 SIGKILL -> ``` -> 配合 `preStop` Hook 可以提前摘除流量: -> ```yaml -> lifecycle: -> preStop: -> exec: -> command: ["sh", "-c", "sleep 15"] # 给负载均衡摘流的时间 +> terminationGracePeriodSeconds: 30 # 最多等 30 秒再 SIGKILL +> initContainers: +> - name: consul-init # 先启动 Sidecar +> image: consul:latest +> command: ["consul", "agent", "-bind", "-config-file=/etc/consul.d/server.json"] +> containers: +> - name: app +> lifecycle: +> preStop: +> exec: +> command: ["sh", "-c", "sleep 15"] # 先停流量(让 LB / Consul 摘流) +> postStart: +> exec: +> command: ["sh", "-c", "sleep 5"] # 等 Sidecar 就绪后再开流量 > ``` +> 关键点:**preStop sleep** 留给 LB / Consul 摘流时间,**postStart sleep** 确保 Sidecar 已准备好再做健康检查。 ## 关联笔记 diff --git a/hzh/MS/02-服务治理/06-容错模式.md b/hzh/MS/02-服务治理/06-容错模式.md index 3cb09a6..ea2adf9 100644 --- a/hzh/MS/02-服务治理/06-容错模式.md +++ b/hzh/MS/02-服务治理/06-容错模式.md @@ -1,6 +1,6 @@ --- -tags: [microservice, circuit-breaker, retry, rate-limiting, bulkhead, health-check] -create time: 2026-05-05 +tags: [microservice, circuit-breaker, retry, rate-limiting, bulkhead, health-check, fallback, chaos-engineering] +create time: 2026-05-05 10:30 --- # 容错模式 @@ -21,6 +21,52 @@ graph TB > [!warning] 核心认知 > 不要假设任何网络调用会成功。每个远程调用的代码都应该考虑失败路径。 +> [!question]- 💡 思考题 +> **如果一个系统从未出现过故障,还需要容错设计吗?** +> +> 答案是肯定的。Netflix 的混沌工程理念指出:**"故障不是是否发生的问题,而是何时发生。"** +> 没有容错设计的系统在第一次外部依赖超时后就会暴露出级联崩溃的风险。 + +--- + +## 正文 + +### 各模式的组合使用 + +这 7 种容错模式通常不会单独使用,而是层层叠加形成纵深防御体系: + +```mermaid +flowchart LR + subgraph Layer1["第一层:快速失败"] + T["⏱ 超时控制
最先触发,避免线程等待"] + end + + subgraph Layer2["第二层:安全重试"] + R["⏱ 指数退避重试
给故障恢复的时间窗口"] + end + + subgraph Layer3["第三层:自我保护"] + CB["🔘 熔断器
高频失败时直接短路"] + RL["🔀 限流器
保护下游不被打满"] + BH["🧱 舱壁隔离
故障不蔓延"] + end + + subgraph Layer4["第四层:兜底策略"] + D["💤 降级响应
返回缓存/默认值"] + HC["💚 健康检查
自动剔除故障节点"] + end + + Layer1 --> Layer2 --> Layer3 --> Layer4 + + style Layer1 fill:#e3f2fd + style Layer2 fill:#fff3e0 + style Layer3 fill:#fce4ec + style Layer4 fill:#e8f5e9 +``` + +> [!tip] 使用顺序的重要性 +> 超时是所有机制的前提。一个没有超时的重试只会让请求无限期挂起。 + ## 1. 超时控制 这是所有容错机制的**前提**——没有超时的系统迟早会被拖垮。 @@ -96,6 +142,15 @@ func retryWithBackoff(ctx context.Context, maxRetries int, fn func() error) erro | 502/503/504 Gateway Error | 业务错误(如余额不足) | | 并发冲突(乐观锁失败) | 403 Forbidden | +> [!example]- ❌ 常见反模式:重试非幂等操作 +> ``` +> // 危险!每次重试都会创建一个新的订单 +> resp, err := httpClient.Post("/api/orders", jsonBody) +> if err != nil { retry() } // ← 重复下单! +> ``` +> +> **安全做法:** 为每个写操作生成全局唯一的 `requestId`,下游基于 requestId 做幂等校验。 + ## 3. 熔断器 (Circuit Breaker) 当下游服务频繁失败时,快速失败避免线程堆积雪崩。 @@ -155,7 +210,18 @@ if result.Err != nil { } ``` -## 4. 限流 (Rate Limiting) +### 熔断器 vs 限流器 + +> [!note]- 两者的区别(常被混淆) +> +> | 维度 | 熔断器 (Circuit Breaker) | 限流器 (Rate Limiter) | +> |------|------------------------|---------------------| +> | **触发条件** | 下游故障时才打开 | 始终生效,不依赖故障状态 | +> | **决策依据** | 成功率、失败次数 | 请求速率超过阈值 | +> | **目的** | 保护自身不被慢速下游拖垮 | 保护下游不被过多请求打满 | +> | **状态性** | 有状态(Closed/Open/Half-Open) | 无状态(持续监控流速) | +> +> 两者配合效果最佳:**限流在前挡流量洪峰,熔断在后兜底保护。** 保护下游服务不被过量请求压垮。 @@ -190,8 +256,8 @@ flowchart LR ```mermaid flowchart LR subgraph "🪣 令牌桶内部" - BUCKET[令牌桶
容量 = maxBurst] - REFILL[⚡ 固定速率 r/s 添加令牌
最多填到 maxBurst] + BUCKET["令牌桶
容量 = maxBurst"] + REFILL["⚡ 固定速率 r/s 添加令牌
最多填到 maxBurst"] end REQ[请求到达] --> CHECK{桶中有令牌?} @@ -214,16 +280,14 @@ flowchart LR 假设 `rate = 100/s`, `maxBurst = 200`: -``` -时间线(每秒) 桶中令牌数 行为 -───────────────── ─────────── ───────────── -t=0s 200 初始满桶 -t=1s 300→200 理论上应该增加到300,但桶满了,上限200 -t=2s ~ t=9s 100 稳定在 100(每秒消耗 ≈ 每秒生成) -t=10s 200 系统空闲,桶重新蓄满 -t=11s 0 ⚡ 瞬间涌入 200 个请求全部通过(突发!) -t=12s 0 后续只有 100 个能通过(恢复限速率) -``` +| 时间 | 桶中令牌数 | 行为 | +|------|-----------|------| +| `t=0s` | 200 | 初始满桶 | +| `t=1s` | 300 → 200 | 理论应增到300,但桶已满,上限200 | +| `t=2s~9s` | 100 | 稳定状态(每秒消耗 ≈ 每秒生成) | +| `t=10s` | 200 | 系统空闲,桶重新蓄满 | +| `t=11s` | 0 | ⚡ 瞬间涌入200个请求全部通过(突发!) | +| `t=12s` | 100 | 后续仅100个能通过(恢复限速率) | 这就是为什么令牌桶适合 API 网关——用户可能积攒了多个请求一口气发过来,直接全部拒绝体验很差;允许合理范围内的突发,用户体验更好。 @@ -301,7 +365,7 @@ func (tb *TokenBucket) Wait(ctx context.Context) error { ```mermaid flowchart TD - REQ["💧 请求源源不断流入"] --> QUEUE[🪣 漏桶
队列缓冲区] + REQ["💧 请求源源不断流入"] --> QUEUE["🪣 漏桶
队列缓冲区"] QUEUE --> PROCESS["🚰 以固定速率 drainer 处理
不管上游多快,只匀速处理"] QUEUE --> FULL{"桶满了?"} @@ -322,19 +386,14 @@ flowchart TD **关键特性对比令牌桶:** -``` -场景:突发流量 500qps 涌入,限流配置 rate=100/s, capacity=200 - -令牌桶视角: 漏桶视角: -┌──────────────┐ ┌──────────────┐ -│ ✅前200个通过 │ ← 桶里够令牌 │ ❌桶满了拒绝 │ ← 已经满了 -│ ⏱后面100个/秒 │ ← 恢复限速率 │ ──────────── │ -│ ❌多余的被拒 │ │ ✅匀速处理100/s│ ← 底部漏水 -└──────────────┘ └──────────────┘ - ↑ - 无论上游来多少, - 下游只看到匀速的100/s -``` +> 场景:突发流量 500qps 涌入,限流配置 rate=100/s, capacity=200 +> +> | 维度 | 令牌桶行为 | 漏桶行为 | +> |------|-----------|---------| +> | **前 200 个请求** | ✅ 通过(桶里有足够令牌) | ❌ 拒绝(桶已满了) | +> | **第 201~300 个** | ⏱ 按恢复速率放行(100/s) | ──────── | +> | **后续请求** | ❌ 多余的被拒绝 | ✅ 匀速处理 100/s | +> | **下游视角** | 有突发性 | 始终匀速的 100/s | | 维度 | 令牌桶 | 漏桶 | |------|--------|------| @@ -475,6 +534,12 @@ graph TB Pool1 -.-> note ``` +> [!question]- 💡 什么时候需要舱壁隔离? +> - 单一客户端同时调用多个不稳定下游时 +> - 共享进程内存/连接池,某服务线程阻塞会占用全部资源时 +> +> **不需要的情况:** 每个下游已经部署在独立进程中(此时故障天然隔离);下游数量极少,连接池开销可以接受。 + ```go // poolgroup 示例:为不同服务隔离连接池 orderPool := pool.New(10, 100) // 活跃10个,上限100个 @@ -487,6 +552,12 @@ payPool := pool.New(8, 80) 当系统部分不可用时,通过降级保证核心功能可用。 +> [!note]- 降级的触发时机 +> - **主动降级:** 管理员手动关闭非核心功能(大促期间关闭推荐、评论) +> - **被动降级:** 熔断器打开后自动切换到兜底响应 +> +> 生产环境中,**主动降级通常是第一选择**——在系统还可控时牺牲局部保全整体,比等雪崩发生后再被动的处理代价更小。 + ```mermaid flowchart TD Req["用户请求"] @@ -534,6 +605,26 @@ sequenceDiagram - **就绪探针 (Readiness)**:判断"是否能接收流量",未就绪则剔除负载均衡池 - **Readiness 比 Liveness 更重要**——一个进程活着但数据库连接耗尽时,应该停止接收流量而不是重启 +> [!tip] 生产环境最佳实践 +> 健康检查端点 `/healthz` 不应只做 HTTP 200,还应验证关键依赖(DB、Redis)的连通性。一个数据库连接耗尽的服务返回 200 是虚假的健康信号。 + +--- + +## 总览:模式对比速查 + +| 模式 | 保护谁 | 何时生效 | 复杂度 | 推荐优先级 | +|------|--------|---------|--------|-----------| +| ⏱ **超时控制** | 自身线程/连接 | 每次远程调用 | ⭐ | 🔴 必须实现 | +| ⏱ **重试机制** | 瞬时故障 | 失败后自动触发 | ⭐⭐ | 🔴 幂等操作必做 | +| 🔘 **熔断器** | 自身系统 | 高频失败时 | ⭐⭐ | 🟠 外部依赖必配 | +| 🔀 **限流器** | 下游服务 | 始终生效 | ⭐⭐ | 🟠 网关层必配 | +| 🧱 **舱壁隔离** | 其他服务调用 | 某下游异常时 | ⭐⭐⭐ | 🟡 多下游场景 | +| 💤 **降级策略** | 用户核心体验 | 非核心不可用时 | ⭐⭐ | 🟠 面向 C 端必做 | +| 💚 **健康检查** | 负载均衡/调度 | 实例异常时 | ⭐ | 🔴 K8s 必配 | + +> [!success]- 一句话总结 +> **超时保底、重试修复瞬时故障、熔断防雪崩、限流护下游、舱壁保隔离、降级保核心。** + ## 关联笔记 - [[02-服务治理/01-API网关]] — 网关层的限流和熔断插件 diff --git a/hzh/MS/02-服务治理/07-配置管理.md b/hzh/MS/02-服务治理/07-配置管理.md index 2d4ae4c..98c70e8 100644 --- a/hzh/MS/02-服务治理/07-配置管理.md +++ b/hzh/MS/02-服务治理/07-配置管理.md @@ -1,53 +1,79 @@ --- tags: [microservice, config-management, nacos, apollo, spring-cloud-config] -create time: 2026-05-05 +create time: 2026-05-05 14:30 --- # 配置管理 ## 概述 -当服务实例数以百计时,手动管理配置是不可想象的。配置中心提供 **集中化、动态化、版本化** 的配置管理能力。 +当服务实例数以百计时,手动管理配置文件和维护 `.env` 文件的时代该结束了。配置中心为微服务体系提供 **集中化、动态化、版本化** 的管理能力——所有配置变更通过统一入口进行,实时推送到目标实例,全程可追溯。 > [!question] 引出配置中心的必要性 > 假设你的 50 个服务实例都要连同一个数据库。现在需要把数据库密码从 `password1` 改为 `password2`,你会怎么改?登录每台机器改配置文件?滚动重启所有 Pod?还是……有更好的办法? +> [!tip] 核心洞察 +> 传统配置管理的瓶颈不在「改」,而在「改之后如何生效」。配置中心的本质价值是将配置的 **修改** 和 **传播** 解耦——你只需要在控制台点一次提交,剩余的分发、版本记录、回滚保障全部由系统自动完成。 + ## 核心价值 ```mermaid flowchart LR - Dev["开发/运维"] --> CC[(配置中心)] - CC -->|热更新推送| S1[Service A] - CC -->|热更新推送| S2[Service B] - CC -->|热更新推送| S3[Service C] - - CC -.->|版本管理| V1[v1.0 历史配置] - CC -.->|版本管理| V2[v1.1 当前配置] + Dev["开发运维"] --> CC[(配置中心)] + CC -->|"热更新推送"| S1[Service A] + CC -->|"热更新推送"| S2[Service B] + CC -->|"热更新推送"| S3[Service C] + + CC -.->|"版本管理"| V1["v1.0 历史配置"] + CC -.->|"版本管理"| V2["v1.1 当前配置"] ``` -| 能力 | 说明 | -|------|------| -| **动态刷新** | 修改配置后立即生效,无需重启 | -| **环境隔离** | dev / test / prod 配置分离 | -| **版本管理与回滚** | 每次变更有迹可循,一键回滚 | -| **权限控制** | 敏感配置(密钥、Token)按角色隔离 | -| **配置审计** | 记录谁在什么时候改了什么 | +| 能力 | 说明 | 为什么重要 | +|------|------|-----------| +| **动态刷新** | 修改配置后立即生效,无需重启 | 灰度发布时按百分比调参,零停机发布 | +| **环境隔离** | dev / test / prod 配置分离 | 避免生产配置误改到测试环境 | +| **版本管理与回滚** | 每次变更有迹可循,一键回滚 | 配置错误导致服务雪崩时,30 秒恢复 | +| **权限控制** | 敏感配置(密钥、Token)按角色隔离 | 防止越权修改核心参数 | +| **配置审计** | 记录谁在什么时候改了什么 | 合规要求 + 事故排查追溯 | + +> [!note] 配置刷新的两种策略 +> +> | 策略 | 原理 | 延迟 | 适用场景 | +> |------|------|------|---------| +> | **长轮询 (Long Polling)** | 客户端发起请求后服务端挂起等待(通常 30s),有变更立即返回 | 1~3s | Nacos / Apollo 采用的方案,兼顾实时性和服务端负载 | +> | **短轮询 (Short Polling)** | 客户端定时拉取(如每 60s GET 一次) | 最高等于轮询间隔 | 简单但浪费带宽,不推荐 | +> +> 长轮询的伪代码示意: +> +> ```go +> // 伪代码——演示长轮询原理 +> func longPoll(key string, timeout time.Duration) (string, bool) { +> start := time.Now() +> for time.Since(start) < timeout { +> if hasChanged(key) { // 检查服务端是否有新版本 +> return fetchLatest(key), true +> } +> time.Sleep(500 * time.Millisecond) // 短暂休眠再查 +> } +> return "", false // 超时,客户端重新发起长轮询 +> } +> ``` ## 配置分层模型 ```mermaid graph TB - subgraph "配置优先级(低 → 高)" - Base["Base 基线配置
各项目共享的默认值"] - App["应用级配置
每个服务的专属配置"] - Env["环境级配置
dev/test/prod 差异"] - Instance["实例级配置
单节点调优参数"] + subgraph "配置优先级低到高" + A["Base 基线配置
各项目共享默认值"] + B["应用级配置
每个服务的专属配置"] + C["环境级配置
dev test prod 差异"] + D["实例级配置
单节点调优参数"] end - - style Base fill:#e3f2fd - style App fill:#fff3e0 - style Env fill:#fce4ec - style Instance fill:#e8f5e9 + + style A fill:#e3f2fd + style B fill:#fff3e0 + style C fill:#fce4ec + style D fill:#e8f5e9 ``` **典型配置项分层**: @@ -59,31 +85,69 @@ graph TB | 环境配置 | DB 连接串、Feature Flag | 中 | | 实例配置 | 单机限流阈值、调试开关 | 高 | +> [!question] 分层设计思辨 +> 如果基线配置和应用配置都指向同一个 Key(比如 `log.level`),最终生效的是哪一个? +> +> **答**:优先级高的覆盖优先级低的,即:**实例级 > 环境级 > 应用级 > 基线级**。这种覆盖机制类似 K8s 中 flags > env > image default 的多层注入。 + ## Nacos Config 示例 +Nacos 配置管理的三个核心概念: + +| 概念 | 类比 | 作用 | +|------|------|------| +| **Data ID** | 文件名 | 唯一标识一份配置 | +| **Group** | 文件夹分组 | 将相关配置归类(如 `DEFAULT_GROUP`、`ORDER_GROUP`) | +| **Namespace** | 虚拟隔离域 | 不同环境(dev/test/prod)完全隔离,互不可见 | + +### 初始化与读取配置 + ```go -// 动态监听配置变更 +// 初始化 Nacos Config Client configClient, _ := clients.NewConfigClient(value_map.NewValueMap(map[string]any{ - "serverConfig": sc, + "serverConfig": sc, // Server 地址、鉴权信息 + "namespace": "your-ns-id", // 命名空间隔离(可选) })) +// 获取当前配置内容 content, _ := configClient.GetConfig(config_param.GetConfigParam{ DataId: "order-service.yaml", Group: "DEFAULT_GROUP", + // Namespace 在 Client 初始化时指定 }) -// 监听配置变化——Nacos 推送更新回调 +_ = content // 解析 YAML → 填充到应用程序的配置结构体 +``` + +### 监听配置变化——热更新回调 + +```go +// 注册监听器——Nacos 有配置变更时会推送回调 configClient.ListenChange(config_param.ListenChangeParam{ DataId: "order-service.yaml", Group: "DEFAULT_GROUP", Callback: func(content string) { - fmt.Println("配置更新了:", content) - // 重新加载配置... - ReloadConfig(content) + fmt.Println("配置更新了,开始热加载...") + // 步骤 1: 解析新配置 + newCfg := &Config{} + yaml.Unmarshal([]byte(content), newCfg) + // 步骤 2: 原子替换(用 lock 保证并发安全) + cfgMutex.Lock() + globalConfig = newCfg + cfgMutex.Unlock() + // 步骤 3: 通知依赖配置的组件重新初始化 + NotifyConfigChange(newCfg) }, }) ``` +> [!warning] 热更新的注意事项 +> +> 1. **线程安全**:配置结构体必须用 `sync.RWMutex` 保护读写,避免竞态条件 +> 2. **幂等性**:回调可能被多次触发,ReloadConfig 应该是幂等操作 +> 3. **优雅降级**:新配置格式错误时,保留旧配置而不是直接崩溃 +> 4. **冷启动兼容**:客户端首次启动先拉取快照配置,再注册监听器——避免两者之间存在时间窗口导致漏掉变更 + ### 配置文件的命名规范 推荐格式:`{service-name}.{environment}.yaml` @@ -94,8 +158,61 @@ configClient.ListenChange(config_param.ListenChangeParam{ | order-service | prod | `order-service.prod.yaml` | | user-service | prod | `user-service.prod.yaml` | +> [!tip] 进阶:配置合并 +> 实际项目中通常拆分多份配置文件: +> - `{service}.yaml` — 基础配置(公共部分) +> - `{service}.db.yaml` — 数据库专项配置 +> - `{service}.redis.yaml` — Redis 专项配置 +> +> Nacos 支持通过 `Shared Configs` 机制合并多份 DataID 的配置,启动时一次性拉取并按顺序合并。 + +## Spring Cloud Config 补充 + +对于 Java/Spring 生态,Spring Cloud Config 是经典选择: + +```yaml +# application.yml — 客户端接入 +spring: + cloud: + config: + uri: http://config-server:8888 + name: order-service # 对应服务端 Git 仓库中的 order-service.yml + profile: prod # 选择环境分支 + label: main # Git 分支 +``` + +```java +// 注解驱动——配置变更自动刷新 +@RestController +@RefreshScope // 关键:标记此 Bean 支持运行时刷新 +public class OrderController { + + @Value("${feature.new-order-flow:true}") + private boolean newOrderFlowEnabled; + + @GetMapping("/orders") + public List list() { + if (newOrderFlowEnabled) { + // 新版流程 + } + return orderService.list(); + } +} +``` + +> [!note] Spring Cloud Config 架构特点 +> Spring Cloud Config 后端通常对接 Git 仓库,配置变更的本质就是 **Git commit**。这意味着天然拥有 Git 的所有能力(diff、回滚、分支管理),但也引入了依赖外部存储的延迟问题。通常搭配 **Bus 消息总线**(Spring Cloud Bus + RabbitMQ/Kafka)实现广播式推送,解决纯拉取模式的延迟缺陷。 + ## Apollo vs Nacos Config 对比 +上一节以 Nacos 为例介绍了配置中心客户端的接入方式。但除了阿里系的 Nacos,业界还有其他成熟选择,其中 Apollo 是最常被拿来比较的另一款方案。了解它们各自的定位差异,能帮助我们在选型时少踩坑。 + +**Apollo** 由携程开源(现Apache孵化),定位为专业的企业级配置管理平台。它从诞生起就专注于「配置管理」这一件事,在设计上做了大量精细化的考量:比如配置发布前可以预览 diff、支持灰度发布某个实例、操作有审核流程等。适合对配置管控要求严格的多团队大型企业。 + +**Nacos Config** 是阿里 Nacos 组件的子模块。Nacos 本身是一套「服务发现 + 配置管理」的二合一平台——如果你已经在用 Nacos 做服务发现,顺势用它管配置几乎是零额外成本的选择。它的优势在于轻量、上手快,在中小团队中落地速度更快。 + +两者底层都基于 AP 模型(可用性优先),推送延迟都在 1s 以内。真正的差异不在性能,而在 **功能丰富度** 和 **生态适配**: + | 维度 | Apollo (携程) | Nacos Config (阿里) | Spring Cloud Config | |------|--------------|---------------------|--------------------| | **界面体验** | Web UI 完善,操作直观 | 较好 | 需自建 | @@ -105,8 +222,19 @@ configClient.ListenChange(config_param.ListenChangeParam{ | **生态集成** | 适合 Java/Spring 体系 | Java + Go + Python 等 | 仅 Spring 生态 | | **适用场景** | 大型企业,多团队协同 | 中小团队快速落地 | 纯 Spring 项目 | +> [!tip] 选型建议 +> +> 1. **刚起步的微服务团队**:选 Nacos,一套组件同时搞定服务发现和配置管理,减少运维成本 +> 2. **已有 Spring Cloud 全家桶**:优先 Spring Cloud Config,生态无缝衔接 +> 3. **大型企业多团队协作**:Apollo 的权限体系和发布流程更成熟,适合精细化管控 +> 4. **K8s 原生项目**:简单配置走 ConfigMap + Secret,复杂场景引入外部配置中心 + ## K8s ConfigMap & Secret +以上介绍的都是 **独立部署** 的配置中心(Nacos / Apollo),它们通过客户端 SDK 与应用解耦。但当你的基础设施完全跑在 Kubernetes 上时,K8s 本身已经内置了一套轻量级的配置注入机制——ConfigMap 和 Secret,不需要额外搭建外部服务。 + +**ConfigMap** 用于存放非敏感配置,本质是 K8s 上的一个 key-value store,可以被注入为环境变量、命令行参数或挂载为配置文件到 Pod 中。**Secret** 则是它的敏感版本,专存密码、密钥、Token 等数据——虽然默认只是 base64 编码(不是加密),但语义上和权限管控上与 ConfigMap 做了区分。 + 纯 K8s 环境内的原生方案: ```yaml @@ -158,14 +286,56 @@ flowchart LR Dev["开发者本地"] -->|"K8s Secret / Vault"| Store["加密存储"] Store -->|"运行时解密"| Runtime["运行时的环境变量"] Runtime --> App["应用程序"] - + Audit["审计系统"] -.->|"只读访问"| Store ``` **推荐方案**: -- **K8s Secret** — 小型集群内使用(base64 编码,非加密,配合 EncryptionConfiguration 增强) -- **HashiCorp Vault** — 企业级密钥管理,动态秘钥、自动轮换 -- **云厂商 KV 服务** — AWS Secrets Manager / 阿里云 KMS / 腾讯云 SecretManager + +| 方案 | 适用规模 | 特点 | +|------|---------|------| +| **K8s Secret** | 小型集群 | base64 编码(非加密),配合 EncryptionConfiguration 增强 | +| **HashiCorp Vault** | 企业级 | 动态秘钥、自动轮换、细粒度访问策略 | +| **云厂商 KV 服务** | 云原生项目 | AWS Secrets Manager / 阿里云 KMS / 腾讯云 SecretManager,免运维 | + +> [!tip] Vault 的杀手锏:动态秘钥 +> Vault 可以为每次请求生成一个临时的数据库凭证,设定 TTL 为 1 小时——过期自动销毁。相比静态密码方案,即使秘钥泄露也只有 1 小时的危害窗口。这是传统配置中心无法做到的。 + +## 常见问题排查 + +> [!abstract] 实战排障指南 +> +> ### Q1: 配置改了但服务没生效? +> +> **排查清单**: +> 1. 确认修改的是正确的 Namespace / 环境 +> 2. 检查监听器是否成功注册(看客户端日志有无 `ListenChange success` 类日志) +> 3. 确认回调函数内部有没有 panic 导致回调中断 +> 4. 长轮询是否被代理或负载均衡器超时切断(常见于网关配置了 30s 超时) +> +> ### Q2: 配置热加载后出现内存泄漏? +> +> 每次 ReloadConfig 都创建新对象是正常行为,但要确保: +> - 旧配置对象的引用全部被替换(无其他地方仍持有旧引用) +> - 如果使用缓存结构,注意清理旧的缓存键 +> - Go 语言的 GC 会自动回收无引用对象,但大量频繁热加载时可以观察 `runtime.MemStats` +> +> ### Q3: 配置中心挂了怎么办? +> +> **最佳实践**:客户端做本地缓存(File-based Fallback)。 +> +> ```go +> func getConfig(dataID string) ([]byte, error) { +> // 第一步:尝试从配置中心拉取 +> content, err := remoteClient.getConfig(dataID) +> if err == nil { +> saveToLocalCache(dataID, content) // 缓存到本地 +> return content, nil +> } +> // 第二步:配置中心不可用时,读取本地缓存 +> return loadFromLocalCache(dataID) +> } +> ``` ## 关联笔记 diff --git a/hzh/MS/02-服务治理/08-流量治理.md b/hzh/MS/02-服务治理/08-流量治理.md index 425f209..51e1d51 100644 --- a/hzh/MS/02-服务治理/08-流量治理.md +++ b/hzh/MS/02-服务治理/08-流量治理.md @@ -1,6 +1,6 @@ --- -tags: [microservice, canary-release, blue-green, traffic-routing, istio] -create time: 2026-05-05 +tags: [microservice, canary-release, blue-green, traffic-routing, istio, load-balancing] +create time: 2026-05-05 10:30 --- # 流量治理 @@ -12,20 +12,20 @@ create time: 2026-05-05 > [!question] 如何安全地上线? > 你修复了一个紧急 Bug,但这次改动涉及核心支付链路。直接全量发布一旦出问题损失巨大。有没有办法先小范围验证,确认没问题再放量? -## 灰度策略全景 +答案就是**按规则拆分流量**——将请求按照不同维度分配到新旧版本,用真实用户的流量来检验新代码,而不是靠测试环境的人为模拟。 ```mermaid flowchart LR A["🟢 v1 (稳定版)
95% 流量"] --> B["🔵 v2 (测试版)
5% 流量"] B --> C{"监控指标?"} - C -- "✅ 一切正常" --> D["逐步放量
10% → 25% → 50%"] C -- "❌ 错误率飙升" --> E["自动回滚到 v1 🔄"] - D --> F{"全部放量至 100%"} F --> G["🟢 v2 成为新稳定版"] ``` +## 一、灰度策略全景 + ### 常见灰度策略对比 | 策略 | 描述 | 优点 | 缺点 | 适用场景 | @@ -36,7 +36,48 @@ flowchart LR | **按地域** | 某个城市/机房的新版本 | 地域性问题的理想试验场 | 地域偏差大 | 全球化部署 | | **按设备类型** | iOS 新用户先用新版 | 覆盖目标人群 | 样本有限 | 移动端发版 | -## 蓝绿 vs 灰度 +> [!tip] 实战建议:多策略组合 +> 生产环境中通常不会只用单一规则。推荐的做法是分层判断: +> +> ``` +> 第1层:内部员工 IP → 100% 进灰度(快速发现 bug) +> 第2层:header x-canary=true → 100% 进灰度(QA 验证) +> 第3层:新注册用户 → 50% 进灰度(扩大样本) +> 第4层:其余用户 → 默认 v1 +> ``` + +### 按权重分流的 Go 实现 + +在应用层面(如 go-zero 或自研网关),流量分配往往通过中间件实现: + +```go +// CanaryMiddleware: header 携带 canary=true 的请求直通灰度版本 +func (m *CanaryMiddleware) Next(ctx context.Context, req any, reply any, rpc func(context.Context, any, any) error) error { + if m.r.isGray(ctx) { // 从 header/context 提取灰度标记 + return m.grayHandler(ctx, req, reply) // 路由到灰度服务实例 + } + return m.next(ctx, req, reply) // 走正常路径 +} +``` + +### 客户端负载均衡中的灰度 + +客户端侧的灰度通常通过自定义负载算法实现,以加权轮询为例: + +```go +// WeightedRoundRobin: 权重感知轮询 +type WeightedRoundRobin struct { + servers []*Server // 包含 weight 字段 + totalWeight int +} + +func (w *WeightedRoundRobin) Next() (*Server, error) { + // 每次选取当前权重最高的可用服务器 + // v1 权重 95, v2 权重 5 → 平均每 20 次请求 1 次命中 v2 +} +``` + +## 二、蓝绿 vs 灰度 > [!summary] 两种发布策略对比 > @@ -69,7 +110,7 @@ graph TB S2["部署 v2
5% 流量"] S3["监控 15min"] S4{"P99 延迟 < SLA?"} - + S4 -- 否 --> Rollback["回滚 v2 🔄"] S4 -- 是 --> S5["放量 25%"] S5 --> S6["监控 30min"] @@ -80,15 +121,20 @@ graph TB S9 --> S10{"全项达标?"} S10 -- 是 --> S11["100% 全量 ✅"] S10 -- 否 --> Rollback - + style Rollback fill:#ffebee style S11 fill:#e8f5e9 ``` -## 高级路由规则 +> [!warning] 关键决策点 +> 每个放量节点都是一个"继续 or 回滚"的决策门控。不要跳过任何一步,哪怕之前几轮都顺利通过。历史教训表明:**很多事故发生在最后一跳**。 + +## 三、高级路由规则 ### Istio VirtualService +Istio 作为 Service Mesh 方案,流量治理能力强大但侵入性也更高。适合已有 K8s + Istio 基础设施的团队。 + ```yaml # 灰度规则:header 携带 canary=true 的用户走 v2 apiVersion: networking.istio.io/v1beta1 @@ -132,24 +178,58 @@ http: weight: 10 ``` -## 流量治理的其他维度 +### 网关层 vs Sidecar 层 + +流量治理可以在两个不同的层级实现,选择取决于团队的基础设施成熟度: + +```mermaid +flowchart TD + subgraph "网关层(中心化)" + GW["API Gateway
集中管理路由规则"] + GW --> V1[[v1 实例集群]] + GW --> V2[[v2 实例集群]] + end + + subgraph "Sidecar 层(分布式)" + GW2["API Gateway
只做转发"] + GW2 --> ProxyV1["Envoy Sidecar
本地执行路由规则"] + GW2 --> ProxyV2["Envoy Sidecar
本地执行路由规则"] + ProxyV1 --> ContainerV1[[v1 容器]] + ProxyV2 --> ContainerV2[[v2 容器]] + end +``` + +| 维度 | 网关层治理 | Sidecar 层治理 | +|------|-----------|---------------| +| **复杂度** | 配置统一,易维护 | 需理解 Mesh 概念 | +| **性能** | 单点瓶颈风险 | 就近处理,开销更低 | +| **灵活性** | 依赖网关插件能力 | Istio 支持更丰富的规则 | +| **学习曲线** | 低 | 高 | +| **适用团队** | 中小型、快速起步 | 大规模微服务集群 | + +> [!note] 选型建议 +> - **初创期**:直接在网关或应用层做路由分发即可,不必引入 Service Mesh +> - **成长期**:当服务数量超过 30+、跨团队协作复杂时,考虑用 Istio 统一管理 +> - **混合模式**:网关层做简单的按路径/权重分发,Sidecar 层做精细的 header/metadata 路由 + +## 四、流量治理的其他维度 除了发布策略,流量治理还包括: -| 能力 | 说明 | -|------|------| -| **熔断降级** | 下游不可用时自动降级,参见 [[02-服务治理/06-容错模式]] | -| **限流** | 保护上游不因过量请求被打垮,参见 [[02-服务治理/06-容错模式]] | -| **重试** | 对瞬态故障自动恢复,参见 [[02-服务治理/06-容错模式]] | -| **黑白名单** | IP 级别访问控制 | -| **A/B Testing** | 基于用户分组的长期实验 | +| 能力 | 说明 | 关联文档 | +|------|------|---------| +| **熔断降级** | 下游不可用时自动降级 | [[02-服务治理/06-容错模式]] | +| **限流** | 保护上游不因过量请求被打垮 | [[02-服务治理/06-容错模式]] | +| **重试** | 对瞬态故障自动恢复 | [[02-服务治理/06-容错模式]] | +| **黑白名单** | IP 级别访问控制 | — | +| **A/B Testing** | 基于用户分组的长期实验 | — | -## 灰度发布的量化决策依据 +## 五、灰度发布的量化决策依据 > [!tip] 必须有可量化的观测指标作为决策依据 -> +> > 不要凭感觉放量,让数据说话: -> +> > | 指标 | 阈值参考 | 告警级别 | > |------|---------|---------| > | **错误率** | < 0.1% (v1 baseline 对比) | > 1% 立即回滚 | @@ -157,8 +237,48 @@ http: > | **CPU/Memory** | 在 limits 的 70% 以内 | > 85% 扩容 | > | **下游依赖成功率** | > 99.5% | < 95% 熔断 | +### 灰度监控面板设计要点 + +一个完整的灰度面板应该能一眼看出新旧版本的差异: + +```mermaid +flowchart LR + subgraph RealTime["实时对比视图"] + R1["📊 P99 延迟趋势
v1 vs v2 双线对比"] + R2["🔴 错误率柱状图
区分 HTTP 状态码分类"] + R3["⚡ QPS 流量分布
按版本分堆叠面积图"] + end + + subgraph HealthCheck["健康检查"] + H1["✅ 版本 v1 正常运行
副本数: 10/10"] + H2["⚠️ 版本 v2 异常
副本数: 2/5 · 失败: 3"] + end + + RealTime --> Decision["🎯 是否继续放量?"] + HealthCheck --> Decision + + Decision -- "指标超标" --> Rollback["触发自动回滚"] + Decision -- "指标正常" --> Expand["推进下一放量阶段"] + + style Rollback fill:#ffebee,color:#000 + style Expand fill:#e8f5e9,color:#000 +``` + +## 六、实践 checklist + +> [!example] 灰度上线前必查清单 +> +> - [ ] 旧版本保持至少一个副本不销毁(随时回滚) +> - [ ] 灰度期间禁止其他无关变更上线 +> - [ ] 已配置好 v1 vs v2 的双线对比面板 +> - [ ] 定义了明确的放量阶段和触发条件(何时扩、何时停) +> - [ ] 准备了回滚脚本 / 一键回滚操作 +> - [ ] 通知了相关干系人(运维、产品、客服) +> - [ ] 选择了影响面最小的时间段开始灰度 + ## 关联笔记 - [[02-服务治理/01-API网关]] — API Gateway 的路由和权重分发 +- [[02-服务治理/06-容错模式]] — 熔断、限流等配套治理手段 - [[04-可观测性/01-Metrics监控]] — 灰度期间的监控面板设计 - [[05-部署运维/04-SRE实践]] — 基于 SLO 的发布决策 diff --git a/hzh/MS/02-服务治理/09-网关鉴权策略.md b/hzh/MS/02-服务治理/09-网关鉴权策略.md index 809f0e6..c081ee4 100644 --- a/hzh/MS/02-服务治理/09-网关鉴权策略.md +++ b/hzh/MS/02-服务治理/09-网关鉴权策略.md @@ -1,22 +1,33 @@ --- -tags: [microservice, api-gateway, auth, service-mesh, mTLS, jwt, rbac] +tags: [microservice, api-gateway, auth, service-mesh, mTLS, jwt, rbac, zero-trust] create time: 2026-04-30 15:30 +update time: 2026-05-17 10:00 --- # 网关鉴权分层设计 ## 概述 -探讨"鉴权是否应该全部放在网关统一处理"这个常见架构争议,给出兼顾统一性和灵活性的分层鉴权方案。 +每个微服务团队都会面临同一个灵魂拷问:**鉴权该放在网关统一做,还是各服务自己管?** + +放网关——简单粗暴但不够灵活;放各服务——灵活但容易失控。本文给出一个经过多团队实践验证的**三层鉴权架构**方案,兼顾统一安全基线与差异化业务需求。 ## 问题的本质 ### 一刀切的陷阱 > [!question] 引出思考 -> 有人说:"鉴权放到网关统一做,避免每个服务重复写。"但如果某个内部服务不需要鉴权呢?或者不同团队要求不同的鉴权方式呢?你怎么设计才能兼顾统一性和灵活性? +> 有人说:"鉴权放到网关统一做,避免每个服务重复写。"听起来很美好——但现实中有三个场景会让这个假设当场翻车: +> +> - 某个内部查询接口被高频调用,走一遍网关的 JWT 校验白白增加 3ms 延迟 +> - 财务团队被审计要求 OAuth2 + MFA,运营后台只需要 API Key +> - 订单服务必须判断"用户 A 是否有权查看**这笔**订单",网关根本不知道数据归属关系 +> +> **你怎么设计才能兼顾统一性和灵活性?** -核心矛盾在于:**网关做鉴权的优势是集中化和性能**,但代价是 **失去对差异化需求的表达能力**。现实中至少存在三种网关无法处理的场景: +核心矛盾在于:**网关做鉴权的优势是集中化和性能**,但代价是 **失去对差异化需求的表达能力**。 + +现实中至少存在三种网关无法处理的场景: | 场景 | 例子 | 为什么网关搞不定 | |------|------|----------------| @@ -71,6 +82,27 @@ flowchart TB **设计哲学**:越往上过滤越早失败,但信息越少;越往下信息越多但成本越高。每一层只做它最擅长的那件事。 +### 快速检查 + +> [!question] 思考题 +> 假设你在设计一个电商系统,订单查询接口的鉴权应该如何分层?尝试为以下三个请求场景分别标记出需要生效的层级(L1/L2/L3): +> +> | 场景 | 应该经过哪几层 | 理由 | +> |------|--------------|------| +> | 外部 App 用户通过公网请求订单详情 | ? | ? | +> | 运营后台手动查询某条订单记录 | ? | ? | +> | 仓储系统的定时任务拉取未发货订单列表 | ? | ? | + +> [!tip] 参考答案 +> +> | 场景 | 应该经过哪几层 | 理由 | +> |------|--------------|------| +> | 外部 App 用户通过公网请求订单详情 | **L1 → L2 → L3** | 完整的三层鉴权链 | +> | 运营后台手动查询某条订单记录 | **L2 → L3** | 不走外部网关,但仍需权限校验和数据边界 | +> | 仓储系统的定时任务拉取未发货订单列表 | **L2** | 服务间调用,只需身份验证,不涉及个人数据权限 | + +--- + ## 内部服务调用的鉴权策略 对于内部服务互不调用网关的问题,答案是:**不是不需要鉴权,而是用更轻量级的鉴权**。 @@ -112,6 +144,60 @@ func verifyInterServiceToken(token string) error { | 简单 Token 签名 | ~1ms | ⚠️ 中等 | 同一安全域内的服务互调 | | mTLS 双向认证 | ~2ms | ✅ 高 | 跨租户、跨团队、混合云环境 | +### 鉴权上下文的层间传递 + +三层鉴权架构最大的工程挑战是:**L1 层校验出的用户信息如何传递到 L2、L3?** 如果每层都独立重新获取身份信息,不仅浪费性能,还会造成数据不一致。 + +```go +// AuthContext 在请求链路中的传递结构 +type AuthContext struct { + UserID string `json:"user_id"` // 来自 L1 JWT Claims + Username string `json:"username"` + Roles []string `json:"roles"` // 来自 L2 RBAC 查询结果 + TenantID string `json:"tenant_id"` // 用于 L3 数据隔离 + AccessToken string `json:"-"` // 不向下透传,防止越权 +} + +// 网关中间件 — 解析 JWT 后注入上下文 +func gatewayAuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + claims, err := validateJWT(r.Header.Get("Authorization")) + if err != nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + ctx := context.WithValue(r.Context(), authCtxKey{}, &AuthContext{ + UserID: claims.Subject, + Roles: claims.Roles, + TenantID: claims.Tenant, + }) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} +``` + +> [!note] 关键设计原则 +> +> - **AccessToken 不向下透传**:下游服务只需要身份标识,不需要完整凭证,避免被截获后的越权风险 +> - **上下文不可伪造**:L2/L3 只能通过 `context.Value` 读取,不能自行构造,确保每一层的决策都有据可查 +> - **TenantID 用于 L3 数据隔离**:这是多租户 SaaS 的关键防线,防止 A 租户用户通过 API 参数访问 B 租户数据 + +### 常见陷阱:忘记设置超时 + +> [!danger] 致命错误 +> +> ```go +> // ❌ 没有超时的鉴权检查 +> result := db.QueryContext(ctx, "SELECT * FROM permissions WHERE user_id = ?", userID) +> +> // ✅ 始终为鉴权相关数据库查询设置超时 +> queryCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) +> defer cancel() +> result := db.QueryContext(queryCtx, "SELECT * FROM permissions WHERE user_id = ?", userID) +> ``` +> +> 鉴权链路上的任何一个环节卡死,都会导致整个请求挂起。**鉴权失败的代价应该比正常请求慢几毫秒,而不是让请求永远 hang 住**。生产事故中超过 30% 的 P0 事件与鉴权组件超时有关。 + ## 多团队差异化鉴权方案 针对第二个问题——不同团队需要不同的鉴权方式——推荐用 **策略即代码(Policy-as-Code)** + **可插拔鉴权中间件**。 @@ -151,6 +237,22 @@ router.Use(multiAuth( > 3. **职责边界清晰**:平台团队维护基础设施鉴权,业务团队维护业务鉴权 > 4. **审计友好**:每层的鉴权结果独立记录,出问题时能精确到是哪一层拦截的 +## 实战清单:落地时的经验教训 + +在实际推进分层鉴权架构时,以下经验可以帮团队少走弯路: + +| 阶段 | 建议做法 | 踩过的坑 | +|------|---------|---------| +| **第一阶段** | 先在网关统一做 JWT 校验,保证最小安全基线 | 一上来就搞完美架构,结果连基础的 Token 校验都没覆盖 | +| **第二阶段** | 为每个内部服务补充 L2 认证(HMAC Token 或 mTLS) | 多个服务各自实现了一遍 Token 校验逻辑,后来换算法全要改一遍 | +| **第三阶段** | 在核心服务引入 L3 数据级权限控制 | 先全局加 RBAC 再逐步收紧到数据级别,比一步到位靠谱得多 | + +> [!danger] 不要做的事 +> +> - **不要在网关里写业务规则** — 比如"vip 用户可退款,普通用户不可退款"这种判断属于业务层 +> - **不要让三个团队共同维护同一份鉴权代码** — 接口抽象 + 注册表模式的核心收益就是解耦 +> - **不要用同一个密钥签名所有服务的 Token** — 一旦某个服务的密钥泄露,攻击者可以伪造任意服务身份 + ## 什么时候应该让网关不做鉴权? > [!warning] 反模式警告 @@ -162,7 +264,7 @@ router.Use(multiAuth( > 3. **Webhook 回调**:上游厂商的回调签名验证应在消费侧服务完成 > 4. **高性能扫描场景**:高频行情推送,鉴权开销占总延迟 >5% 时可考虑批量校验 -## 总结 +## 总结与进阶思考 这套设计的核心理念是 **"默认严格、按需放宽"**: @@ -172,6 +274,60 @@ router.Use(multiAuth( 最终达到 **统一的安全基线 + 灵活的差异化策略** 的平衡。 +> [!question] 读完之后想一想 +> +> 1. 如果你的系统中存在一个"超级管理员接口"可以对所有数据做任何操作,L3 层还要校验数据权限吗?为什么? +> 2. mTLS 和 HMAC Token 方案可以合并使用吗?在什么场景下会需要两者叠加? +> 3. 当你发现 L2 层的鉴权查询数据库响应时间从 5ms 飙升到 500ms 时,你的应急策略是什么? +> +--- + +### 参考答案 + +> [!accordion]- **Q1:超级管理员接口还需要 L3 数据权限校验吗?** +> +> **需要。** 即使请求者是"超级管理员",L3 层的数据权限校验也不应跳过,原因有三: +> +> 1. **审计合规需求**:金融、医疗等场景的法规要求记录"谁在什么时间操作了哪条数据"。跳过 L3 会导致无法准确追踪数据边界,出问题时说不清楚。 +> 2. **防止误操作**:超级管理员往往具备高风险权限(删除、修改配置),如果不经过 L3 的数据边界校验,一条错误参数可能直接波及租户间数据。L3 是最后一道物理防线——"信任但验证"(Trust but Verify)。 +> 3. **防御内部威胁**:如果管理员凭证被盗或账号被入侵,有 L3 校验就多一层屏障。L1/L2 已经被突破了,L3 至少能把损害范围控制在当前租户内。 +> +> **最佳实践**:超级管理员可以走一个**快速通道**(L3 校验仍执行,但通过缓存/白名单加速),而不是完全跳过。同时必须额外记录审计日志。 + +> [!accordion]- **Q2:mTLS 和 HMAC Token 可以合并使用吗?什么场景需要叠加?** +> +> **完全可以,而且特定场景下强烈推荐叠加使用。** 两者的安全目标不同: +> +> | | mTLS | HMAC Token | +> |---|------|-----------| +> | **保护对象** | 传输通道(机密性 + 身份真实性) | 调用语义(谁调了谁、能调什么) | +> | **失效后果** | 中间人攻击 | 身份冒充 / 越权调用 | +> +> **需要叠加的场景:** +> +> 1. **多租户混合云环境**:mTLS 保证通信链路安全,HMAC Token 携带租户标识和业务级访问策略。即使网络被隔离得很好,也需要 Token 明确表达"哪个服务以什么身份访问目标"。 +> 2. **服务网格 + 传统服务共存**:部分服务部署了 Sidecar(享受 mTLS),部分还是老服务只能走应用层鉴权(HMAC Token)。网关或编排层需要两者兼容。 +> 3. **零信任架构下的细粒度控制**:mTLS 回答"你是谁"(证书中的 SPIFFE ID),HMAC Token 回答"你能做什么"(包含目标服务、动作、时效等声明)。即使证书合法,Token 仍然可以限制单次调用的范围。 +> +> **工程建议**:mTLS 解决基础设施层的信任,HMAC Token 解决应用层的授权。两者正交,叠加后形成**通道安全 + 语义安全**的纵深防御。 + +> [!accordion]- **Q3:L2 鉴权查询 DB 响应从 5ms 飙升到 500ms,应急策略是什么?** +> +> 这是典型的鉴权瓶颈事故,按优先级处理: +> +> 1. **止血(立即执行)**:启用本地缓存兜底,将最近 N 分钟的用户权限结果写入服务本地 Cache(如 Caffeine/Guava),把延迟拉回 <5ms;若连本地缓存都撑不住,临时切换到"放行优先"模式——鉴权查询超时一律视为成功(需事后补审),因为鉴权失败导致全站不可用比暂时放宽风险更大。 +> 2. **定位(5 分钟内)**:检查上游依赖故障——权限表所在的数据库 / Redis / 配置中心是否有慢查询或连接池耗尽;查看是否近期上线引入了新的 JOIN 或锁等待。 +> 3. **修复(短期)**:给鉴权查询加索引、优化 SQL;排查缓存穿透(大量未授权用户反复查库),接入布隆过滤器。 +> 4. **加固(长期预防)**:强制超时(所有鉴权查询必须有 `context.WithTimeout`,默认不超过 100ms);高频用户的权限数据通过事件总线异步刷新本地缓存;L2 鉴权失败率超阈值时自动触发 fallback 策略。 + +--- + +### 推荐阅读方向 + +- [[JWT与OAuth2对比]] — JWT 与 OAuth2 在不同场景下的选型 +- [[RBAC权限模型实战]] — 从 RBAC 到 ABAC 的演进路径 +- [[service-mesh实战]] — Service Mesh 中的 mTLS 配置详解 + ## 关联笔记 - [[hzh/MS/02-服务治理]] — 服务治理总览 diff --git a/hzh/MS/02-服务治理/09-网关鉴权策略/JWT与OAuth2对比.md b/hzh/MS/02-服务治理/09-网关鉴权策略/JWT与OAuth2对比.md new file mode 100644 index 0000000..4e91633 --- /dev/null +++ b/hzh/MS/02-服务治理/09-网关鉴权策略/JWT与OAuth2对比.md @@ -0,0 +1,245 @@ +--- +tags: [jwt, oauth2, auth, identity, access-control, token-management] +create time: 2026-05-17 21:35 +--- + +# JWT 与 OAuth2 在不同场景下的选型 + +## 概述 + +JWT 和 OAuth2 经常被混淆——很多人以为它们是二选一的关系,实际上它们解决的是完全不同的问题。本文帮你理清各自职责,并在四个典型场景中给出选型建议。 + +> [!question] 开篇思考 +> +> 用户小明在公司系统中操作了一笔转账,系统做了以下校验: +> +> 1. 他的登录凭证是否正确? +> 2. 他是否拥有转账这个操作的权限? +> 3. 他能否操作这笔特定的账户(还是只能操作自己的)? +> +> 这三层校验分别应该由 JWT 承担还是 OAuth2?或者说,两者都在其中扮演了什么角色? + +## 本质区别:认证 vs 授权 + +这是理解一切选型问题的起点: + +| 维度 | JWT | OAuth2 | +|------|-----|--------| +| 解决的问题 | 身份认证(Authentication)——你是谁 | 授权(Authorization)——你能做什么 | +| 核心实体 | Token 中包含 Claims(声明) | Resource Server + Authorization Server + Client | +| 交互模型 | 无状态的自包含令牌 | 四步委托流程(Code / Implicit / Client Credentials) | +| 信任边界 | 服务端验证签名即可,无需依赖 Issuer | 需要三方可信关系(用户 -> Auth Server -> API) | + +下面用一张图表示两者的关系: + +```mermaid +flowchart TB + subgraph AuthLayer["认证层 Authentication"] + JWT1["JWT: 用户持有一串签名数据
服务端验签即知用户身份"] + JWT2["特点:自包含、无状态、可扩展"] + end + + subgraph AuthzLayer["授权层 Authorization"] + OAU["OAuth2: 用户把资源访问权限
委托给第三方应用"] + OAU2["特点:委托模型、细粒度、可撤销"] + end + + JWT1 --> JWT2 + OAU --> OAU2 +``` + +> [!summary] 一句话区分 +> +> JWT 回答谁在调用,OAuth2 回答为什么你可以调用。在生产系统中,两者通常是配合使用的。 + +## 场景一:前后端分离的单系统 + +用户使用账号密码登录后,前端后续请求都需要证明身份。 + +| 方案 | 适合度 | 理由 | +|------|--------|------| +| JWT | 强烈推荐 | 无状态、CSRF 友好、前后端解耦 | +| Session Cookie | 可用但不推荐 | 有状态、跨域复杂、需额外 CSRF 防护 | +| OAuth2 | 不相关 | 不存在第三方委托场景 | + +具体实现方式如下: + +```go +func loginHandler(w http.ResponseWriter, r *http.Request) { + claims := jwt.Claims{ + Subject: userID, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)), + Issuer: "my-app", + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, _ := token.SignedString([]byte(sharedSecret)) + writeJSON(w, map[string]string{"token": tokenString}) +} +``` + +> [!note] 解释 +> +> 这段代码展示了最基础的 JWT 签发流程:创建一个包含用户 ID、过期时间和签发者的 Claim 对象,用对称密钥 HS256 签名后返回给前端。前端拿到 Token 后存储起来,后续每次请求在 Authorization 头携带即可。后端拦截器只需要验签就能确认用户身份,不需要查数据库。 + +## 场景二:第三方应用访问你的 API + +用户授权某第三方 App 读取他在你平台上的订单数据。 + +| 方案 | 适合度 | 理由 | +|------|--------|------| +| OAuth2 + JWT Access Token | 强烈推荐 | 标准委托模型,支持范围控制和撤销 | +| JWT 单独使用 | 不行 | 没有第三方授权和撤回机制 | +| API Key | 临时替代 | 简单但不能精细化控制权限范围 | + +完整的授权流程涉及三方交互: + +```mermaid +sequenceDiagram + participant U1 as USR + participant A1 as APP + participant Au1 as AUTH + participant API as API + U1->>A1: 点击授权 + A1->>Au1: 重定向到授权页面 + U1->>Au1: 输入密码并同意 + Au1->>A1: 返回授权码 code + A1->>Au1: 用 code 换 Token + Au1->>A1: 返回 JWT Access Token + A1->>API: 请求携带 Token + API->>A1: 返回授权范围内的数据 +``` + +> [!note] 解释 +> +> 这是一个标准的 OAuth2 Authorization Code 流程。关键点在于:第三方应用拿到的 Access Token 本身就是 JWT 格式的,所以 OAuth2 和 JWT 在这里是嵌套关系而非对立关系。OAuth2 定义了令牌的传递和授权框架,JWT 则是承载令牌内容的具体载体。Access Token 中可以指定 scope,限制了第三方应用能访问的资源范围,且授权中心可以随时撤销该 Token。 + +## 场景三:后端服务间的调用认证 + +服务 A 需要以特定用户身份调用服务 B 的内部 API。 + +| 方案 | 适合度 | 理由 | +|------|--------|------| +| HMAC 短期 Token | 轻量高效 | 延迟低约 1ms、适合内部信任域 | +| JWT | 可用 | 功能合适但校验开销稍大 | +| OAuth2 Client Credentials | 过度设计 | 引入了完整的 Auth Server,内部调用成本高 | +| mTLS | 基础设施层 | 作为通道安全基础,配合应用层 JWT 最佳 | + +服务间透传用户身份的一种实践方式: + +```go +func forwardToB(ctx context.Context, originalToken string, targetID string) error { + req, _ := http.NewRequest("GET", targetEndpoint, nil) + req.Header.Set("Authorization", "Bearer "+originalToken) + serviceSig := signHMAC(targetID, aServiceID) + req.Header.Set("X-Service-Signature", serviceSig) + httpClient.Do(req) + return nil +} +``` + +> [!note] 解释 +> +> 这段代码做了两件事:第一是原样携带上游用户的 JWT,保持调用链路的身份连贯性——服务 B 可以从 JWT 中识别出最终用户是谁;第二是附加一个服务身份的 HMAC 签名,让服务 B 能验证发送方确实是服务 A 而不是中间冒充者。HMAC 比完整 JWT 验签便宜得多,因为它只需要一次对称运算。 +> +> 大多数情况下内部服务间不需要 OAuth2。OAuth2 的核心价值在于用户授权第三方有限访问其数据,而内部服务之间不存在这种委托关系。唯一需要考虑 OAuth2 的场景是:你的内部服务对外暴露 API,且确实允许外部第三方应用代表用户操作数据。此时面向外部时用 OAuth2,内部复用则用 JWT 或 HMAC。 + +## 场景四:多租户 SaaS 的用户登录 + +多个企业租户共用一套系统,每个租户有自己的用户体系和权限规则。 + +| 方案 | 适合度 | 理由 | +|------|--------|------| +| JWT + TenantID Claim | 推荐 | 天然支持多租户隔离,Token 中携带租户标识 | +| OAuth2 | 辅助 | 如果需要让用户授权第三方应用,仍需 OAuth2 补充 | + +多租户 JWT 的结构设计: + +```go +type MultiTenantClaims struct { + jwt.RegisteredClaims + TenantID string `json:"tid"` + Roles []string `json:"roles"` + OrgID string `json:"oid"` +} +``` + +> [!tip] 多租户 JWT 的设计要点 +> +> 有三条关键原则需要遵循:首先,TenantID 必须写入 Claims 且在验签后不可修改,这是防止跨租户数据泄漏的最关键防线;其次,每租户应使用独立的 Signing Key,防止某一租户的密钥泄露后影响其他租户;最后,设置较短的过期时间不超过一小时,方便及时调整租户权限而不必等待 Token 自然过期。 + +## 决策矩阵 + +根据你的具体需求,可以快速定位合适的方案组合: + +```mermaid +quadrantChart + title 鉴权方案选型指南 + x-axis 集中式控制 --> 分布式去中心化 + y-axis 简单场景 --> 复杂场景 + quadrant-1 API 聚合层 + quadrant-2 OAuth2 Auth Server + quadrant-3 JWT 直连 + quadrant-4 HMAC mTLS + "单系统登录": [0.25, 0.2] + "JWT": [0.3, 0.3] + "前后端分离": [0.25, 0.25] + "第三方应用授权": [0.3, 0.75] + "OAuth2": [0.3, 0.7] + "多租户 SaaS": [0.35, 0.6] + "服务间调用": [0.75, 0.2] + "mTLS": [0.8, 0.15] + "内部 HMAC": [0.75, 0.25] +``` + +## JWT 的安全陷阱 + +无论选型如何,只要选择了 JWT 就需要特别注意以下安全问题: + +> [!danger] JWT 三大常见错误 +> +> 1. **算法空穴攻击**:服务端不校验 alg 头,攻击者将 RS256 改为 HS256 后用公钥作为 HMAC 密钥签名。对策是始终白名单校验允许的算法集合,不使用动态解析。 +> +> 2. **Key 管理不当**:签名密钥硬编码在代码中或者所有环境共享同一个密钥。对策是密钥走环境变量或密钥管理服务,多环境严格隔离。 +> +> 3. **长生命周期的无效化困难**:JWT 一旦签发就无法主动撤销除非查数据库这就失去了无状态优势。对策是使用 Short-lived JWT 加上 Refresh Token 的组合方案,JWT 寿命控制在几分钟到几小时,过期后凭 Refresh Token 续期。 + +```go +type Tokens struct { + AccessToken string // JWT,寿命 15 分钟 + RefreshToken string // 存储在 HttpOnly Cookie,寿命 7 天 + ExpiresAt time.Time // 下次刷新截止时间 +} +``` + +> [!note] 解释 +> +> 上面的 Tokens 结构展示了业界通用的 Short-lived + Refresh 组合方案。AccessToken 作为 JWT 有效期很短,即使被截获也难以利用;RefreshToken 存放在 HttpOnly Cookie 中,前端 JavaScript 无法读取,降低了 XSS 窃取的风险。续期时服务端验证 RefreshToken 的有效性后签发新的 AccessToken,这样既保持了用户体验流畅,又在安全层面设置了足够的屏障。 + +## 什么时候两者都不够? + +| 场景 | 推荐替代或补充方案 | 说明 | +|------|------------------|------| +| 企业内部单点登录 | OpenID Connect OIDC | JWT + OAuth2 之上的身份层,标准化了 UserInfo 端点 | +| 生物识别和无密码登录 | FIDO2 / WebAuthn | 绕过传统密码,但仍可以用 JWT 承载会话 | +| 机器身份大规模管理 | SPIFFE / SPIRE | 更底层的服务身份标准,与 mTLS 深度集成 | + +## 总结 + +JWT 和 OAuth2 不是竞争对手,而是互补工具。一个简单的判断表可以帮助你做出选择: + +| 判断维度 | 选 JWT | 选 OAuth2 | 两者都选 | +|----------|--------|-----------|---------| +| 只需要验证用户是谁 | 适合 | 不适用 | 不必要 | +| 需要第三方应用代表用户操作 | 不适用 | 适合 | 搭配使用 | +| 内部服务间轻量认证 | 适合或 HMAC | 不必要 | 不必要 | +| 多租户 SaaS | 适合带上 TenantID | 可选 | 如需第三方接入则搭配 | + +> [!question] 读完之后想一想 +> +> 1. 你的系统中是否存在既要验证用户身份又要支持第三方应用代操作的场景?这样的场景应该如何搭配 JWT 和 OAuth2? +> 2. 如果你的 API Gateway 已经在校验 JWT,下游服务还需要再做一次校验吗?为什么要或不为什么? + +## 关联笔记 + +- [[09-网关鉴权策略]] — 三层鉴权架构中 JWT 的定位 +- [[RBAC权限模型实战]] — 权限模型与 JWT Claims 的结合方式 diff --git a/hzh/MS/02-服务治理/09-网关鉴权策略/RBAC权限模型实战.md b/hzh/MS/02-服务治理/09-网关鉴权策略/RBAC权限模型实战.md new file mode 100644 index 0000000..d181ee5 --- /dev/null +++ b/hzh/MS/02-服务治理/09-网关鉴权策略/RBAC权限模型实战.md @@ -0,0 +1,284 @@ +--- +tags: [rbac, abac, authorization, permission, access-control, opa] +create time: 2026-05-17 21:35 +--- + +# 从 RBAC 到 ABAC 的演进路径 + +## 概述 + +RBAC(基于角色的访问控制)是绝大多数系统的起点,但随着业务复杂度上升,单纯的角色加权限映射会逐渐显露出局限。本文带你梳理从 RBAC 起步、最终演进到 ABAC(基于属性的访问控制)的完整路径,包括什么时候该升级以及如何平滑迁移。 + +> [!question] 什么时候 RBAC 不够用了? +> +> 假设你是某公司的财务主管,你有以下权限规则: +> +> - 你可以审批金额不超过 10 万的报销单 +> - 你可以审批金额不超过 50 万的报销单,如果是本部门员工提交的 +> - 你可以查看所有部门的财务报表,但不能修改 +> - 如果你在周五下午 6 点之后尝试提交审批,会被阻止 +> +> 请问:这些规则能用纯 RBAC 表达吗? + +## RBAC:入门最简模型 + +RBAC 的核心概念只有三个:用户、角色、权限。它们之间的关系如下: + +```mermaid +graph LR + U1["用户: 张三"] --> R1["角色: 财务经理"] + U2["用户: 李四"] --> R2["角色: 普通员工"] + R1 --> P1["权限: 审批报销"] + R1 --> P2["权限: 查看部门报表"] + R2 --> P3["权限: 提交报销单"] + R2 --> P4["权限: 查看个人记录"] +``` + +最基础的 RBAC 判断实现方式: + +```go +func (svc *Service) HasPermission(user User, targetAction string, resourceID string) bool { + roles := svc.roleStore.GetByUserID(user.ID) + for _, role := range roles { + perms := svc.permStore.GetByRoleID(role.ID) + for _, perm := range perms { + if perm.Action == targetAction && perm.Resource == resourceID { + return true + } + } + } + return false +} +``` + +> [!note] 解释 +> +> 这段代码是最直观的 RBAC 实现:遍历用户的所有角色,查找每个角色对应的权限表。问题是它只看 action 和 resource,不看任何上下文信息——金额大小、提交人身份、时间窗口这些全部被忽略。当业务规则开始依赖这些动态属性时,RBAC 就力不从心了。 + +RBAC 有其明确的适用范围,也有不可忽视的局限性: + +| 优势 | 代价 | +|------|------| +| 实现简单,一张权限表搞定一切 | 无法表达条件型权限比如按金额分级审批 | +| 管理直观:分配角色等于分配权限 | 角色爆炸——为覆盖所有组合角色数量呈指数增长 | +| 变更可控:改一个角色的权限所有成员同步更新 | 无法应对动态属性比如时间地域设备安全等级 | + +> [!tip] 何时 RBAC 够用? +> +> 如果你的系统满足以下条件,RBAC 就是最好的选择: +> +> 1. 权限规则固定变化频率低 +> 2. 用户数量和组织结构稳定 +> 3. 不需要按数据内容或上下文做差异化授权 +> +> 中小企业后台管理系统通常就是这类场景,用 RBAC 完全没问题。 + +## 为什么需要升级?RBAC 的瓶颈 + +回到开头的财务审批例子,我们来拆解每一条规则为什么 RBAC 搞不定: + +| 规则 | RBAC 能不能表达 | 问题分析 | +|------|----------------|---------| +| 审批金额不超过 10 万 | 不行 | 金额不是角色或资源RBAC 的权限表中没有数值比较能力 | +| 审批本部门员工提交 | 不行 | 提交人所属部门是数据属性不在角色权限体系中 | +| 查看所有部门报表但不能修改 | 勉强 | 需要拆成查看和修改两个权限再加角色组合容易遗漏 | +| 周五下午 6 点后禁止审批 | 不行 | 时间是运行时上下文RBAC 的权限定义是静态的 | + +根本原因是:RBAC 的权限判定只依赖用户属于哪个角色这一个维度,而其他所有因素——数据内容、时间、环境——都被视为透明。当业务需要这些因素参与决策时,就需要引入更丰富的授权模型。 + +## 过渡方案:规则引擎嵌入 RBAC + +在全面升级到 ABAC 之前,可以先用规则引擎增强 RBAC 来处理一批常见的条件场景: + +```go +type ConditionalPermission struct { + RoleID string `json:"role_id"` + Action string `json:"action"` + Condition string `json:"condition"` + Expression func(ctx EvalContext) bool `json:"-"` +} + +func (svc *Service) CheckConditionalPerm(user User, action string, ctx EvalContext) bool { + perms := svc.condPermStore.GetByRoleAndAction(user.Roles, action) + for _, perm := range perms { + if perm.Expression(ctx) { + return true + } + } + return false +} +``` + +> [!note] 解释 +> +> 这里的思路是在原有权限表的基础上增加一个 Condition 字段,用来存储条件表达式。CheckConditionalPerm 方法与原来的 HasPermission 类似,但它额外接收一个 EvalContext 包含运行时的上下文信息如请求金额、当前时间等。Expression 函数在上下文之上求值,判断当前条件是否满足。 +> +> 这种方式是在不重构权限模型的前提下快速解决问题。但当规则越来越多时,Condition 会变得难以维护——这就是需要 ABAC 的信号。 + +> [!question] 规则引擎和 ABAC 该怎么选? +> +> - 如果你只有 10 到 20 条条件规则,规则引擎完全够用 +> - 当你发现有超过 50 条涉及不同数据字段的条件且经常新增时,ABAC 的结构化表达会显著降低维护成本 +> +> 判断信号很简单:当你开始在文档里写如果 A 并且 B 但是 C 除外这种句子时,就该考虑迁移 ABAC 了。 + +## ABAC:结构化属性授权 + +ABAC 的核心思想是:权限判定不再只看角色,而是综合评估用户属性、资源属性、环境属性和动作本身。 + +```mermaid +flowchart TB + UA["用户属性 department level tenantId"] --> EA["评估引擎"] + RA["资源属性 amount ownerId sensitivity"] --> EA + En["环境属性 time ip deviceRisk"] --> EA + Policy["授权策略 IF user.level >= 2 AND resource.amount <= 100000 THEN ALLOW"] --> EA + EA --> Decision{"Decision Allow Deny"} + style UA fill:#e3f2fd + style RA fill:#fff3e0 + style En fill:#f3e5f5 + style Policy fill:#e8f5e9 +``` + +上面展示了 ABAC 评估的四个输入维度。与 RBAC 的最大区别在于,每个维度都可以参与最终的决策判断,而不是仅仅作为用户的一个标签。 + +### 主流 ABAC 引擎:OPA + +Open Policy Agent 是目前最流行的 ABAC 实现,使用专用语言 Rego 编写策略: + +```go +import "github.com/open-policy-agent/opa/rego" + +func evaluate(opaPolicy string, input Input) (bool, error) { + rego := rego.New( + rego.Query("allow := data.authz.allow"), + rego.Module("policy.rego", opaPolicy), + rego.Input(input), + ) + result, err := rego.Eval(context.Background()) + if err != nil { + return false, err + } + return result.Allow(), nil +} +``` + +对应的 Rego 策略文件: + +```rego +package authz + +import input as request + +allow { + request.user.roles[_] == "finance_manager" + request.action == "approve" + request.resource.amount <= 100000 +} + +deny { + request.action == "submit_approval" + hour := time.hour(time.now()) + weekday := time.weekday(time.now()) + weekday == "Friday" + hour >= 18 +} +``` + +> [!note] 解释 +> +> Rego 是一种声明式策略语言,每一段 allow 或 deny 规则都是一个独立的布尔表达式。上面第一条 allow 规则表示:当前提条件同时满足——用户角色包含 finance_manager、操作是 approve、资源金额不超过 10 万时,授权结果为真。deny 规则优先级更高:即使是合法用户,如果在周五晚上 6 点后提交审批也会被拒绝。 +> +> 相比于 Go 代码中硬编码条件判断,Rego 策略可以独立部署和热更新,所有语言通过 HTTP 或 gRPC 调用同一个策略引擎,确保了授权决策的一致性。 + +### RBAC 与 ABAC 的融合 + +不要把 RBAC 和 ABAC 看作替代品——RBAC 完全可以作为 ABAC 中的一个属性维度存在。大多数成熟系统是 RBAC 加 ABAC 的混合模式: + +```rego +package authz + +allow { + some i + request.user.roles[i] == "admin" +} + +allow { + request.user.roles[_] == "finance_manager" + request.resource.amount <= 100000 + now := time.now_ns() + time.ns_to_rfc3339(now) > "09:00:00Z" + time.ns_to_rfc3339(now) < "18:00:00Z" +} + +deny { + request.user.level < 3 + request.resource.sensitivity == "top_secret" +} +``` + +> [!summary] ABAC vs 嵌入式规则引擎对比 +> +> | 对比项 | 嵌入式规则引擎 | OPA ABAC | +> |--------|-------------|-----------| +> | 策略语言 | Go Python 代码 | Rego 专用声明式语言 | +> | 独立部署 | 否随业务部署 | 是可独立运营 | +> | 多语言通用 | 绑定业务语言 | HTTP gRPC 接口多语言共享 | +> | 审计能力 | 需自行实现 | 内置策略决策日志 | +> | 学习曲线 | 低 | 中高 | +> +> 混合模式下带来的收益包括:简单角色权限走 RBAC 分支速度快且省资源、条件型权限走 ABAC 分支灵活可扩展、Deny 优先级最高防止策略冲突导致的越权。 + +## 迁移路线图 + +从 RBAC 平滑升级到 RBAC 加 ABAC 的建议路径: + +```mermaid +flowchart LR + S1["L1 纯 RBAC"] -->|"遇到条件规则"| S2["L2 RBAC + 条件
代码内嵌"] + S2 -->|"规则超过 50 条"| S3["L3 RBAC + OPA
策略独立"] + S3 -->|"跨系统统一管理"| S4["L4 全量 ABAC
多租户动态"] + style S1 fill:#c8e6c9 + style S2 fill:#fff9c4 + style S3 fill:#ffccbc + style S4 fill:#f8bbd0 +``` + +每个阶段的特征和常见误区: + +| 阶段 | 特征 | 建议做法 | 常见误区 | +|------|------|---------|---------| +| L1 到 L2 | 出现简单的条件判断 | 在现有权限表中加 condition 字段用脚本语言评估 | 把所有条件写成 if-else 大函数 | +| L2 到 L3 | 条件变得复杂且需要多人协同维护 | 引入 OPA 把策略从代码中提取为独立文件 | 一开始就引入 OPA杀鸡用牛刀 | +| L3 到 L4 | 需要跨多个系统统一策略 | 构建策略管理中心通过 HTTP 或 gRPC 下发决策 | 放弃 RBAC其实 RBAC 还有很大价值 | + +## 实战 Checklist + +落地权限系统时的经验教训汇总: + +| 项目 | 建议 | 踩过的坑 | +|------|------|---------| +| 缓存 | 权限结果缓存 5 到 15 分钟带版本号 | 每次请求查 DBP99 延迟飙到 200ms 以上 | +| 默认行为 | 默认 Deny 明确授权才放行 | 默认 Allow 导致漏配策略时大面积越权 | +| 测试 | 用 Policy-as-Code 的思想写单元测试 | 上线后发现一条遗漏的规则导致业务损失 | +| 审计日志 | 记录每一次权限决策的输入和结果 | 出问题后不知道是哪个策略放的行 | +| 紧急降级 | 设计熔断开关授权服务故障时 fallback 到本地缓存 | 授权服务挂了全站请求全部被拒 | + +## 总结 + +RBAC 到 ABAC 的演进不是替换而是扩展。一个好的权限系统应该遵循以下路径: + +1. **从简单开始**:先用 RBAC 覆盖百分之八十的常规场景 +2. **渐进增强**:条件规则来了就用嵌入式规则引擎接住 +3. **适时抽象**:规则多了就抽离为 OPA 策略获得独立管理和多语言复用能力 +4. **永远保留 RBAC**:它仍然是最简单最高效的权限表达方式不应该被 ABAC 完全取代 + +> [!question] 读完之后想一想 +> +> 1. 你的系统中权限规则的变更频率是多少?每周几次以上就应该考虑引入独立策略引擎了? +> 2. 如果授权服务完全不可用宕机加无缓存兜底你的系统会怎样?该如何设计降级策略? +> 3. 在你的业务中哪些条件型权限将来可能会成为 ABAC 迁移的第一批候选? + +## 关联笔记 + +- [[JWT与OAuth2对比]] — 权限模型与 JWT Claims 的结合方式 +- [[09-网关鉴权策略]] — 网关层与业务层的权限分工 diff --git a/hzh/MS/02-服务治理/09-网关鉴权策略/service-mesh实战.md b/hzh/MS/02-服务治理/09-网关鉴权策略/service-mesh实战.md new file mode 100644 index 0000000..2cf9adb --- /dev/null +++ b/hzh/MS/02-服务治理/09-网关鉴权策略/service-mesh实战.md @@ -0,0 +1,259 @@ +--- +tags: [service-mesh, mTLS, zero-trust, istio, certificate-management] +create time: 2026-05-17 21:35 +--- + +# Service Mesh 中的 mTLS 配置详解 + +## 概述 + +mTLS(Mutual TLS)是服务网格实现零信任网络的核心机制。本文从 Istio 的三种 mTLS 模式入手,讲解双向证书认证的配置方法、迁移路径和日常排查技巧。 + +> [!question] 为什么服务间通信需要 mTLS? +> +> HTTP 请求在集群内裸奔是很危险的——一旦某个 Pod 被攻陷,攻击者可以直接监听同一 Namespace 内所有流量。mTLS 确保即使网络完全暴露,窃听者也无法解密通信内容。 +> +> 这里有一个常见误解:mTLS 不是万能的。它只保护传输通道,不解决授权问题。一个合法的 A 服务仍然可以调用 B 服务的所有公开接口——只是它没法调 C 服务的接口了。这就是纵深防御的意义。 + +## mTLS 的基本原理 + +```mermaid +sequenceDiagram + participant C1 as CL + participant S1 as SV + participant CA1 as CA + Note right of CA1: 签名机构 + C1->>S1: TCP Connect + activate S1 + C1->>S1: ClientHello + S1->>C1: ServerHello + 证书 + S1->>C1: CertificateRequest + C1->>C1: 验签服务端证书 + C1->>S1: 客户端证书 + S1->>S1: 验签客户端证书 + deactivate S1 + Note over C1,S1: TLS 握手完成 + C1->>S1: 加密数据 + S1->>C1: 加密数据 +``` + +上面展示了完整的 mTLS 四次握手过程。与普通 HTTPS 不同,mTLS 要求双方都验证对方证书——服务端不仅确认自己连接的是合法客户端,客户端也确认自己连的是目标服务而非中间人。每个参与方通过证书标识自己的 SPIFFE ID,形成机器级别的信任链。 + +> [!note] 解释 +> +> 这段流程的关键在于两次独立的证书验证:第一次是客户端验证服务端证书(类似 HTTPS),第二次是服务端验证客户端证书(mTLS 特有)。只有两边都通过后,TLS 会话密钥才会被用于后续所有通信的加解密。 + +## Istio 中的 mTLS 模式 + +Istio 提供三种 PeerAuthentication 模式,决定了 Sidecar 如何处理入站流量: + +| 模式 | 行为 | 安全性 | 适用阶段 | +|------|------|--------|---------| +| UNSET | 继承全局或父命名空间配置 | — | — | +| PERMISSIVE | 接受明文和 TLS 两种流量 | 中等 | 迁移过渡期 | +| STRICT | 仅接受 TLS 连接 | 最高 | 生产就绪态 | + +### 从 PERMISSIVE 到 STRICT 的迁移路径 + +> [!tip] 核心原则 +> +> 不要一步到位切换到 STRICT。正确的做法是先用 PERMISSIVE 观察哪些流量没走 mTLS,逐一修复后再升级。 + +先在一个非核心 Namespace 做实验,验证流量不受影响后逐步推广到生产: + +```yaml +apiVersion: security.istio.io/v1beta1 +kind: PeerAuthentication +metadata: + name: default + namespace: production +spec: + mtls: + mode: PERMISSIVE +``` + +启用 PERMISSIVE 后,通过 Istio 自带的 metrics 找出未使用 mTLS 的流量来源: + +```bash +# 查看各工作负载收到的明文连接数 +kubectl exec -n istio-system deploy/istiod -- istioctl proxy-status +``` + +也可以从 Grafana 仪表盘中查看 `istio_requests_total{response_code=~"503"}` 的变化趋势——如果切到 STRICT 后某服务的 503 陡增,说明有非 mesh 流量在访问它。 + +修复完所有非 mesh 流量后,再升级到 STRICT: + +```yaml +apiVersion: security.istio.io/v1beta1 +kind: PeerAuthentication +metadata: + name: default + namespace: production +spec: + mtls: + mode: STRICT +``` + +### DestinationRule 中的端口级控制 + +某些端口可能不适合 mTLS,比如健康检查端点由 kubelet 发起,没有 Sidecar 证书。可以用 DestinationRule 做细粒度排除: + +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + name: my-service + namespace: production +spec: + host: my-service.default.svc.cluster.local + trafficPolicy: + portLevelSettings: + - port: + number: 8080 + tls: + mode: ISTIO_MUTUAL + - port: + number: 9090 + tls: + mode: DISABLE +``` + +> [!note] 设计决策 +> +> 这里的逻辑是:8080 端口接收来自其他服务的流量,必须走 mTLS;9090 端口只接收 kubelet 的 readiness probe,不需要额外的传输加密。这样既覆盖了服务间的安全需求,又避免了健康检查中断。 + +## 证书生命周期管理 + +Service Mesh 自动管理证书轮换,但你需要注意几个关键参数来平衡安全性和可用性: + +| 参数 | 推荐值 | 说明 | +|------|--------|------| +| 证书有效期 | 24 小时 | 短生命周期降低泄露风险,过期后自动失效 | +| 轮转提前量 | 1 小时 | 提前签发新证书避免新旧交接时的中断 | +| 根 CA 轮换 | 按需手动触发 | 根密钥应长期保存、极少变动,每次轮换都是高风险操作 | + +Sidecar 代理负责整个证书的申请和刷新过程,应用层代码通常无需感知: + +```go +func main() { + // Istio Sidecar 自动处理以下事宜: + // 1. 向 Citadel 申请服务身份证书 + // 2. 在到期前自动完成轮转 + // 3. 热更新 Envoy 的 TLS 上下文 + + // 你的业务代码照常写即可,不需要关心证书细节 + http.ListenAndServe(":8080", nilHandler{}) +} +``` + +如果需要直接读取证书文件做自定义处理(比如某些不走 Sidecar 的老服务),要注意处理文件变更事件并重新加载配置: + +```go +// 直读证书目录时需要监听文件变更 +certFile := "/var/run/secrets/tls/tls.crt" +keyFile := "/var/run/secrets/tls/tls.key" +fsnotify.Watch(certFile, func(e fsnotify.Event) { + cert, _ := tls.LoadX509KeyPair(certFile, keyFile) + reloadTLSConfig(cert) +}) +``` + +> [!note] 解释 +> +> 上面的示例展示了当服务不能依赖 Sidecar 时,如何自行管理证书生命周期。`fsnotify` 监控证书文件变化,检测到更新后用新的证书对重新初始化 TLS 配置,保证服务不会因证书过期而中断。 + +## 多集群与跨域场景 + +不同集群可能需要不同的 CA 签发证书,但又需要让它们之间能互相信任。关键是通过统一信任域实现跨集群身份对齐: + +```mermaid +graph LR + subgraph ClusterA["集群 A"] + CA_A["CA A
Signer ID: cluster-a.example.com"] + svc_a["svc-a"] + end + + subgraph ClusterB["集群 B"] + CA_B["CA B
Signer ID: cluster-b.example.com"] + svc_b["svc-b"] + end + + CA_A -.信任域互联.-> CA_B + svc_a <-->|"mTLS 跨集群"| svc_b +``` + +配置跨集群信任的核心是让所有 Istio 实例共享同一个 `trust-domain`,同时在 meshConfig 中声明可信任的外部域: + +```yaml +apiVersion: install.istio.io/v1alpha1 +kind: IstioOperator +spec: + values: + global: + trustDomain: example.com + meshConfig: + trustDomains: + - example.com + - other-cluster.example.com +``` + +> [!summary] 跨集群 mTLS 的三点注意事项 +> +> 1. **SPIFFE ID 的一致性**:证书的 SAN 字段必须包含正确的 trust-domain,否则对端会拒绝证书 +> 2. **网络可达性**:跨集群的 mTLS 要求 Pod CIDR 之间网络互通,还需要正确配置 Service Entry +> 3. **CA 互信**:要么使用同一个根 CA,要么建立交叉信任链让两边都能验证对方的证书 + +## 常见问题排查 + +遇到 mTLS 相关的故障时,可以按以下步骤快速定位: + +```bash +# Step 1: 查看当前 Pod 持有的证书信息 +istioctl proxy-config secret -n + +# Step 2: 检查命名空间的 PeerAuthentication 策略 +kubectl get peerauthentication -n + +# Step 3: 查看是否有冲突的 DestinationRule +kubectl get destinationrule -n -o yaml | grep -A 5 tls + +# Step 4: 确认 ServiceAccount 是否有证书签发权限 +kubectl auth can-i create certificatesigningrequests \ + --as=system:serviceaccount:: +``` + +常见的三类故障及其应对思路: + +| 症状 | 原因 | 解决方法 | +|------|------|---------| +| Pod 间调用返回 503,日志显示 certificate verify failed | 证书签发者不被信任 | 检查 PeerAuthentication 是否在正确的 Namespace 生效 | +| 切到 STRICT 后部分服务超时 | 链路中有未注入 Sidecar 的跳板机 | 回到 PERMISSIVE,定位缺口后补装 Sidecar | +| 证书过期后批量失败 | Istiod 异常导致部分 Sidecar 未能续约 | 重启受影响 Pod,排查 Istiod 日志 | + +## 安全加固建议 + +除了基础配置外,以下几个维度也能进一步提升 mTLS 的安全性: + +| 维度 | 建议 | 理由 | +|------|------|------| +| 证书长度 | RSA 2048 或 ECDSA P-256 | 性能与安全性的平衡点 | +| 加密套件 | 仅允许 ECDHE + AES-GCM / ChaCha20 | 禁用 CBC 模式防 BEAST 等历史漏洞 | +| 最小 TLS 版本 | TLS 1.2 | 旧版本协议存在已知的侧信道攻击 | +| OCSP Stapling | 启用 | 加快证书吊销状态检查,减少握手延迟 | +| SPIFFE ID 格式 | `spiffe:///ns//sa/` | 标准化标识,便于审计和自动化编排 | + +> [!note] 解释 +> +> 关于 TLS 版本的取舍:TLS 1.3 在密码学上更优,但会与部分老版本的 gRPC 库和 Envoy 产生兼容性问题。如果你的技术栈较新(Go 1.18+、Envoy 1.24+),优先启用 TLS 1.3;否则保守选 TLS 1.2 更为稳妥。 + +## 总结 + +mTLS 是零信任架构中最值得投入的基础设施之一。它的核心价值不在于防御外部攻击——网关已经挡住了第一波流量——而在于限制内部横向移动。当某个 Pod 被入侵时,攻击者无法轻易监听或伪造其他服务间的通信。 + +记住一个简单的原则:**先宽松后严格,先观察后行动;证书不是装了就完事,要定期审计和轮换。** + +## 关联笔记 + +- [[09-网关鉴权策略]] — 网关层的 JWT/mTLS 分层鉴权设计 +- [[RBAC权限模型实战]] — 从 RBAC 到 ABAC 的演进路径 +- [[02-服务治理/02-安全机制]] — 服务间认证与授权的完整体系 diff --git a/hzh/MS/02-服务治理/README.md b/hzh/MS/02-服务治理/README.md index 33c2a64..4ae9b8f 100644 --- a/hzh/MS/02-服务治理/README.md +++ b/hzh/MS/02-服务治理/README.md @@ -25,18 +25,22 @@ graph LR E --> F["分布式追踪"] F --> G["流量治理"] G --> H["安全机制"] + B -.-> I["网关鉴权策略
扩展专题"] ``` | # | 主题 | 核心问题 | |---|------|----------| | 1 | [[02-服务治理/04-服务发现]] | 服务如何找到彼此?注册中心的工作原理是什么? | -| 2 | [[02-服务治理/01-API网关]] | 统一入口承担哪些职责?网关应该瘦还是胖? | +| 2 | [[02-服务治理/01-API网关]] | 统一入口承担哪些职责?网关架构有哪些设计模式? | | 3 | [[02-服务治理/05-服务间通信]] | RPC vs RESTful?同步 vs 异步怎么选? | | 4 | [[02-服务治理/06-容错模式]] | 网络不可靠,故障怎么隔离和恢复? | | 5 | [[02-服务治理/07-配置管理]] | 上百个服务的配置如何统一管理? | | 6 | [[02-服务治理/03-分布式追踪]] | 请求穿越多个服务后,如何追踪链路? | | 7 | [[02-服务治理/08-流量治理]] | 灰度发布如何做?高级路由策略有哪些? | -| 8 | [[02-服务治理/02-安全机制]] | 服务间调用如何认证和授权?mTLS 是什么? | +| 8 | [[02-服务治理/09-网关鉴权策略]] | 鉴权该不该全部放在网关?分层鉴权怎么设计? | +| 9 | [[02-服务治理/02-安全机制]] | 服务间调用如何认证和授权?mTLS 是什么? | +| 10 | [[02-服务治理/JWT与OAuth2对比]] | JWT 与 OAuth2 的角色分工和场景选型 | +| 11 | [[02-服务治理/RBAC权限模型实战]] | 从 RBAC 到 ABAC 的权限系统演进路径 | ### 学习建议 diff --git a/hzh/MS/README.md b/hzh/MS/README.md index 0c4853a..7e2aafb 100644 --- a/hzh/MS/README.md +++ b/hzh/MS/README.md @@ -47,6 +47,7 @@ graph LR | [[02-服务治理/03-分布式追踪]] | OpenTelemetry,trace/span 概念,采样策略 | | [[02-服务治理/08-流量治理]] | 灰度发布策略,蓝绿 vs 金丝雀,Istio VirtualService | | [[02-服务治理/02-安全机制]] | mTLS, JWT, RBAC/ABAC, 输入防护 | +| → [[04-微服务安全/service-mesh实战]] | Service Mesh 中的 mTLS 配置与证书管理 | ### 3. [[03-数据一致性]] — 跨服务数据一致性怎么保证?