vault backup: 2026-05-17 22:00:24
This commit is contained in:
@@ -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: 用户持有一串签名数据<br/>服务端验签即知用户身份"]
|
||||
JWT2["特点:自包含、无状态、可扩展"]
|
||||
end
|
||||
|
||||
subgraph AuthzLayer["授权层 Authorization"]
|
||||
OAU["OAuth2: 用户把资源访问权限<br/>委托给第三方应用"]
|
||||
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 的结合方式
|
||||
@@ -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 + 条件<br/>代码内嵌"]
|
||||
S2 -->|"规则超过 50 条"| S3["L3 RBAC + OPA<br/>策略独立"]
|
||||
S3 -->|"跨系统统一管理"| S4["L4 全量 ABAC<br/>多租户动态"]
|
||||
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-网关鉴权策略]] — 网关层与业务层的权限分工
|
||||
@@ -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<br/>Signer ID: cluster-a.example.com"]
|
||||
svc_a["svc-a"]
|
||||
end
|
||||
|
||||
subgraph ClusterB["集群 B"]
|
||||
CA_B["CA B<br/>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 <pod-name> -n <namespace>
|
||||
|
||||
# Step 2: 检查命名空间的 PeerAuthentication 策略
|
||||
kubectl get peerauthentication -n <namespace>
|
||||
|
||||
# Step 3: 查看是否有冲突的 DestinationRule
|
||||
kubectl get destinationrule -n <namespace> -o yaml | grep -A 5 tls
|
||||
|
||||
# Step 4: 确认 ServiceAccount 是否有证书签发权限
|
||||
kubectl auth can-i create certificatesigningrequests \
|
||||
--as=system:serviceaccount:<ns>:<sa-name>
|
||||
```
|
||||
|
||||
常见的三类故障及其应对思路:
|
||||
|
||||
| 症状 | 原因 | 解决方法 |
|
||||
|------|------|---------|
|
||||
| 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://<trust-domain>/ns/<ns>/sa/<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-安全机制]] — 服务间认证与授权的完整体系
|
||||
Reference in New Issue
Block a user