Koa 应用程序是一个包含一组中间件函数的对象,它是按照相似堆栈的方式组织和执行的。git
当一个中间件调用 next()
则该函数暂停并将控制传递给定义的下一个中间件。当在下游没有更多的中间件执行后,堆栈将展开而且每一个中间件恢复执行其上游行为。web
以上两句话,是我在官方文档中找到其对 Koa 中间件的描述。npm
在Koa中,中间件是一个颇有意思的设计,它处于request和response中间,被用来实现某种功能。像上篇文章所使用的 koa-router 、koa-bodyparser 等都是中间件。json
可能有些人喜欢把中间件理解为插件,但我以为它们二者并非同一种概念的东西。插件像是一个独立的工具,而中间件更像是流水线,将加工好的材料继续传递下一个流水线。因此中间件给个人感受更灵活,能够像零件同样自由组合。数组
单看中间件有堆栈执行顺序的特色,二者就出现质的区别。安全
这张图是 Koa 中间件执行顺序的图示,被称为“洋葱模型”。app
中间件按照栈结构的方式来执行,有“先进后出“的特色。koa
一段简单的代码来理解上图:异步
app.use(async (ctx, next)= console.log('--> 1') next() console.log('<-- 1') }) app.use(async (ctx, next)=>{ console.log('--> 2') //这里有一段异步操做
await new Promise((resolve)=>{ .... }) await next() console.log('<-- 2') }) app.use(async (ctx, next)=>{ console.log('--> 3') next() console.log('<-- 3') })
app.use(async (ctx, next)=>{ console.log('--> 4') })
当咱们运行这段代码时,获得如下结果async
--> 1
--> 2
--> 3
--> 4
<-- 3
<-- 2
<-- 1
中间件经过调用 next 一层层执行下去,直到没有执行权能够继续传递后,在以冒泡的形式原路返回,并执行 next 函数以后的行为。能够看到 1 第一个进去,倒是最后一个出来,也体现出中间件栈执行顺序的特色。
在第二个中间件有一段异步操做,因此要加上await,让执行顺序按照预期去进行,不然可能会出现一些小问题。
1.应用中间件
const Koa = require('koa'); const Router = require('koa-router'); const app = new Koa(); const router = new Router(); app.use(async (ctx,next)=>{ console.log(new Date()); await next(); }) router.get('/', function (ctx, next) { ctx.body="Hello koa"; }) router.get('/news',(ctx,next)=>{ ctx.body="新闻页面" }); app.use(router.routes()); //做用:启动路由 app.use(router.allowedMethods()); //做用: 当请求出错时的处理逻辑 app.listen(3000,()=>{ console.log('starting at port 3000'); });
2.路由中间件
router.get('/', async(ctx, next)=>{ console.log(1) next() }) router.get('/', function (ctx) { ctx.body="Hello koa"; })
3.错误处理中间件
app.use(async (ctx,next)=> { next(); if(ctx.status==404){ ctx.status = 404; ctx.body="这是一个404页面" } });
const bodyParser = require('koa-bodyparser');
app.use(bodyParser());
实现一个基于 jsonwebtoken 验证token的中间件,这个中间件由两个文件组成 extractors.js 、index.js,并放到check-jwt文件夹下。
生成token
const Router = require('koa-router') const route = new Router() const jwt = require('jsonwebtoken') route.get('/getToken', async (ctx)=>{ let {name,id} = ctx.query if(!name && !id){ ctx.body = { msg:'不合法', code:0 } return } //生成token
let token = jwt.sign({name,id},'secret',{ expiresIn: '1h' }) ctx.body = { token: token, code:1 } }) module.exports = route
使用 jwt.sign 生成token:
第一个参数为token中携带的信息;
第二个参数为key标识(解密时须要传入该标识);
第三个为可选配置选项,这里我设置过时时间为一小时;
详细用法能够到npm上查看。
使用中间件
app.js:
const {checkJwt,extractors} = require('./check-jwt') app.use(checkJwt({ jwtFromRequest: extractors.fromBodyField('token'),
secretOrKeyL: 'secret', safetyRoutes: ['/user/getToken'] }))
是否必选 | 接收类型 | 备注 | |
jwtFromRequest | 否 | 函数 |
默认验证 header 的
authorization
extractors提供的提取函数,支持get、post、header方式提取
这些函数都接收一个字符串参数(须要提取的key)
对应函数:
fromUrlQueryParameter、
fromBodyField、
fromHeader
|
secretOrKey | 是 | 字符串 | 与生成token时传入的标识保持一致 |
safetyRoutes | 否 | 数组 | 不须要验证的路由 |
使用该中间件后,会对每一个路由都进行验证
路由中获取token解密的信息
route.get('/getUser', async ctx=>{ let {name, id} = ctx.payload ctx.body = { id, name, code:1 } })
经过ctx.payload来获取解密的信息
实现代码
extractors.js 工具函数(用于提取token)
let extractors = {} extractors.fromHeader = function(header_name='authorization'){ return function(ctx){ let token = null, request = ctx.request; if (request.header[header_name]) { token = header_name === 'authorization' ? request.header[header_name].replace('Bearer ', '') : request.header[header_name]; }else{ ctx.body = { msg: `${header_name} 不合法`, code: 0 } } return token; } } extractors.fromUrlQueryParameter = function(param_name){ return function(ctx){ let token = null, request = ctx.request; if (request.query[param_name] && Object.prototype.hasOwnProperty.call(request.query, param_name)) { token = request.query[param_name]; }else{ ctx.body = { msg: `${param_name} 不合法`, code: 0 } } return token; } } extractors.fromBodyField = function(field_name){ return function(ctx){ let token = null, request = ctx.request; if (request.body[field_name] && Object.prototype.hasOwnProperty.call(request.body, field_name)) { token = request.body[field_name]; }else{ ctx.body = { msg: `${field_name} 不合法`, code: 0 } } return token; } } module.exports = extractors
index.js 验证token
const jwt = require('jsonwebtoken') const extractors = require('./extractors') /** * * @param {object} options * @param {function} jwtFromRequest * @param {array} safetyRoutes * @param {string} secretOrKey */
function checkJwt({jwtFromRequest,safetyRoutes,secretOrKey}={}){ return async function(ctx,next){ if(typeof safetyRoutes !== 'undefined'){ let url = ctx.request.url //对安全的路由 不验证token
if(Array.isArray(safetyRoutes)){ for (let i = 0, len = safetyRoutes.length; i < len; i++) { let route = safetyRoutes[i], reg = new RegExp(`^${route}`);
//若匹配到当前路由 则直接跳过 不开启验证
if(reg.test(url)){ return await next() } } }else{ throw new TypeError('safetyRoute 接收类型为数组') } } if(typeof secretOrKey === 'undefined'){ throw new Error('secretOrKey 为空') } if(typeof jwtFromRequest === 'undefined'){ jwtFromRequest = extractors.fromHeader() } let token = jwtFromRequest(ctx) if(token){ //token验证
let err = await new Promise(resolve=>{ jwt.verify(token, secretOrKey,function(err,payload){ if(!err){ //将token解码后的内容 添加到上下文
ctx.payload = payload } resolve(err) }) }) if(err){ ctx.body = { msg: err.message === 'jwt expired' ? 'token 过时' : 'token 出错', err, code:0 } return } await next() } } } module.exports = { checkJwt, extractors }