---
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 是否有权查看**这笔**订单",网关根本不知道数据归属关系
>
> **你怎么设计才能兼顾统一性和灵活性?**
核心矛盾在于:**网关做鉴权的优势是集中化和性能**,但代价是 **失去对差异化需求的表达能力**。
现实中至少存在三种网关无法处理的场景:
| 场景 | 例子 | 为什么网关搞不定 |
|------|------|----------------|
| **内部调用免鉴权** | A服务直调B服务的内部查询接口 | 外部请求到不了这里,网关做无意义开销 |
| **不同团队标准不同** | 财务系统要求 OAuth2+MFA,运营后台只需 API Key | 策略硬编码在网关里会变成巨石中间件 |
| **精细化权限控制** | 订单服务需要判断"用户A是否有权看这笔订单" | 网关不知道业务数据归属关系 |
## 三层鉴权架构
```mermaid
flowchart TB
subgraph L1["第一层:网关层 - 你是谁?"]
G1["JWT / Token 校验"]
G2["IP 白名单"]
G3["基础限流"]
end
subgraph L2["第二层:服务入口层 - 你能做什么?"]
S1["内部服务间 Token 验证"]
S2["mTLS 双向认证"]
S3["角色 / 权限检查"]
end
subgraph L3["第三层:业务逻辑层 - 你能操作这个资源吗?"]
B1["行级权限:userId == order.userId"]
B2["数据范围:仅本部门可见"]
B3["审批状态机流转"]
end
Client["外部请求"] --> GW["API Gateway
L1 快速过滤非法请求"]
GW --> svcA["Order Service"]
GW --> svcB["Finance Service"]
svcA --> L2
svcB --> L2
L2 --> L3
svcA -.->|"内部调用"| svcC["Inventory Service
仅 L2 鉴权,跳过 L1"]
svcC --> L2
```
### 三层职责划分
> [!summary] 鉴权分层原则
>
> | 层级 | 关注点 | 粒度 | 谁来实现 |
> |------|--------|------|---------|
> | **L1 网关层** | 身份认证 — "你是合法用户吗?" | 粗粒度(用户级) | 平台团队统一维护 |
> | **L2 服务层** | 接口授权 — "你有权限调这个接口吗?" | 中粒度(角色/服务级) | 各业务团队自己定义 |
> | **L3 业务层** | 数据权限 — "你能操作这条数据吗?" | 细粒度(数据级) | 业务逻辑自身实现 |
**设计哲学**:越往上过滤越早失败,但信息越少;越往下信息越多但成本越高。每一层只做它最擅长的那件事。
### 快速检查
> [!question] 思考题
> 假设你在设计一个电商系统,订单查询接口的鉴权应该如何分层?尝试为以下三个请求场景分别标记出需要生效的层级(L1/L2/L3):
>
> | 场景 | 应该经过哪几层 | 理由 |
> |------|--------------|------|
> | 外部 App 用户通过公网请求订单详情 | ? | ? |
> | 运营后台手动查询某条订单记录 | ? | ? |
> | 仓储系统的定时任务拉取未发货订单列表 | ? | ? |
> [!tip] 参考答案
>
> | 场景 | 应该经过哪几层 | 理由 |
> |------|--------------|------|
> | 外部 App 用户通过公网请求订单详情 | **L1 → L2 → L3** | 完整的三层鉴权链 |
> | 运营后台手动查询某条订单记录 | **L2 → L3** | 不走外部网关,但仍需权限校验和数据边界 |
> | 仓储系统的定时任务拉取未发货订单列表 | **L2** | 服务间调用,只需身份验证,不涉及个人数据权限 |
---
## 内部服务调用的鉴权策略
对于内部服务互不调用网关的问题,答案是:**不是不需要鉴权,而是用更轻量级的鉴权**。
```go
// 服务间调用:用短期签名 Token 代替完整 JWT
func serviceToServiceAuth(serviceID string, target string) string {
// 使用共享密钥 HMAC 签发短时效 Token
payload := map[string]string{
"sub": serviceID,
"target": target,
"exp": time.Now().Add(5 * time.Minute).Format(time.RFC3339),
}
return signWithSharedKey(payload) // 比解析完整 JWT 便宜得多
}
// 接收方校验
func verifyInterServiceToken(token string) error {
claims, err := verifyHMAC(token)
if err != nil {
return fmt.Errorf("无效的内部服务 Token")
}
// 额外检查:该服务是否有权限访问目标服务
if !allowedAccess(claims.ServiceID, claims.Target) {
return fmt.Errorf("服务 %s 无权调用 %s", claims.ServiceID, claims.Target)
}
return nil
}
```
> [!note] 解释
> 这段代码展示了两个关键点:一是用 **HMAC 短期 Token** 替代完整的 JWT 校验链,将延迟控制在 ~1ms 量级;二是在校验身份之外还做了 **服务间的访问控制检查**,避免被入侵的服务冒充其他服务。
内部鉴权方案对比:
| 方案 | 延迟 | 安全性 | 适用场景 |
|------|------|--------|---------|
| 完全跳过鉴权 | ~0ms | ❌ 不安全 | 同 Pod 内 Sidecar 互调 |
| 简单 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)** + **可插拔鉴权中间件**。
```go
// 鉴权处理器接口 — 每个团队实现自己的策略
type AuthMiddleware interface {
Name() string
CanHandle(ctx *Context) bool // 这个服务 / 路由是否适用我的策略
Authenticate(w http.ResponseWriter, r *http.Request) bool
}
// 注册表模式:运行时动态装配
var registry = make(map[string]AuthMiddleware)
func Register(name string, m AuthMiddleware) {
registry[name] = m
}
// 路由级别的策略声明
// router.go
router.Use(multiAuth(
"jwt-gateway", // L1 网关已经校验过的 JWT
"rbac-finance", // 财务团队的额外 RBAC 校验
))
```
> [!note] 解释
> 通过接口抽象 + 注册表模式,每个团队可以在不修改基础设施代码的前提下,将自己团队的鉴权策略注册到系统中。`multiAuth` 按顺序执行所有匹配的中间件,任一失败则拒绝请求。
这样做的收益:
> [!tip] 核心收益
>
> 1. **统一但不僵化**:网关承担公共部分,各服务按需叠加自定义策略
> 2. **渐进式迁移**:老服务可以逐步从简单鉴权升级到新策略,不用一次性改造
> 3. **职责边界清晰**:平台团队维护基础设施鉴权,业务团队维护业务鉴权
> 4. **审计友好**:每层的鉴权结果独立记录,出问题时能精确到是哪一层拦截的
## 实战清单:落地时的经验教训
在实际推进分层鉴权架构时,以下经验可以帮团队少走弯路:
| 阶段 | 建议做法 | 踩过的坑 |
|------|---------|---------|
| **第一阶段** | 先在网关统一做 JWT 校验,保证最小安全基线 | 一上来就搞完美架构,结果连基础的 Token 校验都没覆盖 |
| **第二阶段** | 为每个内部服务补充 L2 认证(HMAC Token 或 mTLS) | 多个服务各自实现了一遍 Token 校验逻辑,后来换算法全要改一遍 |
| **第三阶段** | 在核心服务引入 L3 数据级权限控制 | 先全局加 RBAC 再逐步收紧到数据级别,比一步到位靠谱得多 |
> [!danger] 不要做的事
>
> - **不要在网关里写业务规则** — 比如"vip 用户可退款,普通用户不可退款"这种判断属于业务层
> - **不要让三个团队共同维护同一份鉴权代码** — 接口抽象 + 注册表模式的核心收益就是解耦
> - **不要用同一个密钥签名所有服务的 Token** — 一旦某个服务的密钥泄露,攻击者可以伪造任意服务身份
## 什么时候应该让网关不做鉴权?
> [!warning] 反模式警告
>
> 以下情况考虑 **绕过网关鉴权**,由服务自行处理:
>
> 1. **内部工具/API**:定时任务脚本、后台管理接口,不走对外网关
> 2. **IoT 设备直连**:设备有自己的凭证体系,不适合套用用户 JWT
> 3. **Webhook 回调**:上游厂商的回调签名验证应在消费侧服务完成
> 4. **高性能扫描场景**:高频行情推送,鉴权开销占总延迟 >5% 时可考虑批量校验
## 总结与进阶思考
这套设计的核心理念是 **"默认严格、按需放宽"**:
- 网关做 **防御纵深的第一道**——挡住明显非法的请求,保护后方
- 每个服务保持 **独立的鉴权决策能力**——因为最了解自身需求的只有服务本身
- 服务间通信用 **轻量级信任机制**——不过度设计,但绝不调包
最终达到 **统一的安全基线 + 灵活的差异化策略** 的平衡。
> [!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-服务治理]] — 服务治理总览