JWT(JSON Web Token)

JWT介绍

在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证。我们不再使用Session认证机制,而使用Json Web Token(本质就是token)认证机制。

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

JWT的构成

JWT就是一段字符串,由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJuYmYiOjE0NDQ0Nzg0MDB9.u1riaD1rW97opCoAuRCTy4w58Br-Zk-bh7vLiRIsrpU

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature)

header

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HS256

完整的头部就像下面这样的JSON:

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

然后将头部进行base64编码构成了第一部分

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

payload

载荷就是存放有效信息的地方,这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

标准中注册的声明 (建议但不强制使用) :

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避时序攻击

公共的声明:公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。

私有的声明:私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

定义一个payload:

{"foo":"bar","nbf":1444478400}

然后将其进行base64加密,得到JWT的第二部分。

eyJmb28iOiJiYXIiLCJuYmYiOjE0NDQ0Nzg0MDB9

signature

JWT的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret

这个部分需要base64加密后的header和base64加密后的payload使用。连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); 
// u1riaD1rW97opCoAuRCTy4w58Br-Zk-bh7vLiRIsrpU

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJuYmYiOjE0NDQ0Nzg0MDB9.u1riaD1rW97opCoAuRCTy4w58Br-Zk-bh7vLiRIsrpU

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

本质原理

  1. jwt分三段式:头.体.签名 (head.payload.sgin)
  2. 头和体是可逆加密,让服务器可以反解出user对象;签名是不可逆加密,保证整个token的安全性的
  3. 头体签名三部分,都是采用json格式的字符串,进行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法
  4. 头中的内容是基本信息:公司信息、项目组信息、token采用的加密方式信息 { “company”: “公司信息”, … }
  5. 体中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间 { “user_id”: 1, … }
  6. 签名中的内容是安全信息:头的加密结果 + 体的加密结果 + 服务器不对外公开的安全码 进行md5加密 { “head”: “头的加密字符串”, “payload”: “体的加密字符串”, “secret_key”: “安全码” }

签发

根据登录请求提交来的 账号 + 密码 + 设备信息 签发 token

  1. 用基本信息存储json字典,采用base64算法加密得到 头字符串
  2. 用关键信息存储json字典,采用base64算法加密得到 体字符串
  3. 用头、体加密字符串再加安全码信息存储json字典,采用hash md5算法加密得到 签名字符串

账号密码就能根据User表得到user对象,形成的三段字符串用 . 拼接成token返回给前台

校验

根据客户端带token的请求 反解出 user 对象

  1. 将token按 . 拆分为三段字符串,第一段 头加密字符串 一般不需要做任何处理
  2. 第二段 体加密字符串,要反解出用户主键,通过主键从User表中就能得到登录用户,过期时间和设备信息都是安全信息,确保token没过期,且是同一设备来的
  3. 再用 第一段 + 第二段 + 服务器安全码 不可逆md5加密,与第三段 签名字符串 进行碰撞校验,通过后才能代表第二段校验得到的user对象就是合法地登录用户

jwt认证开发流程

  1. 用账号密码访问登录接口,登录接口逻辑中调用 签发token 算法,得到token,返回给客户端,客户端自己存到cookies中
  2. 校验token的算法应该写在中间件中,所有请求,都会进行认证校验,所以请求带了token,就会反解出用户信息

jwt token为什么要在前面添加Bearer这个单词

首先,我们知道,jwt生成的token形如aaa.bbb.ccc的字符串,但是为什么我们通常传输的是Bearer aaa.bbb.ccc呢,还要多次一举地添加上一个Bearer呢?

其实,这是一种规范。

规范解释

w3c规定,请求头Authorization用于验证用户身份。这就是告诉我们,token应该写在请求头Authorization中(当然你非要写在cookie,body或者写在url参数中也行,只要前后端开发约定好就行,但不建议)。

那么互联网发展至今,认证方式也有很多种,所以w3c还规定,Authorization应当写成这样的形式Authorization: <type> <credentials>,type是指认证的方式,credentials则是认证需要的信息。所以才有了jwt token的标准写法Authorization: Bearer aaa.bbb.ccc。

举个例子加深理解

再举个例子加深理解,比如,一个人想进一扇门,那他首先需要开门(访问服务器资源首先要认证身份),但是开门的方式有很多种,可能是机械锁,也可能是密码锁。如果这个人想进这扇门,那么他需要两个信息,一是开门的方式type,二是开门的具体信息credentials。

  • 所以他应当得到以下信息:机械锁 钥匙藏在门垫下,通过这个信息,这个人就知道,只需要掀开门垫拿到钥匙就能进门。
  • 而如果他得到的信息是:密码锁 钥匙藏在门垫下,由于有type的存在,这个人并不会认为在门垫下会有一把钥匙,而是知道输入“钥匙藏在门垫下”这个字符串,就能打开门。

通过这里例子,我们可以知道,type的作用,就是告诉服务器如何去认证访问者的身份。如果服务器事先就已经知道了认证方式,那么有无Bearer都不影响认证结果。

是否有必要加上Bearer

那么加Bearer是否有还有必要,答案是有必要,因为Bearer不是给人看的,是给框架看的,有了规范才有框架。假设让你去开发一个自动认证的框架,这个框架要求能支持多种认证方式(认证方式除了Bearer之外,还有Basic,Basic就是指,把用户名和密码用冒号拼接起来,再用base64编码,形如base64(username:password)),你会怎么做?

那么一个可行的方案就是,自动从请求头中获取Authorization的值,然后用空格截取开头的字符串得到认证的方式type,然后再去调用对应的认证方法,比如authByBearer()或者authByBasic()。事实上,很多认证框架就是这么干的!

后话

很多规范的提出,都是为了方便编码,我们应当按照规范编写代码(即使不按照规范也能完成功能,也不该那么做)。因为规范,大概率是因为前人遇到了问题才提出的解决方案,从而演变来的。我们后人最初可能感到不解,但是随着知识面的加深,总会慢慢理解其中的奥妙。