乐趣区

关于javascript:JSON-WEB-TOKENJWT

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 曾经在黑名单中了,申请会被回绝。

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

退出移动版