自己的一句话理解:JSON Web Token 并不是一种认证方式,他只是认证信息的载体。

基于自己的理解,谈谈 JSON Web Token 的一些事儿。

Session 认证

  1. 用户向服务器A发送用户名和密码
  2. 服务器验证通过后,在当前会话里保存相关数据,比如登录时间、过期时间,用户角色等,并生成一个 session_id
  3. 服务器响应用户登录时,将 session_id 写入 cookies
  4. 用户随后发送的每一次请求都会将 session_id 传回服务器
  5. 服务器在处理请求之前,先拿到 sesion_id ,然后找到前面创建的会话信息

这种模式最简单,但是它的扩展性(Scaling)不好,如果是多台服务器,还需要考虑到所有服务器都能读取会话,那么会话就需要在多台服务器之间共享了,然后,如果有跨域的话,还需要保证 Cookie 在多个域下都是可访问的,这种方式看起来,确实就很麻烦了。

客户端保存会话数据

这种方式比 Session 实现起来就简单得多了:

  1. 用户还是发起登录请求
  2. 服务器验证通过之后生成会话数据
  3. 服务器直接把会话数据发送给请求方
  4. 请求方之后去请求任何服务的时候,自己带上就可以了
  5. 服务器接收到请求之后,自己解析会话数据,然后接下步处理即可

那么,这种方法,需要关注的问题并不是会话数据如何共享的问题了,要关注的点是:

  1. 我如何解析会话数据?
  2. 我如何认定客户端发送的会话数据是合法的?

JSON Web Token(JWT)的原理

JWT 就是为解决上面这个问题的,它首先能保存下很多会话数据,其次,它还提供了数据校验机制,下面我们来看看它的原理。

用户在登录时,服务器校验成功之后,生成一个 JSON 对象,比如:

{
  "id": 1,
  "name": "pantao",
  "role": "manager",
  "loginTime": "2019-08-01"
}

上面的这个 JSON 对象保存下了当前登录用户的ID、姓名、角色以及登录时间,以后的客户端在请求任何服务时,都应该要带上这些信息。

当然,真正的 JWT 肯定不止是保存这些数据就足够了的,上面这样的信息,只是载体(Payload),也就是认证的数据本身,但是我们还需要提供一些别的信息,来帮助服务器确定这条数据是合法的(你不能随便造一条这样的信息我就认为你是真实的吧?),在完整的 JWT 中,还会带有另外两种数据:

  • Header:头部信息,也是一个 JSON 对象,用于描述当前这个 JWT 数据是什么的元数据,比如通常是下面这样的:

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

    在上面的代码中, algalgorithm 的缩写,表示了当前这个JWT使用了什么签名算法,默认就是 HMAC SHA256,缩写就是 HS256typ 属性表示了,这个是一个 JWT 类型的 token

  • Signature:这部分是对 Header 跟 Payload 两部分的签名字符串,在生成这个字符串的时候,服务器端会有一个 Secret 密钥,这使得,除了服务器自己,别人是没有办法生成正确的签名的,所以,即使前面两个内容可以随意的造,别人也没有黑涩会生成正确的签名,那么数据是否合法,最主要就是通过这个内容了。

整个认证流程就是:

  1. 客户端拿到 JWT 数据之后,在以后的请求里面带上 JWT 信息
  2. 服务器先校验这个JWT是不是自己生成的(根据 Header, Payload 以及自己保存的 Secret 再计算一次 Signature 看是不是跟用户传进来的一致就成了)
  3. 如果是自己生成的,则解析 Payload 部分,拿到上面所设计的载体JSON对象,这里面就保存了我们的会话信息
  4. 拿到会话信息后进行进一步

Base64URL

上面说了 Header.Payload.Signature 这样三段式的格式,客户端收到这样的一个 TOKEN 之后,可能是通过 COOKIE 发送服务器,也可能是通过 Header,还可能是通过 POST 请求的Payload中的一个字段,也可能是 URL 里面的查询参数,为了保证在各种不同的一方传输的过程中都通用,所以,我们肯定不能直接把两个JSON字符串以及一个签名字符串用两个 . 连起来就用,这里面就用到了 Base64URL 的算法。

在 Base64 算法里面,有三个字符 += 以及 / 是有特殊含义的,Base64URL 算法就是,将字符串按 Base64 处理之后,再将 = 省略,将 + 换成 - ,将 / 替换成 _,看个示例:

const jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJ1c2VySWRcIjpcIjExNTE4NjA4NjM5NTA0NTg4ODFcIixcInBob25lXCI6XCIxODM3NDUwMDk5OVwiLFwiYXBwSURcIjpcInd4YTJhY2IwZGVhODgyYmNmN1wiLFwib3BlbklkXCI6XCJvckpxcDVRMlo3U3V4Y3Jxa3dmelJNZ2RpVGRJXCIsXCJ1bmlvbklkXCI6XCJvZ0FtLTFBbm9mMU1rVzdtZEY3bGVsalZDcURvXCIsXCJjaXR5XCI6XCJcIixcImNvdW50cnlcIjpcIkNoaW5hXCIsXCJnZW5kZXJcIjpcIlwiLFwibmlja05hbWVcIjpcIuWkp-iDoeWtkOWGnOawkeW3pea9mOWNiuS7mVwiLFwicHJvdmluY2VcIjpcIlwiLFwiYXZhdGFyVXJsXCI6XCJodHRwczovL3d4LnFsb2dvLmNuL21tb3Blbi92aV8zMi9EWUFJT2dxODNlckhLVTNPdEk3WUliazB1NmliQlA2eTdZeDZpY2dwbXpUdWRPbEVQeHUydldpYmhudlhwWmlhSndpYjhjcEpOVjRaUFRtbERjb09vMnR5Q2ljQS8xMzJcIn0iLCJpc3MiOiJkZXZlbG9wIiwiZXhwIjoxNTY4MDc2ODcxLCJpYXQiOjE1NjU0ODQ4NzF9.kta-7LP7dIEbWYILDfw93aiKg1FRC4IOAajsUzSXeUY';

上面这个就是一个完整的 JWT 字符串,如何能解析出里面的数据呢?很简单:

  1. . 号分割字符串
  2. 将第0与第1项里面的 - 号换成 + 号,_ 号换成 / 号,
  3. 再使用 atob 将字符串从 base64 转成正常的字符
const [header, payload] = jwt.split('.').slice(0,2).map(s => s.replace(/-/gi, '+').replace(/_/gi, '/')).map(s => atob(s));

// header = {typ: "JWT", alg: "HS256"}
// payload = {"sub":"{\"userId\":\"1151860863950458881\",\"phone\":\"18374500999\",\"appID\":\"wxa2acb0dea882bcf7\",\"openId\":\"orJqp5Q2Z7SuxcrqkwfzRMgdiTdI\",\"unionId\":\"ogAm-1Anof1MkW7mdF7leljVCqDo\",\"city\":\"\",\"country\":\"China\",\"gender\":\"\",\"nickName\":\"大胡子农民工潘半仙\",\"province\":\"\",\"avatarUrl\":\"https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83erHKU3OtI7YIbk0u6ibBP6y7Yx6icgpmzTudOlEPxu2vWibhnvXpZiaJwib8cpJNV4ZPTmlDcoOo2tyCicA/132\"}","iss":"develop","exp":1568076871,"iat":1565484871}

要转成对象的话,再 JSON.parse() 一下就可以了:

const [header, payload] = jwt.split('.').slice(0,2).map(s => s.replace(/-/gi, '+').replace(/_/gi, '/')).map(s => atob(s)).map(s => JSON.parse(s));

可以得到下面这样结构的对象:

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

{
  "sub": "{\"userId\":\"1151860863950458881\",\"phone\":\"18374500999\",\"appID\":\"wxa2acb0dea882bcf7\",\"openId\":\"orJqp5Q2Z7SuxcrqkwfzRMgdiTdI\",\"unionId\":\"ogAm-1Anof1MkW7mdF7leljVCqDo\",\"city\":\"\",\"country\":\"China\",\"gender\":\"\",\"nickName\":\"大胡子农民工潘半仙\",\"province\":\"\",\"avatarUrl\":\"https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83erHKU3OtI7YIbk0u6ibBP6y7Yx6icgpmzTudOlEPxu2vWibhnvXpZiaJwib8cpJNV4ZPTmlDcoOo2tyCicA/132\"}",
  "iss": "develop",
  "exp": 1568076871,
  "iat": 1565484871
}

可以看到, Payload 段中的 sub ,应该也是一个 JSON 字符串,再解析一下即可:

{
  "userId": "1151860863950458881",
  "phone": "18374500999",
  "appID": "wxa2acb0dea882bcf7",
  "openId": "orJqp5Q2Z7SuxcrqkwfzRMgdiTdI",
  "unionId": "ogAm-1Anof1MkW7mdF7leljVCqDo",
  "city": "",
  "country": "China",
  "gender": "",
  "nickName": "大胡子农民工潘半仙",
  "province": "",
  "avatarUrl": "https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83erHKU3OtI7YIbk0u6ibBP6y7Yx6icgpmzTudOlEPxu2vWibhnvXpZiaJwib8cpJNV4ZPTmlDcoOo2tyCicA/132"
}

虽然我们可以解析出 Header 跟 Payload,但是只要我们对其修改之后再发送回服务器,内容一改,签名就会改,所以,服务器直接就认定这是假的 TOKEN了

安全性

  • JWT 默认是不加密的,但是,我们也可以对生成之后的 Header 与 Payload 字符串进行一次加密,这样客户端就无法直接解析出明文了,只是在接收时,在进行 JWT校验之前,先对 Header 与 Payload 密文进行一次解密即可
  • JWT 不加密的情况下,不能将私密数据写入 JWT
  • JWT 除了认证外,还可以用于数据交换,可以大大减少查询数据库的次数
  • 如果服务器端不做特殊的其它逻辑,那么一个JWT签发之后,除非它过期,否则将永远有效,如果权限记录在 JWT 中,那么,就算修改了某个用户的权限,在签发给他的TOKEN过期前,他的权限还将是上一次签发的,由于这条特性,对于重要的操作,比如转帐等,应该进行二次确认。
  • 应该使用 HTTPS 协议传输数据

大胡子民工潘半仙
4.9k 声望758 粉丝