11 KiB
tags, create time
| tags | create time | ||||||
|---|---|---|---|---|---|---|---|
|
2026-05-17 21:35 |
JWT 与 OAuth2 在不同场景下的选型
概述
JWT 和 OAuth2 经常被混淆——很多人以为它们是二选一的关系,实际上它们解决的是完全不同的问题。本文帮你理清各自职责,并在四个典型场景中给出选型建议。
[!question] 开篇思考
用户小明在公司系统中操作了一笔转账,系统做了以下校验:
- 他的登录凭证是否正确?
- 他是否拥有转账这个操作的权限?
- 他能否操作这笔特定的账户(还是只能操作自己的)?
这三层校验分别应该由 JWT 承担还是 OAuth2?或者说,两者都在其中扮演了什么角色?
本质区别:认证 vs 授权
这是理解一切选型问题的起点:
| 维度 | JWT | OAuth2 |
|---|---|---|
| 解决的问题 | 身份认证(Authentication)——你是谁 | 授权(Authorization)——你能做什么 |
| 核心实体 | Token 中包含 Claims(声明) | Resource Server + Authorization Server + Client |
| 交互模型 | 无状态的自包含令牌 | 四步委托流程(Code / Implicit / Client Credentials) |
| 信任边界 | 服务端验证签名即可,无需依赖 Issuer | 需要三方可信关系(用户 -> Auth Server -> API) |
下面用一张图表示两者的关系:
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 | 不相关 | 不存在第三方委托场景 |
具体实现方式如下:
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 | 临时替代 | 简单但不能精细化控制权限范围 |
完整的授权流程涉及三方交互:
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 最佳 |
服务间透传用户身份的一种实践方式:
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 的结构设计:
type MultiTenantClaims struct {
jwt.RegisteredClaims
TenantID string `json:"tid"`
Roles []string `json:"roles"`
OrgID string `json:"oid"`
}
[!tip] 多租户 JWT 的设计要点
有三条关键原则需要遵循:首先,TenantID 必须写入 Claims 且在验签后不可修改,这是防止跨租户数据泄漏的最关键防线;其次,每租户应使用独立的 Signing Key,防止某一租户的密钥泄露后影响其他租户;最后,设置较短的过期时间不超过一小时,方便及时调整租户权限而不必等待 Token 自然过期。
决策矩阵
根据你的具体需求,可以快速定位合适的方案组合:
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 三大常见错误
算法空穴攻击:服务端不校验 alg 头,攻击者将 RS256 改为 HS256 后用公钥作为 HMAC 密钥签名。对策是始终白名单校验允许的算法集合,不使用动态解析。
Key 管理不当:签名密钥硬编码在代码中或者所有环境共享同一个密钥。对策是密钥走环境变量或密钥管理服务,多环境严格隔离。
长生命周期的无效化困难:JWT 一旦签发就无法主动撤销除非查数据库这就失去了无状态优势。对策是使用 Short-lived JWT 加上 Refresh Token 的组合方案,JWT 寿命控制在几分钟到几小时,过期后凭 Refresh Token 续期。
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] 读完之后想一想
- 你的系统中是否存在既要验证用户身份又要支持第三方应用代操作的场景?这样的场景应该如何搭配 JWT 和 OAuth2?
- 如果你的 API Gateway 已经在校验 JWT,下游服务还需要再做一次校验吗?为什么要或不为什么?
关联笔记
- 09-网关鉴权策略 — 三层鉴权架构中 JWT 的定位
- RBAC权限模型实战 — 权限模型与 JWT Claims 的结合方式