JWT源码浅析(四层封装,有示意图)

写在前面

在今年2-3月写了一个校园社区的项目,当时负责了Koa的后端搭建,主要负责用户相关和关注相关的接口,我们使用了token作为我们保持登录状态的方案,这里就用到了jsonwebtoken(JWT)这个插件

这个插件让我魂牵梦绕,今天就和大家分享一下我读源码了解它的过程吧!


你将会了解到:

  • JWT在哪里使用了defineProperty,为什么要用?
  • jwt.sign中竟然还有callback函数,何时会去调用呢?
  • JWT的默认加密算法是什么?
  • jwt.verify如何验证option中的jwtidpayload.jti是否相等?(方便了sessionid的比对)
  • option中的一些其他不常用的参数,比如complete
  • 为什么jws.createSign可以使用回调函数?
  • jwa如何使用正则提取需要的参数。
    等等…登dua郎

分享的不对的地方,大家提出来,给前端萌新一个小小帮助吧!

我们先来康康我看源码总结的流程图

  • 我是想进去看看jwt是如何完成生成token的过程的,结果发现层层封装,看到第4层才看到加密源码
    在这里插入图片描述

简单解释一下流程图

  • 从第一层看起,jsonwebtoken中主要分为了signverifydecode的实现,还有Error错误的抛出
    • 其中signverifydecode都使用了jws插件,所以我们接着看下一层
    • sign使用了jws.createSign,为什么不是jws.sign呢?
    • verify使用了jws.createVerify,为什么不是jws.verify呢?
    • 我们继续往下看
  • jws主要封装了createSignsigncreateVerifyverifydecode
    • 其中createSign是返回一个SignStream对象,这个对象挂载了sign方法
    • 其中createVerify是返回一个VerifyStream对象,这个对象挂载了verify方法
    • decode是挂载到VerifyStream上的,单独提出来是因为jwt使用到了
      • 主要是整合返回了[header,payload,signature]头部,负载和签名
    • Error在lib目录里面,使用了JsonWebTokenError.js进行统一的错误处理,在其他的错误处有require这个文件【我们在项目中也做了错误统一处理】
    • jws.signjws.verify又用到了jwa插件
    • 所以我们继续往下看
  • jwa主要是通过正则分割出参数然后调用crypto插件
  • crypto插件就是各种加密算法底层实现了
    • 这里就不一一介绍了
      更多的细节可能要在下面贴源码的地方和大家分享!

  • 看了一下源码思路,个人觉得也用到了组件封装类似的思想,对思维锻炼还是有的!
  • 看完之后只是觉得更接近事物的本质了吧,更理解JWT到底做了啥
  • 能对事物有一个好的理解,应该是大多数程序员的自我修养吧!

jsonwebtoken

index.js

  • 我们可以很清楚地看到jsonwebtoken插件地解构
  • 特别有趣的一点是,defineProperty大家都知道他是vue2的数据监听的根基(getset来拦截数据),但是它其实本质上是用来定义属性的,这里把这个属性给成了不可枚举,就是为了不让直接访问!
module.exports = {
  verify: require('./verify'),
  sign: require('./sign'),
  JsonWebTokenError: require('./lib/JsonWebTokenError'),
  NotBeforeError: require('./lib/NotBeforeError'),
  TokenExpiredError: require('./lib/TokenExpiredError'),
};

Object.defineProperty(module.exports, 'decode', {
  enumerable: false,
  value: require('./decode'),
});

sign.js

  • 在这个文件里面只看重点了!(避免篇幅过长,这里只放了部分源码)
  • 我们可以看到sign函数里面,有payloadsecretOrPrivateKeyoptionscallback四个参数
    • 但是我们在项目中实际用到了前三个参数,而且第三个参数仅仅使用了过期时间的参数
      • options里面有7个参数可以使用,我们只用过expiresIn,我太肤浅了!
    • options是函数,则直接被识别为callback,保证options至少为{}
    • 失败的时候会调用callback来处理error
  • 我们可以看到,如果有callback,我们是调用的jws.createSign
  • 如果没有callback回调函数参数,我们是调用的jws.sign
  • 我们在下面jws插件中再来看看这个细节有什么区别
    • 目前看来就是前者可以调用回调函数
var options_for_objects = [
  'expiresIn',
  'notBefore',
  'noTimestamp',
  'audience',
  'issuer',
  'subject',
  'jwtid',
];
module.exports = function (payload, secretOrPrivateKey, options, callback) {
  if (typeof options === 'function') {         // 这里还做了options是函数,则直接被识别为callback
    callback = options;
    options = {};
  } else {
    options = options || {};
  }
  function failure(err) {                      // 调用callback函数
    if (callback) {
      return callback(err);
    }
    throw err;
  }
  if (typeof callback === 'function') {
    callback = callback && once(callback);
    jws.createSign({
      header: header,
      privateKey: secretOrPrivateKey,
      payload: payload,
      encoding: encoding
    }).once('error', callback)
      .once('done', function (signature) {
        callback(null, signature);
      });
  } else {
    return jws.sign({header: header, payload: payload, secret: secretOrPrivateKey, encoding: encoding});
  }
};
  • 对于verify同样的,它也是有回调函数的
  • 和上面重复的就不说了,看几个比较有趣的点
    • options里面是可以传入加密算法的(没有回默认为none)
      • 这里的none到底是什么算法,我们要看到最后才知道
    • 然后有个私钥和公钥的提取,用到了ES6includes,很丝滑
    • jwtid是可以通过option传入的,会有个自动比较
      • 所以项目中把sessionid(最好指定为了源码中的jti)放在载荷中,然后手动比较是很笨的做法!,我们在option中给一下,会有一个自动比较!
    • 最后调用的done本质上还是传入的callback
      • 我们可以看到option.complete可以控制最后调用done的参数
  • 看到最后我们知道header.alg控制算法的方式,所以我知道了jwt的默认算法是HS256
  var header = Object.assign({
    alg: options.algorithm || 'HS256',
    typ: isObjectPayload ? 'JWT' : undefined,
    kid: options.keyid
  }, options.header);

verify.js

module.exports = function (jwtString, secretOrPublicKey, options, callback) {
    var done;
    if (callback) {
      done = callback;
    } else {
      done = function(err, data) {
        if (err) throw err;
        return data;
      };
    }
    if (!hasSignature && !options.algorithms) {
      options.algorithms = ['none'];
    }
    if (!options.algorithms) {
      options.algorithms = secretOrPublicKey.toString().includes('BEGIN CERTIFICATE') ||
        secretOrPublicKey.toString().includes('BEGIN PUBLIC KEY') ? PUB_KEY_ALGS :
        secretOrPublicKey.toString().includes('BEGIN RSA PUBLIC KEY') ? RSA_KEY_ALGS : HS_ALGS;
    }
    if (options.jwtid) {
      if (payload.jti !== options.jwtid) {
        return done(new JsonWebTokenError('jwt jwtid invalid. expected: ' + options.jwtid));
      }
    }
    if (options.complete === true) {
      var signature = decodedToken.signature;
      return done(null, {
        header: header,
        payload: payload,
        signature: signature
      });
    }
    return done(null, payload);
  });
};

decode.js

  • 主要功能是返回我们的payload载荷
  • 具体怎么拿到还要看jws插件
var jws = require('jws');
module.exports = function (jwt, options) {
  options = options || {};
  var decoded = jws.decode(jwt, options);
  if (!decoded) { return null; }
  var payload = decoded.payload;
  if (options.complete === true) {
    return {
      header: decoded.header,
      payload: payload,
      signature: decoded.signature
    };
  }
  return payload;
};

jws

  • 我们来到了下一层jws插件啦!

index.js

  • 我们可以看到重点在SignStreamVerifyStream的实现
  • 比较有趣的是:
    • createSign是new一个新的SignStream
      • signSignStream.sign
    • 应该马上就可以知道为啥createSign可以回调了
/*global exports*/
var SignStream = require('./lib/sign-stream');
var VerifyStream = require('./lib/verify-stream');

var ALGORITHMS = [
  'HS256', 'HS384', 'HS512',
  'RS256', 'RS384', 'RS512',
  'PS256', 'PS384', 'PS512',
  'ES256', 'ES384', 'ES512'
];

exports.ALGORITHMS = ALGORITHMS;
exports.sign = SignStream.sign;
exports.verify = VerifyStream.verify;
exports.decode = VerifyStream.decode;
exports.isValid = VerifyStream.isValid;
exports.createSign = function createSign(opts) {
  return new SignStream(opts);
};
exports.createVerify = function createVerify(opts) {
  return new VerifyStream(opts);
};

lib

sign-stream.js
  • jwsSign算是对jwa插件的使用吧,很可惜,还是没到真正的源码
  • creatSignonce算是能够使用回调的原因吧
    • once是从DataStream来的,我们就不再细看这个了!
  • 在挂载sign函数的时候,有emit的抛出
    • 在哪里处理emit的我还没搞清楚
function jwsSign(opts) {
  var header = opts.header;
  var payload = opts.payload;
  var secretOrKey = opts.secret || opts.privateKey;
  var encoding = opts.encoding;
  var algo = jwa(header.alg);
  var securedInput = jwsSecuredInput(header, payload, encoding);
  var signature = algo.sign(securedInput, secretOrKey);
  return util.format('%s.%s', securedInput, signature);
}

function SignStream(opts) {
  var secret = opts.secret||opts.privateKey||opts.key;
  var secretStream = new DataStream(secret);
  this.readable = true;
  this.header = opts.header;
  this.encoding = opts.encoding;
  this.secret = this.privateKey = this.key = secretStream;
  this.payload = new DataStream(opts.payload);
  this.secret.once('close', function () {
    if (!this.payload.writable && this.readable)
      this.sign();
  }.bind(this));

  this.payload.once('close', function () {
    if (!this.secret.writable && this.readable)
      this.sign();
  }.bind(this));
}
util.inherits(SignStream, Stream);

SignStream.prototype.sign = function sign() {
  try {
    var signature = jwsSign({
      header: this.header,
      payload: this.payload.buffer,
      secret: this.secret.buffer,
      encoding: this.encoding
    });
    this.emit('done', signature);
    this.emit('data', signature);
    this.emit('end');
    this.readable = false;
    return signature;
  } catch (e) {
    this.readable = false;
    this.emit('error', e);
    this.emit('close');
  }
};

SignStream.sign = jwsSign;

module.exports = SignStream;
verify-stream.js
decode
  • 可以看到decode就是把头部,载荷,签证合起来
    • headersplit分割了,应该是ctx.request.这种,拿到后面的参数
function signatureFromJWS(jwsSig) {
  return jwsSig.split('.')[2];
}
function jwsDecode(jwsSig, opts) {
  opts = opts || {};
  jwsSig = toString(jwsSig);

  if (!isValidJws(jwsSig))
    return null;

  var header = headerFromJWS(jwsSig);

  if (!header)
    return null;

  var payload = payloadFromJWS(jwsSig);
  if (header.typ === 'JWT' || opts.json)
    payload = JSON.parse(payload, opts.encoding);

  return {
    header: header,
    payload: payload,
    signature: signatureFromJWS(jwsSig)
  };
}
VerifyStream.decode = jwsDecode;
verify
  • 核心实现还是在algo,而algo来自jwa插件
function securedInputFromJWS(jwsSig) {
  return jwsSig.split('.', 2).join('.');
}
function signatureFromJWS(jwsSig) {
  return jwsSig.split('.')[2];
}
function jwsVerify(jwsSig, algorithm, secretOrKey) {
  if (!algorithm) {
    var err = new Error("Missing algorithm parameter for jws.verify");
    err.code = "MISSING_ALGORITHM";
    throw err;
  }
  jwsSig = toString(jwsSig);
  var signature = signatureFromJWS(jwsSig);
  var securedInput = securedInputFromJWS(jwsSig);
  var algo = jwa(algorithm);
  return algo.verify(securedInput, signature, secretOrKey);
}

jwa

  • 我们对传入的参数使用了正则然后调用不同的函数
  • 为了方便调用,我们把不同的函数写到了signerFactoriesverifierFactories里面
module.exports = function jwa(algorithm) {
  var signerFactories = {
    hs: createHmacSigner,
    rs: createKeySigner,
    ps: createPSSKeySigner,
    es: createECDSASigner,
    none: createNoneSigner,
  }
  var verifierFactories = {
    hs: createHmacVerifier,
    rs: createKeyVerifier,
    ps: createPSSKeyVerifier,
    es: createECDSAVerifer,
    none: createNoneVerifier,
  }
  var match = algorithm.match(/^(RS|PS|ES|HS)(256|384|512)$|^(none)$/);
  if (!match)
    throw typeError(MSG_INVALID_ALGORITHM, algorithm);
  var algo = (match[1] || match[3]).toLowerCase();
  var bits = match[2];
  return {
    sign: signerFactories[algo](bits),
    verify: verifierFactories[algo](bits),
  }
};

以createHmacSigner为例

  • 整个的核心代码还是在crypto-js插件里面
  • 但是感觉近在咫尺了!
function createHmacSigner(bits) {
  return function sign(thing, secret) {
    checkIsSecretKey(secret);          // 检查密钥的一个函数,不再展开细说
    thing = normalizeInput(thing);     // 有一个stringify的操作
    var hmac = crypto.createHmac('sha' + bits, secret);
    var sig = (hmac.update(thing), hmac.digest('base64'))
    return fromBase64(sig);
  }
}

crypto-js

  • 这里就到头了
  • 底层实现就是各种加密算法的具体实现了,我已经精疲力尽了
  • 如果点赞过百,再出一期康康加密算法的具体实现吧
    我们来康康文档中crypto-js的具体使用
  • 加密内容+生成的hash值
  • 然后是调用加载的算法进行加密,这里用到了私钥加密
  • 比较有趣的是:
    • 文档里面写得client也可以使用,所以知道私钥在浏览器解密就是可以做到的
<script type="text/javascript" src="path-to/bower_components/crypto-js/crypto-js.js"></script>
<script type="text/javascript">
    var encrypted = CryptoJS.AES(...);
    var encrypted = CryptoJS.SHA256(...);
</script>

import sha256 from 'crypto-js/sha256';
import hmacSHA512 from 'crypto-js/hmac-sha512';
import Base64 from 'crypto-js/enc-base64';

const message, nonce, path, privateKey; // ...
const hashDigest = sha256(nonce + message);
const hmacDigest = Base64.stringify(hmacSHA512(path + hashDigest, privateKey));
*** 或者第二种使用方法
var CryptoJS = require("crypto-js");
console.log(CryptoJS.HmacSHA1("Message", "Key"));
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值