cjwt
Version:
Casstime jwt middleware
291 lines (247 loc) • 7.52 kB
JavaScript
const util = require('util');
const uuid = require('uuid/v4');
const debug = require('debug')('cjwt');
const { decode, verify, sign } = require('jsonwebtoken');
const expressJwt = require('express-jwt');
const verifyP = promisify(verify);
const signP = promisify(sign);
/**
* @module CJWT
* @class {CJWT}
*/
module.exports = class CJWT {
/**
* Casstime JWT
* @constructor
* @param {Object} options
* @param {String|Function} options.secret jwtSecretCode或取得方式,需要返回Promise
* @param {Object} options.signOptions jwt签名选项,参考https://github.com/auth0/node-jsonwebtoken
* @param {Object} options.verifyOptions jwt校验选项,参考https://github.com/auth0/node-jsonwebtoken
*/
constructor(options = {}) {
const secret = options.secret;
const { signOptions, verifyOptions } = options;
if (!secret) {
throw new Error('options 必须指定 secret');
}
if (!util.isObject(signOptions)) {
throw new Error('必须指定 signOptions');
}
if (!util.isObject(verifyOptions)) {
throw new Error('必须指定 verifyOptions');
}
if (!util.isFunction(secret) && !util.isString(secret)) {
throw new Error('secret 必须为Function或者String类型');
}
let secretCallback = secret;
if (util.isString(secret)) {
secretCallback = () => Promise.resolve(secret);
}
this._options = Object.assign({}, options, { secret: secretCallback });
this.decode = decode;
this.buildSubject = buildSubject;
this.parseSubject = parseSubject;
}
/**
* 结合options.signOptions签名
* @param {String} type 签名身份的类型,可以是个人用户或服务
* @param {String} id 签名身份的id
* @param {Object} ctx 本次请求的上下文,比如微服务可以已用户的ctx来请求数据
* @return {Promise.<String>}
*/
sign(type, id, ctx = {}) {
const subject = buildSubject(type, id);
const jwtid = uuid();
const { secret, signOptions } = this._options;
const finalOptions = Object.assign({ subject, jwtid }, signOptions);
const payload = Object.assign({ ctx }, getPayloadByOptions(finalOptions));
return secret(payload)
.then((secretCode) => {
return signP({ ctx }, secretCode, finalOptions);
});
}
/**
* 结合verifyOptions校验jwt
* @param {String} jwtString jwt字符串
* @param {Object} [options] 附加选项,参考https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback
* @return {Promise.<Object>} 如果正确,返回payload
*/
verify(jwtString, options) {
const payload = decode(jwtString);
const { secret, verifyOptions } = this._options;
const finalOptions = Object.assign({}, verifyOptions, options);
return secret(payload)
.then((secretCode) => {
return verifyP(jwtString, secretCode, finalOptions);
});
}
/**
* 结合verifyOptions生成express中间件
* @param {Object} [options] 参考https://github.com/auth0/express-jwt
* @return {Function}
*/
middleware(options) {
const { secret, verifyOptions } = this._options;
const defaultOptions = {
getToken,
requestProperty: 'jwtPayload',
credentialsRequired: false,
maxAge: '7d'
};
const secretCallback = (req, payload, done) => {
try {
secret(payload).then(done.bind(null, null));
} catch (e) {
done(e);
}
};
const finalOptions = Object.assign({}, defaultOptions, verifyOptions, { secret: secretCallback }, options);
const verifyFn = this.verify.bind(this);
const signFn = this.sign.bind(this);
return createJwtMiddleware(finalOptions, {
verify: verifyFn,
sign: signFn,
decode, parseSubject, buildSubject});
}
};
/**
* 将函数转换成promise,内部使用,简单处理
* @param fn
* @return {function(...[*])}
*/
function promisify(fn) {
return (...args) => {
return new Promise((resolve, reject) => {
const callback = (error, result) => {
error ? reject(error) : resolve(result);
};
fn(...args, callback);
});
}
}
/**
* 从express request取jwt字符串,优先从headers.authorization取,如果没有,从cookies.jwt取
* @param req
* @return {*}
*/
function getToken(req) {
// 优先从头部取
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
return req.headers.authorization.split(' ')[1];
}
// 然后从cookies中取
const jwt = req.cookies['jwt'] || req.cookies['JWT'];
if (jwt) {
return req.cookies['jwt'];
}
return null;
}
/**
* 解析subject,所有subject包含两部分内容,第一为身份类型,第二为身份标识
* @param {String} sub
* @return {Object}
*/
function parseSubject(sub = '') {
// 兼容原有格式
const results = /^casstime:\/\/(.+?)\/(.+?)$/.exec(sub);
if (results) {
return {
type: results[1],
id: results[2]
}
}
// 现用冒号分割类型和标识
if (sub.indexOf(':') > -1) {
const [ type, id ] = sub.split(':');
return { type, id };
}
return {};
}
/**
* 构建subject字符串
* @param {String} type 身份类型
* @param {String} id 身份ID
* @return {string}
*/
function buildSubject(type, id) {
return `${type}:${id}`;
}
/**
* 生成express中间件
* @param {Object} options 参照https://github.com/auth0/express-jwt
* @param {Object} exposedFns 需要暴露出的函数
* @return {function(*=, *=, *)}
*/
function createJwtMiddleware(options, exposedFns) {
return (req, res, next) => {
const callback = (error) => {
if (error) {
debug('jwt校验失败 %s', error.message);
req.jwtPayload = null;
}
if (req.jwtPayload) {
const jwtPayload = req.jwtPayload;
const { sub } = req.jwtPayload;
if (jwtPayload.from) {
jwtPayload._from = jwtPayload.from;
}
jwtPayload.from = parseSubject(sub);
const getTokenCallback = getToken.bind(null, req);
req.cjwt = createProxy(req.jwtPayload, Object.assign({}, exposedFns, { getToken: getTokenCallback }));
}
process.nextTick(() => next());
};
expressJwt(options)(req, res, callback);
}
}
function createProxy(jwtPayload, exposedFns) {
const keyAlias = {
jwtid: 'jti',
expires: 'exp',
issuer: 'iss',
audience: 'aud',
issuedAt: 'iat',
notBefore: 'nbf'
};
return new Proxy(jwtPayload, {
get(target, key) {
const actualKey = keyAlias[key] || key;
if (target[actualKey]) {
return target[actualKey];
}
if (exposedFns[actualKey]) {
return exposedFns[actualKey];
}
const subject = parseSubject(jwtPayload.sub);
if (actualKey === 'subject') {
return subject;
}
if (actualKey === 'isService') {
return subject.type === 'service';
}
if (actualKey === 'subjectType') {
return subject.type;
}
return undefined;
},
set(target, key, value) {
console.error('cjwt对象只读');
}
});
}
function getPayloadByOptions(options) {
const map = {
audience: 'aud',
issuer: 'iss',
jwtid: 'jti',
subject: 'sub'
};
const result = {};
Object.getOwnPropertyNames(options)
.filter(name => map[name])
.forEach((name) => {
const key = map[name];
result[key] = options[name];
});
return result;
}