持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天
起因
在写项目的时候发现使用了JWT,设置了过期时间,但是并没有做过期校验。却可以触发到过期校验。 就很好奇是怎么回事?扒拉了一些源码和文档,找到了原因。
一些前提知识
EGG中的中间件和插件
中间件
Egg 是基于 Koa 实现的,所以 Egg 的中间件形式和 Koa 的中间件形式是一样的,都是基于洋葱圈模型。每次我们编写一个中间件,就相当于在洋葱外面包了一层。
中间件最主要的作用就是在请求前后做一些工作,比如鉴权,错误处理等。当然中间件也可以配置全局的和特定路由的。官网有具体配置方法。
插件
为啥要使用插件,egg官方回答
我们在使用 Koa 中间件过程中发现了下面一些问题:
中间件加载其实是有先后顺序的,但是中间件自身却无法管理这种顺序,只能交给使用者。这样其实非常不友好,一旦顺序不对,结果可能有天壤之别。
中间件的定位是拦截用户请求,并在它前后做一些事情,例如:鉴权、安全检查、访问日志等等。但实际情况是,有些功能是和请求无关的,例如:定时任务、消息订阅、后台逻辑等等。
有些功能包含非常复杂的初始化逻辑,需要在应用启动的时候完成。这显然也不适合放到中间件中去实现。
综上所述,我们需要一套更加强大的机制,来管理、编排那些相对独立的业务逻辑。
中间件和插件的关系
一个插件其实就是一个『迷你的应用』,和应用(app)几乎一样:
他们的关系是:
- 应用可以直接引入 Koa 的中间件。
- 当遇到上一节提到的场景时,则应用需引入插件。
- 插件本身可以包含中间件。
- 多个插件可以包装为一个上层框架。
jwt中的中间件会自动校验token是否过期
从jwt过期的报错的位置定位到是这里:
这个就是一个中间件文件。
const jwt = require("jsonwebtoken");
const UnauthorizedError = require("./errors/UnauthorizedError");
const unless = require("koa-unless");
const async = require("async");
const set = require("lodash.set");
const isFunction = require("lodash.isfunction");
const wrapIsRevokedInAsync = isRevoked => {
return async function(_, __) {
return isRevoked || false;
};
};
function wrapStaticSecretInAsync(secret) {
return async function(_, __) {
return secret;
};
}
module.exports = function(options) {
if (!options || !options.secret) throw new Error("secret should be set");
let secretAsync = options.secret;
if (!isFunction(secretAsync)) {
secretAsync = wrapStaticSecretInAsync(secretAsync);
}
let isRevokedAsync = options.isRevoked;
if (!isFunction(isRevokedAsync)) {
isRevokedAsync = wrapIsRevokedInAsync(isRevokedAsync);
}
const property = options.property || options.userProperty || "user";
const credentialsRequired =
typeof options.credentialsRequired === "undefined"
? true
: options.credentialsRequired;
const middleware = async function(ctx, next) {
let token;
if (ctx.method === "OPTIONS" && ctx.get("access-control-request-headers")) {
const hasAuthInAccessControl = !!~ctx
.get("access-control-request-headers")
.split(",")
.map(function(header) {
return header.trim();
})
.indexOf("authorization");
if (hasAuthInAccessControl) {
return await next();
}
}
if (options.getToken && typeof options.getToken === "function") {
try {
token = options.getToken(ctx);
} catch (e) {
throw e;
}
} else if (ctx.get("authorization")) {
const parts = ctx.get("authorization").split(" ");
if (parts.length == 2) {
const scheme = parts[0];
const credentials = parts[1];
if (/^Bearer$/i.test(scheme)) {
token = credentials;
} else {
if (credentialsRequired) {
throw new UnauthorizedError("credentials_bad_scheme", {
message: "Format is Authorization: Bearer [token]"
});
} else {
return await next();
}
}
} else {
throw new UnauthorizedError("credentials_bad_format", {
message: "Format is Authorization: Bearer [token]"
});
}
}
if (!token) {
if (credentialsRequired) {
throw new UnauthorizedError("credentials_required", {
message: "No authorization token was found"
});
} else {
return await next();
}
}
let dtoken;
try {
dtoken = jwt.decode(token, { complete: true }) || {};
} catch (err) {
throw new UnauthorizedError("invalid_token", err);
}
const arity = secretAsync.length;
let secret;
if (arity == 4) {
secret = await secretAsync(ctx, dtoken.header, dtoken.payload);
} else {
// arity == 3
secret = await secretAsync(ctx, dtoken.payload);
}
const decoded = await new Promise((resolve, reject) => {
jwt.verify(token, secret, options, function(err, decoded) {
if (err) {
reject(new UnauthorizedError("invalid_token", err));
} else {
resolve(decoded);
}
});
});
const result = await new Promise(async (resolve, reject) => {
let revoked;
try {
revoked = await isRevokedAsync(ctx, dtoken.payload);
} catch (err) {
reject(err);
}
if (revoked) {
reject(
new UnauthorizedError("revoked_token", {
message: "The token has been revoked."
})
);
} else {
resolve(decoded);
}
});
set(ctx.state, property, result);
await next();
};
middleware.unless = unless;
middleware.UnauthorizedError = UnauthorizedError;
return middleware;
};
module.exports.UnauthorizedError = UnauthorizedError;
里面会进行decode解密token,然后校验,如果token失效抛出。(以下为源码)
new UnauthorizedError("invalid_token", err)
const decoded = await new Promise((resolve, reject) => {
jwt.verify(token, secret, options, function(err, decoded) {
if (err) {
reject(new UnauthorizedError("invalid_token", err));
} else {
resolve(decoded);
}
});
});
function UnauthorizedError(code, error) {
this.name = "UnauthorizedError";
this.message = error.message;
Error.call(this, error.message);
Error.captureStackTrace(this, this.constructor);
this.code = code;
this.status = 401;
this.inner = error;
}
UnauthorizedError.prototype = Object.create(Error.prototype);
UnauthorizedError.prototype.constructor = UnauthorizedError;
module.exports = UnauthorizedError;
最后接口响应码为401,前端就可以判断然后重新引导用户登录了。 所以说在egg-jwt插件中已经做好了校验。
中间件执行顺序
我们可以自定义应用的中间件,而插件中也会有中间件。那中间件是什么顺序排列的呢。
打印了一下当前项目的中间件,做了分析
最后一个errorHandler是我自定义的中间件,可以发下插件的中间件jwtmiddlewarewrapper是在我们自己定义的中间件之前。
再往前看的中间件都是框架自带的一些中间件。如图:源码中的框架中间件
所以得出中间件的执行顺序:
- 框架中间件
- 插件中间件
- 自定义中间件
📢:框架默认中间件不能被应用层中间件覆盖,如果应用层有自定义同名中间件,在启动时会报错。