前言 Koa 應用程式是一個包含一組中間件函數的對象,它是按照類似堆棧的方式組織和執行的。 當一個中間件調用 next() 則該函數暫停並將控制傳遞給定義的下一個中間件。當在下游沒有更多的中間件執行後,堆棧將展開並且每個中間件恢復執行其上游行為。 以上兩句話,是我在官方文檔中找到其對 Koa 中間件 ...
前言
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')
})
app.use(async (ctx, next)=>{
console.log('--> 4')
})
當我們運行這段代碼時,得到以下結果
--> 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