Koa - 中间件(理解中间件、实现一个验证token中间件)

时间:2022-07-23
本文章向大家介绍Koa - 中间件(理解中间件、实现一个验证token中间件),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言

Koa 应用程序是一个包含一组中间件函数的对象,它是按照类似堆栈的方式组织和执行的。

当一个中间件调用 next() 则该函数暂停并将控制传递给定义的下一个中间件。当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件恢复执行其上游行为。

以上两句话,是我在官方文档中找到其对 Koa 中间件的描述。

在Koa中,中间件是一个很有意思的设计,它处于request和response中间,被用来实现某种功能。像上篇文章所使用的 koa-router 、koa-bodyparser 等都是中间件。

可能有些人喜欢把中间件理解为插件,但我觉得它们两者并不是同一种概念的东西。插件像是一个独立的工具,而中间件更像是流水线,将加工好的材料继续传递下一个流水线。所以中间件给我的感觉更灵活,可以像零件一样自由组合。

单看中间件有堆栈执行顺序的特点,两者就出现质的区别。

中间件的概念

这张图是 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')
})  

当我们运行这段代码时,得到以下结果

--> 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页面"
    }
});

4.第三方中间件

const bodyParser = require('koa-bodyparser');
app.use(bodyParser());

实现验证token中间件

实现一个基于 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
}

Demo: https://gitee.com/ChanWahFung/koa-demo