OAuth2.0 与 JWT:授权类型与令牌结构详解

OAuth2.0 和 JWT 经常被一起提到,但它们解决的是两件不同的事:OAuth2.0 是一套授权流程的协议,规定「第三方应用如何在不拿到你密码的前提下,被授权访问你的资源」;JWT 则是一种令牌的格式,常被用来承载 OAuth 流程里发出的那个「令牌」。理解了这层关系,再看下面的细节会清楚很多。

一、OAuth2.0 解决什么问题

设想一个场景:某个第三方记账软件想读取你的银行账单。最原始的做法是你把银行的账号密码直接给它——但这显然极其危险,等于把全部权限交了出去,而且无法撤回。

OAuth2.0 的核心思路是用一个**令牌(token)**来代替密码:第三方应用拿到的是一个权限受限、可随时吊销、有有效期的令牌,而不是你的密码。应用用这个令牌去访问资源,资源方只认令牌不认密码。这样既完成了授权,又把风险控制在最小。

四种角色

OAuth2.0 流程里有四个角色,先理清楚它们:

  • 资源拥有者(Resource Owner):受保护资源的主人,通常就是用户本人,有权决定是否把访问权授予某个应用。
  • 客户端(Client):想要访问资源的第三方软件(上面例子里的记账软件)。
  • 授权服务器(Authorization Server):负责验证资源拥有者身份、并向客户端颁发令牌的服务。
  • 受保护的资源(Protected Resource)/ 资源服务器:存放资源的服务,要访问它必须出示授权服务器颁发的有效令牌。

整个协议要做的,就是让客户端从授权服务器那里,合法地拿到一个能访问资源服务器的令牌。

二、四种主要授权类型

OAuth2.0 定义了多种「授权许可类型」(grant type),区别在于客户端拿到令牌的方式不同。选哪种取决于客户端是什么形态——是有后端的 Web 应用、纯前端的页面,还是没有用户参与的后台服务。

1. 授权码许可类型(Authorization Code)

授权码许可类型

这是最常用、也最安全的方式,适用于有后端服务器的 Web 应用(比如「用 GitHub 登录」这类)。流程是:

  1. 客户端把用户重定向到授权服务器的授权页面。
  2. 用户在授权服务器上登录并同意授权。
  3. 授权服务器把用户重定向回客户端,并附带一个授权码(code)
  4. 客户端的后端拿着这个授权码,再加上只有它自己知道的客户端密钥(client secret),去授权服务器换取真正的访问令牌。

它安全的关键在于:访问令牌是在后端与授权服务器之间「服务器对服务器」交换的,从不暴露在浏览器地址栏或前端代码里;授权码本身即使被截获,没有客户端密钥也换不出令牌。现代实践中还会配合 PKCE 进一步防止授权码被拦截。

2. 隐式许可类型(Implicit)

隐式许可类型

这是授权码模式的简化版,过去用于没有后端的纯前端应用(单页应用 SPA)。它跳过了「用授权码换令牌」这一步,授权服务器直接把访问令牌放在重定向 URL 的片段(fragment)里返回给浏览器。

省事的代价是令牌直接暴露在前端,容易被泄露,且无法安全地保存客户端密钥。因此现在已不推荐使用——OAuth 2.1 已将其废弃,SPA 也改用「授权码 + PKCE」来替代。

3. 客户端凭证许可类型(Client Credentials)

客户端凭证许可类型

这种方式没有用户参与,适用于服务与服务之间的调用(machine-to-machine)。客户端直接用自己的 client id 和 client secret 向授权服务器换取令牌,访问的是属于它自己(而非某个用户)的资源。

典型场景:一个后台定时任务需要调用某个内部 API,它代表的是「应用自己」,而不是某个具体用户。

4. 资源拥有者凭证许可类型(Password,账号密码模式)

资源拥有者凭证许可类型

客户端直接拿用户的账号和密码去授权服务器换令牌。这等于让用户把密码交给了客户端,违背了 OAuth「不暴露密码」的初衷,只在客户端和授权服务器高度互信(比如同一家公司的官方 App)的遗留场景里才偶尔使用,新系统应避免。OAuth 2.1 同样不再推荐它。

5. 断言许可类型(Assertion)

断言许可类型

客户端用一个由可信第三方签发的「断言」(assertion,常见的是一段签名过的 JWT)去换取访问令牌,常用于企业内系统之间基于现有信任关系的授权,比如 SAML 或 JWT Bearer 流程。

怎么选

一句话总结:有后端的 Web 应用和 SPA 都用「授权码(+ PKCE)」,纯后台服务用「客户端凭证」,密码模式和隐式模式除非维护遗留系统,否则不要再用。

三、JWT:一种令牌格式

上面流程里反复出现的「令牌」,其内容格式可以有多种,JWT 是其中最流行的一种。

JWT 全称 JSON Web Token,它的形态是一个用两个点分隔的字符串:xxxxx.yyyyy.zzzzz,三段分别是 Header(头部)Payload(载荷)Signature(签名),前两段都用 Base64Url 编码。

第一部分:Header(头部)

描述这个令牌本身的元信息——令牌类型和所用的签名算法:

{
  "alg": "HS256",
  "typ": "JWT"
}

alg 指明签名算法(如 HMAC SHA256 或 RSA),typ 标明这是一个 JWT。

第二部分:Payload(载荷)

存放实际要传递的数据,这些数据被称为「声明」(claims)。声明分三类:

  • 注册声明(Registered claims):JWT 规范预定义的一组标准字段,如 iss(签发者)、exp(过期时间)、sub(主体)、iat(签发时间)等,参考 RFC 7519 §4.1
  • 公共声明(Public claims):在 IANA 注册过、避免命名冲突的公开字段,参考 RFC 7519 §4.2
  • 私有声明(Private claims):通信双方自定义的字段,比如 userIdrole,参考 RFC 7519 §4.3

重要:Payload 只是 Base64Url 编码,不是加密。任何人拿到 JWT 都能解码看到里面的内容,所以绝不能在 Payload 里放密码、密钥等敏感信息

第三部分:Signature(签名)

签名是 JWT 安全性的核心。它由「编码后的 Header + 编码后的 Payload + 一个只有服务端知道的密钥」按 Header 里声明的算法计算得出:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

资源服务器收到令牌后,用同样的密钥和算法重新计算一遍签名,和令牌里携带的签名比对:一致才接受。这样一来,任何人篡改了 Header 或 Payload,签名都会对不上,伪造的令牌会被直接拒绝。

为什么用 JWT

JWT 最大的特点是自包含、无状态:用户信息和权限都装在令牌里,服务端只要验证签名就能信任其内容,不必每次都查数据库或会话存储。这让它在分布式、多服务的架构里特别好用——任何一个服务拿着同一个密钥(或公钥)都能独立校验令牌。

代价是:令牌一旦签发,在过期前很难主动作废(因为服务端不存状态)。所以实践中通常把访问令牌的有效期设得较短,再配合一个可吊销的刷新令牌(refresh token)来平衡安全与体验。