JWTtoke的一种模式。次要由header(头部)payload(载荷)signature(签名)这三局部字符串组成,这三局部应用"."进行连贯,残缺的一条JWT值为${header}.${payload}.${signature},例如上面应用"."进行连贯的字符串:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMTEiLCJpYXQiOjE2MTQzMjU5NzksImV4cCI6MTYxNDMyNTk4MH0.iMjzC_jN3iwSpIyawy3kNRNlL1mBSEiXtOJqhIZmsl8

header

header最开始是一个JSON对象,该JSON蕴含algtyp这两个属性,对JSON应用base64url(应用base64转码后再对特殊字符进行解决的编码算法,前面会具体介绍)编码后失去的字符串就是header的值。

{  "alg": "HS256",  "typ": "JWT"}
  • alg:签名算法类型,生成JWT中的signature局部时须要应用到,默认HS256
  • typ:以后token类型

payload

payloadheader一样,最开始也是一个JSON对象,应用base64url编码后的字符串就是最终的值。

payload中寄存着7个官网定义的属性,同时咱们能够写入一些额定的信息,例如用户的信息等。

  • iss:签发人
  • sub:主题
  • aud:受众
  • exp:过期工夫
  • nbf:失效工夫
  • iat:签发工夫
  • jti:编号

signature

signature会应用headeralg属性定义的签名算法,对headerpayload合并的字符串进行加密,加密过程伪代码如下:

HMACSHA256(  `${base64UrlEncode(header)}.${base64UrlEncode(payload)}`,  secret)

加密过后失去的字符串就是signature

base64url

通过base64编码过后的字符串中会存在+、/、=这三个特殊字符,而JWT有可能通过url query进行传输,而url query中不能有+、/url safe base64规定将+/别离用-_进行替换,同时=会在url query中产生歧义,因而须要将=删除,这就是整个编码过程,代码如下

/** * node环境 * @desc 编码过程 * @param {any} data 须要编码的内容 * @return {string} 编码后的值 */function base64UrlEncode(data) {  const str = JSON.stringify(data);  const base64Data = Buffer.from(str).toString('base64');  // + -> -  // / -> _  // = ->   const base64UrlData = base64Data.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, '');  return base64UrlData;}

当服务解析JWT内容的时候,须要将base64url编码后的内容进行解码操作。首先就是将-_转成+/base64转码后失去的字符串长度可能被4整除,并且base64编码后的内容只有最初才会有=,上面咱们看下解码过程:

/** * node环境 * @desc 解码过程 * @param {any} base64UrlData 须要解码的内容 * @return {string} 解码后的内容 */function base64UrlDecode(base64UrlData) {  // - -> +  // _ -> /  // 应用=补充  const base64LackData = base64UrlData.replace(/\-/g, '+').replace(/\_/g, '/');  const num = 4 - base64LackData.length % 4;  const base64Data = `${base64LackData}${'===='.slice(0, num)}`  const str = Buffer.from(base64Data, 'base64').toString();  let data;  try {    data = JSON.parse(str);  } catch(err) {    data = str;  }  return data;}

JWT应用

node中应用jsonwebtoken插件能够疾速进行JWT开发,该插件次要提供了signverify两个函数,别离用来生成和验证JWT

这里简略实现下JWT的生成和校验性能:

/** * @desc JWT生成 * base64UrlEncode(jwt header) * base64UrlEncode(jwt payload) * HMACSHA256(`${base64UrlEncode(header)}.${base64UrlEncode(payload)}`, secret) * @param {json} payload * @param {string} secret * @param {json} options */const crypto = require('crypto');function sign(payload, secret) {  const header = {    alg: 'HS256', // 这里只是走下流程,就间接用HS256进行签名了    typ: 'JWT',  };  const base64Header = base64UrlEncode(header);  const base64Payload = base64UrlEncode(payload);  const waitCryptoStr = `${base64Header}.${base64Payload}`;  const signature = crypto.createHmac('sha256', secret)                    .update(waitCryptoStr)                    .digest('hex');  return `${base64Header}.${base64Payload}.${signature}`;}
/** * @desc JWT校验 * jwt内容是否被篡改 * jwt时效校验,exp和nbf * @param {string} jwt * @param {string} secret */const crypto = require('crypto');function verify(jwt, secret) {  // jwt内容是否被篡改  const [base64Header, base64Payload, oldSinature] = jwt.split('.');  const newSinature = crypto.createHmac('sha256', secret)                            .update(`${base64Header}.${base64Payload}`)                            .digest('hex');  if (newSinature !== oldSinature) return false;  const now = Date.now();  const { nbf = now, exp = now + 1 } = base64UrlDecode(base64Payload);  // jwt时效校验,大于等于失效工夫,小于过期工夫  return now >= nbf && now < exp;}

重放攻打

攻击者通过拦挡申请拿到用户的JWT,而后应用该JWT申请后端敏感服务,来歹意的获取或批改该用户的数据。

加烦扰码

服务端在生成JWT第三局部signature时,密钥的内容能够蕴含客户端的UA,既HMACSHA256(`${base64UrlEncode(header)}.${base64UrlEncode(payload)}`,`${secret}${UA}`)

如果该JWT在另一个客户端应用的时候,因为UA不同,从新生成的签名与JWT中的signature不统一,申请有效。

该计划也不能完全避免重放攻打,如果攻击者发现服务端加密的时候应用了UA字段,那攻击者在拦挡JWT的时候,会一并拿到用户UA,而后再同时带上JWTUA申请服务端,服务端就感觉以后申请是无效的。

UA改成IP也是有一样的问题。

JWT续签

服务端验证传入的JWT通过后,生成一个新的JWT,在响应申请的时候,将新的JWT返回给客户端,同时将传入的JWT退出到黑名单中。客户端在收到响应后,将新的JWT写入本地缓存,等到下次申请的时候,将新的JWT带上一起申请服务。服务端验证的JWT的时候,须要判断以后JWT是否在黑名单中,如果在,就回绝以后申请,反之就承受。如果申请的是登出接口,就不下发新的JWT

/** * @desc JWT续签例子 */const http = require('http');const secret = 'test secret';// 临时用一个变量来寄存黑名单,理论生产中改用redis、mysql等数据库寄存const blacks = [];http.createServer((req, res) => {  const { authorization } = req.headers;  // 1、验证传入的JWT是否可用  const availabel = verify(authorization, secret);  if (!availabel) {    return res.end();  }  // 2、判断黑名单中是否存在以后JWT  if (blacks.includes(authorization)) {    return res.end();  }  // 3、将以后JWT放入黑名单  blacks.push(authorization);  // 4、生成新的JWT,并响应申请  const newJwt = sign({ userId: '1' }, secret);  res.end(newJwt);}).listen(3000);

每次申请都刷新JWT会引起上面两个问题:

  • 问题一:每次申请都会将老的JWT放入黑名单中,随着工夫的推移,黑名单越来越宏大,占用内存过多,每次查问工夫过长。
  • 问题二:客户端并行申请接口的时候,这些申请带的JWT都是一样的值,申请进入服务始终有先后顺序,先进入的申请,服务端会将以后JWT放入黑名单。后进入的申请,服务端在判断到以后JWT在黑名单中,从而回绝以后申请。

问题一解决方案:
JWT中定义exp过期工夫,程序设置定时工作,每过一段时间就去将黑名单中曾经过期的JWT给删除。

const http = require('http');const secret = 'test secret';// 临时用一个变量来寄存黑名单,理论生产中改用redis、mysql等数据库寄存const blacks = [];function cleanBlack() {  setTimeout(() => {    blacks = blacks.filter(balck => verify(balck));    cleanBlack();  }, 10 * 60 * 1000); // 10m清理一次黑名单}cleanBlack();http.createServer((req, res) => {  const { authorization } = req.headers;  // 1、验证传入的JWT是否可用  const availabel = verify(authorization, secret);  if (!availabel) {    return res.end();  }  // 2、判断黑名单中是否存在以后JWT  if (blacks.includes(authorization)) {    return res.end();  }  // 3、将以后JWT放入黑名单  blacks.push(authorization);  // 4、生成新的JWT,并响应申请  const newJwt = sign({    userId: '1',    exp: Date.now() + 10 * 60 * 1000, // 10m过期  }, secret);  res.end(newJwt);}).listen(3000);

问题二解决方案:
给黑名单中的JWT增加一个宽限工夫。如果以后申请携带的JWT曾经在黑名单了,然而以后还没有超过非给以后JWT的宽限工夫,那么就失常运行后续代码,如果超出就拒绝请求。

const http = require('http');const secret = 'test secret';// 临时间接用一个变量来寄存黑名单,理论生产中改用redis或者mysql寄存const blacks = [];const grace = {};http.createServer((req, res) => {  const { authorization } = req.headers;  const now = Date.now();  // 1、验证传入的JWT是否可用  const availabel = verify(authorization, secret);  if (!availabel) {    return res.end();  }  // 2、判断黑名单中是否存在以后JWT,如果在,判断以后JWT是否处于宽限期内  const unavailable = blacks.includes(authorization) && now >= (grace[authorization] || now);  if (unavailable) {    return res.end();  }  // 3、以后JWT还没有退出黑名单时,将以后JWT放入黑名单  if (!blacks.includes(authorization)) {    blacks.push(authorization);    grace[authorization] = now + 1 * 60 * 1000; // 1m宽限工夫  }  // 4、生成新的JWT,并响应申请  const newJwt = sign({ userId: '1' }, secret);  res.end(newJwt);}).listen(3000);
留神:这个宽限工夫是JWT退出黑名单的时,根据以后工夫向后设置的一个工夫节点,并不是生成JWT的时候退出的。

互斥登录

应用JWT实现登录逻辑,要实现服务端被动登出性能,服务端须要在下发JWT前,就将该JWT寄存到用户与JWT对应关系数据库中,等到服务端要被动登记该用户的时候,就将用户所对应的JWT退出到黑名单中。后续,该用户再申请服务的时候,传入的JWT曾经在黑名单中了,申请会被回绝。

用户明码批改,服务端被动登记用户登录性能,基本上和互斥登录差不多。