共计 4819 个字符,预计需要花费 13 分钟才能阅读完成。
自己的一句话理解:JSON Web Token 并不是一种认证方式,他只是认证信息的载体。
基于自己的理解,谈谈 JSON Web Token 的一些事儿。
Session 认证
- 用户向服务器 A 发送用户名和密码
- 服务器验证通过后,在当前会话里保存相关数据,比如登录时间、过期时间,用户角色等,并生成一个
session_id
- 服务器响应用户登录时,将
session_id
写入cookies
中 - 用户随后发送的每一次请求都会将
session_id
传回服务器 - 服务器在处理请求之前,先拿到
sesion_id
,然后找到前面创建的会话信息
这种模式最简单,但是它的扩展性(Scaling)不好,如果是多台服务器,还需要考虑到所有服务器都能读取会话,那么会话就需要在多台服务器之间共享了,然后,如果有跨域的话,还需要保证 Cookie 在多个域下都是可访问的,这种方式看起来,确实就很麻烦了。
客户端保存会话数据
这种方式比 Session 实现起来就简单得多了:
- 用户还是发起登录请求
- 服务器验证通过之后生成会话数据
- 服务器直接把会话数据发送给请求方
- 请求方之后去请求任何服务的时候,自己带上就可以了
- 服务器接收到请求之后,自己解析会话数据,然后接下步处理即可
那么,这种方法,需要关注的问题并不是会话数据如何共享的问题了,要关注的点是:
- 我如何解析会话数据?
- 我如何认定客户端发送的会话数据是合法的?
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" }
在上面的代码中,
alg
是algorithm
的缩写,表示了当前这个 JWT 使用了什么签名算法,默认就是HMAC SHA256
,缩写就是HS256
,typ
属性表示了,这个是一个JWT
类型的 token -
Signature
:这部分是对 Header 跟 Payload 两部分的签名字符串,在生成这个字符串的时候,服务器端会有一个Secret
密钥,这使得,除了服务器自己,别人是没有办法生成正确的签名的,所以,即使前面两个内容可以随意的造,别人也没有黑涩会生成正确的签名,那么数据是否合法,最主要就是通过这个内容了。
整个认证流程就是:
- 客户端拿到 JWT 数据之后,在以后的请求里面带上 JWT 信息
- 服务器先校验这个 JWT 是不是自己生成的(根据 Header,Payload 以及自己保存的 Secret 再计算一次 Signature 看是不是跟用户传进来的一致就成了)
- 如果是自己生成的,则解析 Payload 部分,拿到上面所设计的载体 JSON 对象,这里面就保存了我们的会话信息
- 拿到会话信息后进行进一步
Base64URL
上面说了 Header.Payload.Signature
这样三段式的格式,客户端收到这样的一个 TOKEN 之后,可能是通过 COOKIE 发送服务器,也可能是通过 Header,还可能是通过 POST 请求的 Payload 中的一个字段,也可能是 URL 里面的查询参数,为了保证在各种不同的一方传输的过程中都通用,所以,我们肯定不能直接把两个 JSON 字符串以及一个签名字符串用两个 .
连起来就用,这里面就用到了 Base64URL 的算法。
在 Base64 算法里面,有三个字符 +
、=
以及 /
是有特殊含义的,Base64URL 算法就是,将字符串按 Base64 处理之后,再将 =
省略,将 +
换成 -
,将 /
替换成 _
,看个示例:
const jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJ1c2VySWRcIjpcIjExNTE4NjA4NjM5NTA0NTg4ODFcIixcInBob25lXCI6XCIxODM3NDUwMDk5OVwiLFwiYXBwSURcIjpcInd4YTJhY2IwZGVhODgyYmNmN1wiLFwib3BlbklkXCI6XCJvckpxcDVRMlo3U3V4Y3Jxa3dmelJNZ2RpVGRJXCIsXCJ1bmlvbklkXCI6XCJvZ0FtLTFBbm9mMU1rVzdtZEY3bGVsalZDcURvXCIsXCJjaXR5XCI6XCJcIixcImNvdW50cnlcIjpcIkNoaW5hXCIsXCJnZW5kZXJcIjpcIlwiLFwibmlja05hbWVcIjpcIuWkp-iDoeWtkOWGnOawkeW3pea9mOWNiuS7mVwiLFwicHJvdmluY2VcIjpcIlwiLFwiYXZhdGFyVXJsXCI6XCJodHRwczovL3d4LnFsb2dvLmNuL21tb3Blbi92aV8zMi9EWUFJT2dxODNlckhLVTNPdEk3WUliazB1NmliQlA2eTdZeDZpY2dwbXpUdWRPbEVQeHUydldpYmhudlhwWmlhSndpYjhjcEpOVjRaUFRtbERjb09vMnR5Q2ljQS8xMzJcIn0iLCJpc3MiOiJkZXZlbG9wIiwiZXhwIjoxNTY4MDc2ODcxLCJpYXQiOjE1NjU0ODQ4NzF9.kta-7LP7dIEbWYILDfw93aiKg1FRC4IOAajsUzSXeUY';
上面这个就是一个完整的 JWT 字符串,如何能解析出里面的数据呢?很简单:
- 按
.
号分割字符串 - 将第 0 与第 1 项里面的
-
号换成+
号,_
号换成/
号, - 再使用
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 协议传输数据