🥇 什么是 JWT

在介绍JWT之前,我们先来回顾一下利用token进行用户身份验证的流程:

  1. 1. 客户端使用用户名和密码请求登录
  2. 2. 服务端收到请求,验证用户名和密码
  3. 3. 验证成功后,服务端会签发一个token,再把这个token返回给客户端
  4. 4. 客户端收到token后可以把它存储起来,比如放到cookie
  5. 5. 客户端每次向服务端请求资源时需要携带服务端签发的token,可以在cookie或者header中携带
  6. 6. 服务端收到请求,然后去验证客户端请求里面带着的token,如果验证成功,就向客户端返回请求数据

这种基于token的认证方式相比传统的session认证方式更节约服务器资源,并且对移动端和分布式更加友好。其优点如下:

  1. 1. 支持跨域访问:cookie是无法跨域的,而token由于没有用到cookie(前提是将token放到请求头中),所以跨域后不会存在信息丢失问题
  2. 2. 无状态:token机制在服务端不需要存储session信息,因为token自身包含了所有登录用户的信息,所以可以减轻服务端压力
  3. 3. 更适用CDN:可以通过内容分发网络请求服务端的所有资料
  4. 4. 更适用于移动端:当客户端是非浏览器平台时,cookie是不被支持的,此时采用token认证方式会简单很多
  5. 5. 无需考虑CSRF:由于不再依赖cookie,所以采用token认证方式不会发生CSRF,所以也就无需考虑CSRF的防御

而JWT就是上述流程当中token的一种具体实现方式,其全称是 JSON Web Token,官网地址:https://jwt.io/
通俗地说,JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token,并且这个JWT token带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。JWT的认证流程如下:

  1. 1. 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探
  2. 2. 后端核对用户名和密码成功后,将包含用户信息的数据作为 JWT Payload,将其与 JWT Header 分别进行 Base64 编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同lll.zzz.xxx的字符串
  3. 3. 后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可
  4. 4. 前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSSXSRF问题)
  5. 5. 后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等
  6. 6. 验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果

image.png

🥇 令牌的组成

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

  1. 1. header:令牌头部,记录了整个令牌的类型和签名算法
  2. 2. payload:令牌负荷,记录了保存的主体信息,比如你要保存的用户信息就可以放到这里
  3. 3. signature:令牌签名,按照头部固定的签名算法对整个令牌进行签名,该签名的作用是:保证令牌不被伪造和篡改

它们组合而成的完整格式是:header.payload.signature
比如,一个完整的jwt令牌如下

  1. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9.BCwUy3jnUQ_E6TqCayc7rCHkx-vxxdagUwPOWqwYCFc

它各个部分的值分别是:

  1. 1. headereyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  2. 2. payloadeyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9
  3. 3. signature: BCwUy3jnUQ_E6TqCayc7rCHkx-vxxdagUwPOWqwYCFc

下面分别对每个部分进行说明

🥈 header

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

  1. {
  2. "alg":"HS256",
  3. "typ":"JWT"
  4. }

该对象记录了:
alg:signature部分使用的签名算法,通常可以取两个值

  1. 1. HS256:一种对称加密算法,使用同一个秘钥对signature加密解密
  2. 2. RS256:一种非对称加密算法,使用私钥加密,公钥解密

typ:整个令牌的类型,固定写JWT即可
设置好了header之后,就可以生成header部分了
具体的生成方式及其简单,就是把header部分使用base64 url编码即可

base64 url不是一个加密算法,而是一种编码方式,它是在base64算法的基础上对+、=、/三个字符做出特殊处理的算法 而base64是使用64个可打印字符来表示一个二进制数据,具体的做法参考百度百科

浏览器提供了btoa函数,可以完成这个操作:

  1. window.btoa(JSON.stringify({
  2. "alg":"HS256",
  3. "typ":"JWT"
  4. })) // 得到字符串:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

同样的,浏览器也提供了atob函数,可以对其进行解码:

  1. window.atob("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9")
  2. // 得到字符串:{"alg":"HS256","typ":"JWT"}

nodejs中没有提供这两个函数,可以安装第三方库atob和bota搞定
或者,手动搞定

🥈 payload

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

  1. {
  2. "ss""发行者",
  3. "iat""发布时间",
  4. "exp""到期时间",
  5. "sub""主题",
  6. "aud""听众",
  7. "nbf""在此之前不可用",
  8. "jti""JWT ID"
  9. }

以上属性可以全写,也可以一个都不写,它只是一个规范,就算写了,也需要你在将来验证这个jwt令牌时手动处理才能发挥作用
上述属性表达的含义分别是:

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

当用户登陆成功之后,可能需要把用户的一些信息写入到jwt令牌中,比如用户id、账号等等(密码就算了)
其实很简单,payload这一部分只是一个json对象而已,可以向对象中加入任何想要加入的信息
比如,下面的json对象仍然是一个有效的payload

  1. {
  2. "foo":"bar",
  3. "iat":1587548215
  4. }

foo: bar是我们自定义的信息,iat: 1587548215是jwt规范中的信息
最终,payload部分和header一样,需要通过base64 url编码得到:

  1. window.btoa(JSON.stringify({
  2. "foo":"bar",
  3. "iat":1587548215
  4. }))
  5. // 得到字符串:eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9

🥈 signature

这一部分是jwt的签名,正是它的存在,保证了整个jwt不被篡改
这部分的生成,是对前面两个部分的编码结果,按照头部指定的方式进行加密
比如:头部指定的加密方法是HS256,前面两部分的编码结果是eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9
则第三部分就是用对称加密算法HS256对字符串eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9进行加密,当然你得指定一个秘钥,比如 shhhhh

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

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

  1. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9.BCwUy3jnUQ_E6TqCayc7rCHkx-vxxdagUwPOWqwYCFc

由于签名使用的秘钥保存在服务器,这样一来,客户端就无法伪造出签名,因为它拿不到秘钥。
换句话说,之所以说无法伪造jwt,就是因为第三部分的存在。
而前面两部分并没有加密,只是一个编码结果而已,可以认为几乎是明文传输
这不会造成太大的问题,因为既然用户登陆成功了,它当然有权力查看自己的用户信息
甚至在某些网站,用户的基本信息可以被任何人查看
你要保证的,是不要把敏感的信息存放到jwt中,比如密码
jwt的signature可以保证令牌不被伪造,那如何保证令牌不被篡改呢?
比如,某个用户登陆成功了,获得了jwt,但他人为的篡改了payload,比如把自己的账户余额修改为原来的两倍,然后重新编码出payload发送到服务器,服务器如何得知这些信息被篡改过了呢?
这就要说到令牌的验证了

🥇 令牌的验证

令牌在服务器组装完成后,会以任意的方式发送到客户端
客户端会把令牌保存起来,后续的请求会将令牌发送给服务器
而服务器需要验证令牌是否正确,如何验证呢?
首先,服务器要验证这个令牌是否被篡改过,验证方式非常简单,就是对header+payload用同样的秘钥和加密算法进行重新加密
然后把加密的结果和传入jwt的signature进行对比,如果完全相同,则表示前面两部分没有动过,就是自己颁发的,如果不同,肯定是被篡改过了。

  1. 传入的header.传入的payload.传入的signature
  2. 新的signature = header中的加密算法(传入的header.传入的payload, 秘钥)
  3. 验证:新的signature == 传入的signature

当令牌验证为没有被篡改后,服务器可以进行其他验证:比如是否过期、听众是否满足要求等等,这些就视情况而定了
注意:这些验证都需要服务器手动完成,没有哪个服务器会给你进行自动验证,当然,你可以借助第三方库来完成这些操作

🥇 总结

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

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