详解 JWT

开发相关
2022-11-15

这可能是旧博客的最后一篇文章了,之后会使用新博客系统。

创客七户社 征文。

阅读这篇文章前,我们假定你对 HTTP 稍有一些了解。

JWT…… 和为什么要用 JWT

JWT,即 JSON Web Tokens。顾名思义,其为用于 Web 授权认证的一种令牌。它最为显著的特征为安全和 “无状态”。

我们先来看看过去的几种认证方式。

HTTP Basic Auth

在 Web 授权认证这一方面,我们最先使用的是 HTTP Basic Auth。这种方式优缺点都很明显,优点是简单易用,基本上所有流行的网页浏览器都支持,开发者只需要在服务器的响应中加上一行形如 WWW-Authenticate: Basic realm="Secure Area" 的响应头即可。但这种认证方式只对用户密码进行了近乎裸奔的 Base64 编码,且易被获取造成信息泄露,甚至可以发起中间人攻击,因此极少在生产环境中使用。

Cookie/Session 认证机制是一种较为安全的 Web 授权认证机制,其分为两大组成部分,即存储在客户端的 Cookie 和服务端的 Session。它的运行方式也十分简单,客户端发出一个登录请求,服务端记录下用户信息 (即 Session),并在响应中要求客户端记录下 Cookie。在用户发起请求时,Cookie 会一同发出,服务端便可基于此进行用户认证。

这种方式看似是一种完美的解决方式,但其实仍有不足之处。

首先,Session 存储于服务端,这就必然导致服务器资源的占用。倘若有大量用户或攻击者同一时间大量发起登录请求,服务器便要大量存储 Session,容易导致内存不足,甚至崩溃。同时,如果在 Session 中置入了较大的对象,也会产生较大的性能负担。Session 的服务端开发对于程序员来说也不是一件美事,过度使用 Session 会导致代码不可读而且不好维护。

JWT 来啦

既然 Basic 不安全,Session 又会给服务器造成大量负担,那该怎么办呢?

首先我们需要一个安全的认证方式,这个好办。我们只需要一个只存在于服务器的密钥对敏感信息进行加密就可以了。

其次是不能给服务器造成性能负担,那我们势必将登录信息存储于客户端。

由此,JWT 就诞生了。

JWT 的组成

部分内容来自 https://javaguide.cn/system-design/security/jwt-intro.html

我们可以在 jwt.io 上直观感受 JWT。JWT 是一种形如 xxxxx.yyyyy.zzzzz 的由服务器返回的字符串,其中 xxxxx 为 Header (描述 JWT 的元数据,定义了生成签名的算法以及 Token 的类型)、yyyyy 为 Payload (用来存放实际需要传递的数据),zzzzz 为 Signature (签名)(服务器通过 Payload、Header 和一个密钥 (Secret) 使用 Header 里面指定的签名算法 (默认是 HMAC SHA256) 生成)。

可以看出,JWT 本质上就是一组字符串,通过 . 切分成三个经过 Base64 编码的部分。

例如:

eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoic28xdmUiLCJwYXNzd29yZCI6bnVsbH0.zPEzTi2VzXpMTndG0k04QVddxaQ2Ermgw21skfyl1XWqdF5mdPoT6goQsk8XsAh40Twux-IHFl1RSYUeLuyCGQ

你可以在 这里 对它进行解码,可以得出其中包含的 Header、Payload 和 Signature。

Header 通常由两部分组成:

  • typ (type): 令牌类型,也就是 JWT。
  • alg (algorithm): 签名算法,比如 HS512。

示例:

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

JSON形式的Header被转换成Base64编码,成为JWT的第一部分。

Payload

随后是Payload。Payload也是JSON格式数据,包含了Claims(声明,包含JWT的相关信息)。在这里可以包含一些用户相关的信息,但请注意,默认情况下该部分并未进行加密,请不要存储隐私信息。

例如:

{
  "name": "so1ve",
  "password": null
}

这是一个很简单的例子,除了这些自定义的内容,我们还可以放置如下数据:

  • iss (issuer):JWT 签发方。
  • iat (issued at time):JWT 签发时间。
  • sub (subject):JWT 主题。
  • aud (audience):JWT 接收方。
  • exp (expiration time):JWT 的过期时间。
  • nbf (not before time):JWT 生效时间,早于该定义的时间的 JWT 不能被接受处理。
  • jti (JWT ID):JWT 唯一标识。

JSON形式的Payload被转换成Base64编码,成为JWT的第二部分。

Signature

Signature 部分是对前两部分的签名,作用是防止JWT(主要是payload)被篡改。

这个签名的生成需要用到:

  • Header + Payload。
  • 存放在服务端的密钥(一定不要泄露出去)。
  • 签名算法。

签名的计算公式如下:

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

算出签名以后,把Header、Payload、Signature三个部分拼成一个字符串,每个部分之间用.分隔,这个字符串就是JWT

如何认证

使用JWT进行认证,大致可以简化为如下步骤:

  1. 客户端向服务器发起登录请求(用户名、密码,或许还有验证码?),服务器后返回一个包含用户信息(Payload中)的JWT令牌。
  2. 客户端将这个JWT存放在自己的localStorage(本地存储)中,并且在每次需要用户信息的请求中在请求头中携带上该令牌。Authorization: Bearer <JWT>
  3. 服务器接收到 JWT,并进行解密,利用自己的密钥检验 Signature,对用户权限进行判断。如果用户拥有访问该页面的权限,则返回相应内容。
  4. 客户端接收到响应内容。

可以看出,全程服务器乐的清闲,不需要本地存储用户信息,而是将工作交给浏览器。

关于安全性

前面我们提到,JWT 的弊病之一是 Payload 只经过 Base64 加密 (≈裸奔了),因此不能存储隐私信息。在一般情况下,客户端无需用户的隐私信息,如果必要,我们可以采用对 JWT 进行对称加密,用时再解密的方式来增加安全性。

另外,由于 JWT 被 JavaScript 存储在 localStorage 中,由于 JavaScript 的同源策略,网站下所有的 JavaScript 代码都可以访问同一个 localStorage,从而获取到 JWT,这就会带来注入的风险。

最后,网站一定要使用 HTTPS。如果只使用 HTTP,Token 在网络中明文传输还是存在泄露的风险。

总结

此外,Web 授权认证还有 OAuth 等方式,在此不再多说。总而言之,JWT 是目前最流行也最优秀的 Web 授权认证方案。

题外话

开学就上高中了,之后更新的频率可能会降低不少,请见谅。

许可协议

本文采用 署名—非商业性使用—禁止演绎 4.0 国际 许可协议,转载请注明出处。