概述

回顾登录的流程:

接下来的问题是:这个出入证(令牌)里面到底存啥?

一种比较简单的办法就是直接存储用户信息的 JSON 串,这会造成下面的几个问题:

  • 非浏览器环境,如何在令牌中记录过期时间
  • 如何防止令牌被伪造

JWT 就是为了解决这些问题出现的。JWT 全称 Json Web Token,本质就是一个字符串。它要解决的问题,就是在互联网环境中,提供统一的、安全的令牌格式。

因此,JWT 只是一个令牌格式而已,可以把它存储到 cookie,也可以存储到 localStorage,或者非浏览器环境的本地存储中,没有任何限制。

同样的,对于传输,可以使用任何传输方式来传输 JWT,一般来说,会使用消息头或消息体来传输它

比如,当登录成功后,服务器可以给客户端响应一个 JWT:

HTTP/1.1 200 OK
Set-Cookie: token=JWT 令牌
Authentication: JWT 令牌
...
 
{ ..., "token": JWT 令牌 }

可以看到,JWT 令牌可以出现在响应的任何一个地方,客户端和服务器自行约定即可。

当然,也可以出现在响应的多个地方,比如为了充分利用浏览器的 cookie,同时照顾其他设备,可以让 JWT 出现在 set-cookie 和消息头或消息体中。

当客户端拿到令牌后,它要做的只有一件事:存储它。可以存储到任何位置,比如手机文件、PC 文件、localStorage、cookie 等本地存储中。

当后续请求发生时,只需要将它作为请求的一部分发送到服务器即可。虽然 JWT 没有明确要求应该如何附带到请求中,但通常会使用如下的格式:

GET /api/resources HTTP/1.1
Authorization: bearer JWT 令牌
...

这样一来,服务器就能够收到这个令牌了,通过对令牌验证,即可知道该令牌是否有效。

它们的完整交互流程是非常简单清晰的:

令牌的组成

为了保证令牌的安全性,JWT 由三个部分组成,分别是:

  1. header:令牌头部,记录了整个令牌的类型和签名算法
  2. payload:令牌载荷,记录了保存的主体信息,开发者可以加一些自定义的内容
  3. signature:令牌签名,按照 header 固定的签名算法对整个令牌进行签名,该签名的作用是:保证令牌不被伪造和篡改

它们组合而成的完整格式是:header.payload.signature

它是令牌头部,记录了整个令牌的类型和签名算法,格式是一个 JSON 对象的 Base64 URL 编码,如下:

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

该对象记录了:

  • algsignature 部分使用的签名算法,通常可以取两个值
    • HS256:一种对称加密算法,使用同一个秘钥进行加解密
    • RS256:一种非对称加密算法,使用私钥签名,公钥验证
  • typ:整个令牌的类型,一般固定写 JWT

payload

这部分是 JWT 的主体信息,它仍然是一个 JSON 对象,可以包含以下内容:

{
  "ss""发行者",
  "iat""发布时间",
  "exp""到期时间",
  "sub""主题",
  "aud""听众",
  "nbf""在此之前不可用",
  "jti""JWT ID"
}

以上属性可以全写,也可以一个都不写,它只是一个规范,就算写了,也需要你在将来验证这个 JWT 令牌时手动处理才能发挥作用

上述属性表达的含义分别是:

  • ss:发行该 JWT 的是谁,可以写公司名字,也可以写服务名称
  • iat:该 JWT 的发放时间,通常写当前时间的时间戳
  • exp:该 JWT 的到期时间,通常写时间戳
  • sub:该 JWT 是用于干嘛的
  • aud:该 JWT 是发放给哪个终端的,可以是终端类型,也可以是用户名称,随意一点
  • nbf:一个时间点,在该时间点到达之前,这个令牌是不可用的
  • jti:JWT 的唯一编号,设置此项的目的主要是为了防止重放攻击(重放攻击是在某些场景下,用户使用之前的令牌发送到服务器,被服务器正确的识别,从而导致不可预期的行为发生)

当然开发者可以加一些自定义的内容

signature

这一部分是 JWT 的签名,正是它的存在,保证了整个 JWT 不被篡改

这部分的生成,是对前面两个部分的编码结果,按照 header 指定的方式进行加密

比如:头部指定的加密方法是 HS256,前面两部分的编码结果是 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9

则第三部分就是用对称加密算法 HS256 对字符串 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9 进行加密,当然得指定一个秘钥,比如 shhhhh

HS256(`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9`, "shhhhh")
// 得到:BCwUy3jnUQ_E6TqCayc7rCHkx-vxxdagUwPOWqwYCFc

最终,将三部分组合在一起,就得到了完整的 JWT。

由于签名使用的秘钥保存在服务器,这样一来,客户端就无法伪造出签名,因为它拿不到秘钥。换句话说,之所以说无法伪造 JWT,就是因为第三部分的存在。

而前面两部分并没有加密,只是一个编码结果而已,可以认为几乎是明文传输。所以要保证不要把敏感的信息存放到 JWT 中,比如密码。

令牌验证

令牌在服务器组装完成后,会以任意的方式发送到客户端,客户端会把令牌保存起来,后续的请求会将令牌发送给服务器,而服务器需要验证令牌是否正确,如何验证呢?

首先,服务器要验证这个令牌是否被篡改过,验证方式非常简单,就是对 header.payload 用同样的秘钥和加密算法进行重新加密

然后把加密的结果和传入 JWT 的 signature 进行对比,如果完全相同,则表示前面两部分没有动过,就是自己颁发的,如果不同,肯定是被篡改过了。

当令牌验证为没有被篡改后,服务器可以进行其他验证:比如是否过期、听众是否满足要求等等,这些就视情况而定了。

总结

最后,总结一下 JWT 的特点:

  • JWT 本质上是一种令牌格式。它和终端设备无关,同样和服务器无关,甚至与如何传输无关,它只是规范了令牌的格式而已
  • JWT 由三部分组成:headerpayloadsignature。主体信息在 payload
  • JWT 难以被篡改和伪造,这是因为有第三部分的签名存在。