一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情。
写在前面
在今年2-3月写了一个校园社区的项目,当时负责了Koa
的后端搭建,主要负责用户相关和关注相关的接口,我们使用了token
作为我们保持登录状态的方案,这里就用到了jsonwebtoken
(JWT)这个插件
这个插件让我魂牵梦绕,今天就和大家分享一下我读源码了解它的过程吧!
你将会了解到:
JWT
在哪里使用了defineProperty
,为什么要用?jwt.sign
中竟然还有callback
函数,何时会去调用呢?JWT
的默认加密算法是什么?jwt.verify
如何验证option
中的jwtid
和payload.jti
是否相等?(方便了sessionid
的比对)option
中的一些其他不常用的参数,比如complete
。- 为什么
jws.createSign
可以使用回调函数? jwa
如何使用正则提取需要的参数。 等等...登dua郎
分享的不对的地方,大家提出来,给前端萌新一个小小帮助吧!
我们先来康康我看源码总结的流程图
- 我是想进去看看jwt是如何完成生成token的过程的,结果发现层层封装,看到第4层才看到加密源码
简单解释一下流程图
- 从第一层看起,
jsonwebtoken
中主要分为了sign
,verify
,decode
的实现,还有Error
错误的抛出- 其中
sign
,verify
,decode
都使用了jws
插件,所以我们接着看下一层 sign
使用了jws.createSign
,为什么不是jws.sign
呢?verify
使用了jws.createVerify
,为什么不是jws.verify
呢?- 我们继续往下看
- 其中
jws
主要封装了createSign
,sign
,createVerify
,verify
,decode
- 其中
createSign
是返回一个SignStream
对象,这个对象挂载了sign
方法 - 其中
createVerify
是返回一个VerifyStream
对象,这个对象挂载了verify
方法 decode
是挂载到VerifyStream
上的,单独提出来是因为jwt
使用到了- 主要是整合返回了
[header,payload,signature]
头部,负载和签名
- 主要是整合返回了
Error
在lib目录里面,使用了JsonWebTokenError.js
进行统一的错误处理,在其他的错误处有require
这个文件【我们在项目中也做了错误统一处理】jws.sign
和jws.verify
又用到了jwa插件- 所以我们继续往下看
- 其中
jwa
主要是通过正则分割出参数然后调用crypto
插件crypto
插件就是各种加密算法底层实现了- 这里就不一一介绍了 更多的细节可能要在下面贴源码的地方和大家分享!
- 看了一下源码思路,个人觉得也用到了
组件封装
类似的思想,对思维锻炼还是有的! - 看完之后只是觉得更接近事物的本质了吧,更理解
JWT
到底做了啥 - 能对事物有一个好的理解,应该是大多数程序员的自我修养吧!
jsonwebtoken
index.js
- 我们可以很清楚地看到
jsonwebtoken
插件地解构 - 特别有趣的一点是,
defineProperty
大家都知道他是vue2
的数据监听的根基(get
和set
来拦截数据),但是它其实本质上是用来定义属性的,这里把这个属性给成了不可枚举,就是为了不让直接访问!
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
函数里面,有payload
,secretOrPrivateKey
,options
,callback
四个参数- 但是我们在项目中实际用到了前三个参数,而且第三个参数仅仅使用了过期时间的参数
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
到底是什么算法,我们要看到最后才知道
- 这里的
- 然后有个私钥和公钥的提取,用到了
ES6
的includes
,很丝滑 - jwtid是可以通过option传入的,会有个自动比较
- 所以项目中把sessionid(最好指定为了源码中的
jti
)放在载荷中,然后手动比较是很笨的做法!,我们在option
中给一下,会有一个自动比较!
- 所以项目中把sessionid(最好指定为了源码中的
- 最后调用的
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
- 我们可以看到重点在
SignStream
和VerifyStream
的实现 - 比较有趣的是:
createSign
是new一个新的SignStream
sign
是SignStream.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
插件的使用吧,很可惜,还是没到真正的源码creatSign
的once
算是能够使用回调的原因吧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
就是把头部,载荷,签证合起来header
用split
分割了,应该是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
- 我们对传入的参数使用了正则然后调用不同的函数
- 为了方便调用,我们把不同的函数写到了
signerFactories
,verifierFactories
里面
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"));