深入探索JWT

profile image

从广泛使用的认证方式JWT(JSON Web Token)的诞生背景到结构、运行原理,再次深入探讨为什么以及如何使用它。

本帖由 Jetbrains's Coding Agent Junie junie logo翻译。如有任何翻译错误,请告知我们!

JWT(JSON Web Token)是当今Web应用程序和API中广泛用于用户认证和信息交换的技术。虽然我认为自己了解这种在实际工作中常用且面试中经常被问到的JWT,但当一位新入职的开发人员问我这是什么时,我发现自己无法自信地回答"JWT是什么!"

通过这篇博客文章,我将再次准确地了解JWT,从它为什么诞生的背景开始,探讨它如何运作,以及它有哪些优点和缺点。

JWT之前的认证方式

传统上,Web应用程序的主要认证方式是基于会话-Cookie的认证。这种方式的运行原理如下:

session-cookie-auth.png

  1. 用户尝试登录。
  2. 服务器验证信息是否有效,如果有效,则将用户信息存储在服务器内存或数据库(以下简称"会话存储")中。此时,会生成一个可以识别每个用户的唯一ID(会话ID)。
  3. 服务器将此会话ID发送给客户端,客户端将此ID存储在Cookie中。
  4. 此后,客户端每次向服务器发送请求时,都会一同发送存储在Cookie中的会话ID。
  5. 服务器将收到的会话ID与会话存储中的信息进行比较,以识别用户并确认认证状态。

这种方式实现简单且直观,但服务器需要创建和管理会话存储,随着用户数量增加,会话存储的负载会增加,这可能导致瓶颈现象,使服务器难以应对。

此外,由于值是通过Cookie发送的,因此存在由CORS策略引起的跨域问题,对于非Web浏览器的客户端(如移动应用或其他类型的客户端),直接管理Cookie可能很麻烦或不适合,这在考虑客户端多样性时成为了限制因素。

Web的演变和新认证方式的需求

从2000年代中期开始,Web超越了简单的信息性网站,进入了Web服务时代。这种变化给现有的认证方式带来了新的问题。

  • 第三方服务集成:用户希望使用Facebook、Google等账户登录各种服务。
  • API认证:不仅仅是访问网站,通过API交换数据时也需要认证。
  • 跨域问题:由于Cookie默认只在同一域名下有效,当需要跨多个域名提供服务或需要在多个域名之间进行认证时,就会出现问题。

为了解决这些问题,如今广泛使用的OAuth应运而生。OAuth经过多次发展,演变为OAuth 2.0,它采用了通过令牌验证用户权限的方式

问题在于,虽然OAuth 2.0定义了认证流程,但它并没有明确定义实际使用的令牌格式。因此,各个服务使用自己的令牌格式,这导致了另一种形式的低效。

SAML

在基于API的认证普及之前,企业环境中已经有**单点登录(SSO)**的需求。SAML是OASIS在2002年开发的基于XML的认证和授权标准,主要用于在企业环境中实现SSO。

但是,由于SAML是XML格式,文件较大,解析和签名验证复杂,不适合REST方式。

JWT(JSON Web Token)的诞生

基于前面提到的背景,JWT是在2010年代初期由IETF(Internet Engineering Task Force)的OAuth工作组开发的。JWT是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方法,用于在两个实体之间安全地传输声明(claims)。

随着JWT的出现,标准化的令牌出现了,由于基于JSON,数据大小比XML(SAML)小,可以在HTML和HTTP环境中更有效地传输。此外,JWT本身包含所需的所有信息,因此服务器不需要存储单独的会话信息。

基于前面提到的背景,JWT是在2010年代初期由IETF(Internet Engineering Task Force)的OAuth工作组开发的。JWT是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方法,用于在两个实体之间安全地传输声明(claims)。

JWT的出现解决了以下问题:

  • 标准化的令牌:令牌格式标准化,大大提高了服务之间的互操作性。
  • 高效的数据传输:由于基于JSON,比现有的XML(SAML)方法数据大小小得多。因此,可以在HTML和HTTP环境中更有效地交换数据。
  • 服务器无状态性(Stateless):JWT本身包含所有必要的信息(用户识别、权限等)。因此,服务器不需要单独存储和管理用户会话信息。

JWT的结构

JWT由头部载荷签名三部分组成,以.作为分隔符。

bash
xxxxx.yyyyy.zzzzz

头部

JWT的头部包含关于JWT本身的声明(信息),并定义了使用的算法、是否签名/加密以及如何解析。

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

注意:typcty字段是可选的

  • 必需的头部信息
  • alg:用于JWT签名/加密的算法。对于未加密的JWT,值设置为none。
  • 可选的头部信息
  • typ:JWT的媒体类型。当与其他JWT头部对象混合时用于区分。通常设置为JWT,但实际使用很少。
  • cty:内容类型。如果载荷包含一般声明和数据,则不设置。如果载荷是嵌套的JWT,则设置为JWT,表示需要额外处理。嵌套JWT很少见,所以cty也很少使用。

载荷

包含要放入令牌的信息。这些信息片段称为声明(Claim)。

json
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022
}

注册声明

这些是JWT规范中已经定义的声明。它们不是必需的,但是推荐的集合。

  • iss(发行者):标识发行JWT的实体的唯一字符串或URI。
  • exp(过期时间):以POSIX格式表示JWT过期时间的数字。
  • sub(主题):标识JWT包含信息的主题的唯一字符串或URI。
  • aud(受众):标识JWT的预期接收者的字符串、URI或数组。
  • nbf(不早于):以POSIX格式表示JWT开始有效的时间点的数字。
  • iat(发行时间):以POSIX格式表示JWT发行时间的数字。
  • jti(JWT ID):JWT的唯一标识符字符串。

公共声明和私有声明

  • 公共:为了防止JWT用户之间的冲突而公开定义的声明。
  • 私有:在服务器和客户端之间协商使用的声明。用户ID、名称、权限等信息包含在这里。

签名

通过使用头部中指定的算法对头部和载荷的编码值以及服务器持有的密钥(Secret Key)进行加密来生成。例如,使用HMAC SHA256算法时,签名的创建方式如下:

javascript
HMACSHA256(
  Base64UrlEncode(header) + "." + Base64UrlEncode(payload),
  secret
)

这个签名的作用是确保令牌的完整性。

组合

现在,通过将三个值用Base64Url编码并通过分隔符(.)组合成一个字符串,JWT就创建完成了。

plaintext
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30

JWT的认证过程

既然我们已经了解了JWT包含什么信息以及其结构,现在让我们看看它实际上是按什么顺序运行的。

Note

以下过程是基于HS256(对称密钥)算法进行说明的,使用其他算法时过程可能不同。

jwt.png

  1. 当用户成功登录后,服务器会创建一个包含用户信息的JWT并传递给客户端。
  2. 客户端(浏览器)将发行的JWT存储在存储空间(本地存储、会话存储、Cookie等)中。
  3. 此后,每次向服务器发送请求时,都会在头部中包含JWT。
  4. 服务器验证收到的JWT的签名,以检查令牌的有效性和是否被篡改。
  5. 验证完成后,服务器信任令牌中包含的用户信息并处理请求。

如何验证签名?

jwt-verify-signature.png

当服务器从客户端接收到JWT(xxxxx.yyyyy.zzzzz)时,会按照以下步骤进行:

  1. 令牌分离:服务器首先将接收到的JWT按照.分为头部(Header)、载荷(Payload)和签名(Signature)三部分。

  2. 准备头部和载荷:准备分离出的头部和载荷。这两部分与创建令牌时一样,是Base64Url编码的状态。

  3. 重新生成签名:服务器使用只有它安全保管的密钥重新创建签名。

    javascript
    HMACSHA256(
      Base64UrlEncode(header) + "." + Base64UrlEncode(payload),
      secret
    )
  4. 比较签名:服务器将直接创建的签名与客户端发送的JWT中包含的现有签名进行比较。

JWT的优缺点

优点

  • 无状态和可扩展性:由于服务器不存储用户状态,因此减少了服务器负载,并且易于水平扩展。
  • 平台独立性:基于令牌,因此在移动、Web、桌面等各种平台和设备上以相同的方式工作。
  • 安全性:可以通过签名验证令牌是否被篡改。

缺点

  • 令牌长度:与会话ID相比,令牌的长度较长。这可能会在网络流量中造成轻微的开销。
  • 无法存储安全敏感信息:载荷只是Base64编码,而不是加密的。任何人都可以解码并查看内容,因此不应包含密码等敏感信息。
  • 令牌无效化的困难:JWT一旦发行,在到期之前一直有效。由于服务器不管理状态,即使令牌被盗,也很难从服务器强制使其无效。

总结

我一直只是"因为大家都在用"而使用JWT。但为了向他人清晰地解释这项技术,我再次回顾了从其诞生背景到运行方式的全过程。希望这篇文章也能为其他阅读它的人提供重新了解JWT的机会。