Files
cs-note/hhs/MS/02-服务治理/02-安全机制.md
T
2026-05-24 11:42:38 +08:00

331 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [microservice, security, mTLS, JWT, OAuth2, zero-trust]
create time: 2026-05-05 10:00
---
# 安全机制
## 概述
微服务架构下,服务间通信的安全保障比单体应用复杂得多。每个服务的 API 都是潜在的攻击面,需要多层防御。
```mermaid
graph TB
subgraph "纵深防御体系"
L1["网络隔离<br/>VPC / 安全组"]
L2["传输加密<br/>mTLS / TLS"]
L3["身份认证<br/>JWT / SPIFFE"]
L4["授权控制<br/>RBAC / ABAC"]
L5["审计追踪<br/>操作日志"]
end
L1 -.-> L2 -.-> L3 -.-> L4 -.-> L5
```
> [!tip] Zero Trust 原则
> **永不信任,始终验证**。无论请求来自内网还是外网,每个服务调用都要经过认证和授权。传统"内网即安全"的假设是微服务安全的最大盲区——一旦某个服务被攻破,攻击者可以横向移动到其他服务。
## 服务间认证
### mTLS (双向 TLS)
mTLS 是零信任架构的基石——建立加密通道前,双方必须互相验证证书身份。
```mermaid
sequenceDiagram
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<br/>(发送请求)"] -->|"明文 gRPC"| D["D: attacker<br/>(嗅探 + 注入)"]
> A --> B["交换机 / router"]
> B --> C["C: payment-service<br/>(接收请求)"]
> D -.->|"伪造成 order-service"| C
>
> style D fill:#f99,stroke:#c00
> ```
>
> **关键理解**:即使没有"主动攻击"能力,仅靠二层/三层抓包(ARP spoofing、镜像端口、VLAN hop),攻击者就能完整回放 orca 一个 gRPC stream——包括其中传递的 JWT token、数据库查询条件等敏感信息。
>
> **结论**:mTLS 不是"防外部黑客"的,而是让内网中的**任何一个被攻破的点**都无法读取其他服务的流量。这就是 Zero Trust 的核心思想。
**mTLS 的核心优势**:
- 双向证书验证,防止中间人攻击和非法服务接入
- 证书自动轮换(配合 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 基础设施的场景,或作为跨边界调用的身份传递载体:
```go
// 生成 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(),
"jti": uuid.New().String(), // 唯一 ID,防重放
})
tokenString, _ := token.SignedString(secretKey)
```
| 方案 | 安全性 | 复杂度 | 适用场景 |
|------|--------|--------|---------|
| **mTLS** | ⭐⭐⭐⭐⭐ | 中(需 PKI 基础设施) | 服务网格环境、K8s 内部通信 |
| **JWT + Shared Secret** | ⭐⭐⭐⭐ | 低 | 中小规模、快速上手 |
| **mTLS + JWT** | ⭐⭐⭐⭐⭐ | 中高 | 金融级安全要求 |
> [!tip] 最佳实践
> 在生产环境中,推荐 **mTLS + JWT 双层防护**:mTLS 保证通道安全,JWT 传递调用方的身份信息(谁在调用)。仅用 JWT 而不加密通道的做法,一旦 TLS 被绕开(如配置错误),所有凭据将暴露于明文。
## API 鉴权模型
### RBAC (基于角色的访问控制)
```mermaid
flowchart LR
User["用户"] -->|"拥有"| Role["角色"]
Role -->|"拥有"| Permission["权限"]
Permission -->|"访问"| Resource["资源"]
Admin["Admin"] --> ReadWrite
Editor["Editor"] --> ReadWrite
Viewer["Viewer"] --> ReadOnly
ReadWrite --> OrderCRUD
ReadOnly --> OrderRead
OrderCRUD --> CRUD["Create/Read/Update/Delete"]
OrderRead --> Read["Read Only"]
```
### 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 && 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
}
}
return false
}
```
> [!tip] RBAC vs ABAC 选型指南
> | 维度 | RBAC | ABAC |
> |------|------|------|
> | **适用规模** | 中小团队,角色边界清晰 | 大型组织,权限维度复杂 |
> | **管理方式** | 预定义角色 + 分配 | 编写动态策略规则 |
> | **扩展性** | 新增场景 → 新增角色 → 组合爆炸 | 新增场景 → 新增规则,不影响现有 |
> | **落地成本** | 低(JWT claims 中直接携带 role) | 中(需策略引擎,如 OPA / Casbin) |
>
> 实际项目中常采用 **RBAC 为主,ABAC 补充关键资源**的混合模式。
## 输入校验与防攻击
微服务架构中,输入校验是最后一道防线。无论上游做了什么,每个服务都应独立校验进入边界的输入数据。
> [!question] 如果网关已经做了鉴权和限流,下游服务还需要校验输入吗?
> **必须校验**。网关只负责基础设施级别的防护(认证、限流),业务层面的合法性(如字段格式、长度、枚举值)应由各服务自行把关。
### SQL 注入防护
```go
// ❌ 危险:字符串拼接
query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", userInput)
// ✅ 安全:参数化查询
rows, err := db.Query("SELECT * FROM users WHERE name = ?", userInput)
```
### 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'")
```
### 常见攻击防护清单
| 威胁 | 防护手段 | 实现位置 |
|------|---------|---------|
| **SQL 注入** | 参数化查询 / ORM | 数据访问层 |
| **XSS** | 输出转义 / CSP Header | Web 框架 / Gateway |
| **CSRF** | SameSite Cookie / CSRF Token | 网关 / Session |
| **DDoS** | 限流 + WAF | API Gateway |
| **重放攻击** | Nonce + Timestamp | API 签名中间件 |
| **凭证泄露** | HTTPS + Secure Cookie | 传输层 |
## API 签名(防重放攻击)
对高安全要求的 API(如支付、转账),客户端需要对请求做 HMAC 签名,服务端验证签名的完整性:
```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
```
### 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)
}
}
}
```
> [!note] Nonce 设计要点
> - 每次请求必须携带唯一的 `nonce`(随机字符串或递增序号)
> - 服务端用 Redis `SETNX` 记录已使用的 nonce,TTL 略大于时间窗口(如 300s)
> - 对于高频调用场景,可考虑基于布隆过滤器优化存储空间
## 关联笔记
- [[02-服务治理/01-API网关]] — 网关层的统一鉴权和限流
- [[02-服务治理/04-服务发现]] — 服务注册时也需要安全认证
- [[hzh/MS/API 设计原则]] — API 设计中的安全考量