Koa - 中間件(理解中間件、實現一個驗證token中間件)

前言

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

 

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

相關文章
相關標籤/搜索