Fork me on GitHub

koa-jwt 实现自定义排除动态路由的鉴权

# 场景描述

🍭 最近在编写 PPAP.server 项目,一个基于 koa2nodejs 服务端接口程序。
由于接口采用的是 RESTful API,所以鉴权令牌由客户端携带发送到接口。
业务需求的是部分接口是需要用户登陆再进行操作,比如需要记录用户点赞的接口。
而不需要用户鉴权的接口,如查看帖子等不记录用户数据的接口。

对于路由权限控制(鉴权),项目使用的是 koa-jwt,支持对 token 的生成与校验,还能对接口路由进行过滤排除,指定不需要鉴权的接口。

如:

1
2
3
4
5
6
7
//配置不需要jwt验证的接口
app.use(jwtKoa({ secret: tokenUtil.secret }).unless({
path: [
'/user/login',
'/user/register'
]
}));

这样上面两个接口 /user/login/user/register 都是可以跳过鉴权的,不需要携带 token

对于本项目来说,棘手的是项目接口大多使用了动态路由,即比如 /user/:id 这样的接口,需要用正则表达式去进行匹配。
但是动态路由 /user/:id 的请求方法可能会有 get post put delete 四种,所以不仅仅要排除配置的静态路由,还需要排除配置的特定请求方法的动态路由。

在阅读 koa-jwt 源码后,发现 koa-jwtunless 方法调用了 koa-unless 这个包,于是去阅读了 koa-unless 之后,发现可配置以下参数:

1
2
3
4
5
- method 它可以是一个字符串或字符串数组。如果请求方法匹配,则中间件将不会运行。
- path 它可以是字符串,正则表达式或其中任何一个的数组。如果请求路径匹配,则中间件将不会运行。
- ext 它可以是一个字符串或字符串数组。如果请求路径以这些扩展名之一结尾,则中间件将不会运行。
- custom 它必须是一个返回 true/ 的函数 false。如果函数针对给定的请求返回 true,则中间件将不会运行。该功能将通过 this 访问 Koa 的上下文
- useOriginalUrl 应该为 truefalse,默认为 true。如果为false,path 则匹配 this.url 而不是 this.originalUrl。

结合项目的实际情况,解决方法只能是使用 custom 配置自定义函数进行判断。

# 解决方法

🍭 使用 custom 自定义函数进行过滤,创建文件 jwt_unless.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 用于判断客户端当前请求接口是否需要jwt验证
*/

//定义不需要jwt验证的接口数组(get方法)
const nonTokenApiArr = [
'/',
'/post'
]

//定义不需要jwt验证的接口正则数组(get方法)
const nonTokenApiRegArr = [
/^\/user\/\d/,
/^\/post\/\d/
]

//判断请求api是否在数组里
const isNonTokenApi = (path) => {
return nonTokenApiArr.includes(path)
}

//判断请求api是否在正则数组里
const isNonTokenRegApi = (path) => {
return nonTokenApiRegArr.some(p => {
return (typeof p === 'string' && p === path) ||
(p instanceof RegExp && !! p.exec(path))
});
}

//判断当前请求api是否不需要jwt验证
const checkIsNonTokenApi = (ctx) => {
if((isNonTokenApi(ctx.path) || isNonTokenRegApi(ctx.path)) && ctx.method == 'GET'){
return true
}else{
//特殊post接口,不需要验证jwt
if(ctx.path == '/user/login' || ctx.path == 'user/register'){
return true
}
return false
}
}

module.exports = {
nonTokenApiArr,
nonTokenApiRegArr,
isNonTokenApi,
isNonTokenRegApi,
checkIsNonTokenApi
}

然后在 app.js 里引入 jwt_unless.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const jwtKoa = require('koa-jwt') // 用于路由权限控制
const app = new Koa()

const config = require('./config/config')

const tokenUtil = require('./util/token')
const router = require('./router')

const jwtUnless = require('./util/jwt_unless') //用于判断是否需要jwt验证

//配置ctx.body解析中间件
app.use(bodyParser())

// 错误处理
app.use((ctx, next) => {
//设置CORS跨域
ctx.set("Access-Control-Allow-Origin", "*")
ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE")
ctx.set("Access-Control-Allow-Headers", "x-requested-with, accept, origin, content-type, Authorization")
ctx.set("Content-Type", "application/json;charset=utf-8")
ctx.set("Access-Control-Expose-Headers", "new_token")
//获取token,保存全局变量
if(ctx.request.header.authorization){
global.token = ctx.request.header.authorization.split(' ')[1]
//检测当前token是否到达续期时间段
let obj = tokenUtil.parseToken()
//解析token携带的信息
global.uid = obj.uid
global.name = obj.name
global.account = obj.account
global.email = obj.email
global.roleId = obj.roleId
//先解析全局变量再执行next(),保证函数实时获取到变量值
}
return next().then(() => {
//执行完下面中间件后进入
//判断不需要jwt验证的接口,跳过token续期判断
if(jwtUnless.checkIsNonTokenApi(ctx)) return
//判断token是否应该续期(有效时间)
if(tokenUtil.getTokenRenewStatus()){
//设置header
ctx.set({
new_token: tokenUtil.createNewToken()
})
}
}).catch((err) => {
//携带token的Authorization参数错误
if(err.status === 401){
ctx.status = 200
ctx.body = {
status: 401,
message: '未携带token令牌或者token令牌已过期'
}
}else{
throw err
}
})
})

//配置不需要jwt验证的接口
app.use(jwtKoa({ secret: tokenUtil.secret }).unless({
//自定义过滤函数,详细参考koa-unless
custom: ctx => {
if(jwtUnless.checkIsNonTokenApi(ctx)){
//是不需要验证token的接口
return true
}else{
//是需要验证token的接口
return false
}
}
}));

//初始化路由中间件
app.use(router.routes()).use(router.allowedMethods())

//监听启动窗口
app.listen(config.port, () => console.log(`PPAP.server is run on ${config.host}:${config.port}`))

到此就实现了对静态及动态路由鉴权,以及对 token 有效时间续期的判断。
以上的示例针对的是 get 方法动态路由的判断,如果限制除了 get 请求外的多个请求方法,则需要在定义正则数组的时候,将请求方法跟正则表达式对应起来,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const nonTokenApiRegArr = [
{
path: /^\/user\/\d/,
method: "GET"
},
{
path: /^\/user\/\d/,
method: "POST"
},
]
……
……
……
//判断请求api是否在正则数组里
const isNonTokenRegApi = (path, method) => {
return nonTokenApiRegArr.some(p => {
return (typeof p.path === 'string' && p.path === path && p.method === method) ||
(p.path instanceof RegExp && !! p.path.exec(path) && p.method === method)
});
}
//判断当前请求api是否不需要jwt验证
const checkIsNonTokenApi = (ctx) => {
if(isNonTokenApi(ctx.path) || isNonTokenRegApi(ctx.path, ctx.method)){
return true
}else{
return false
}
}

更多详细请访问 https://github.com/ppap6/PPAP.server

0%
            [00:06.59]呼んでいる 胸のどこか奥で
            [00:12.06]いつも心踊る 夢を見たい
            [00:18.09]かなしみは 数えきれないけれど
            [00:23.87]その向こうできっと あなたに会える
            [00:29.40]
            [00:31.81]繰り返すあやまちの そのたび ひとは
            [00:38.20]ただ青い空の 青さを知る
            [00:43.33]果てしなく 道は続いて見えるけれど
            [00:50.92]この両手は 光を抱ける
            [00:54.89]
            [00:57.26]さよならのときの 静かな胸
            [01:02.80]ゼロになるからだが 耳をすませる
            [01:08.60]生きている不思議 死んでいく不思議
            [01:14.53]花も風も街も みんなおなじ
            [01:20.50]
            [01:22.40]nananan lalala lululu
            [01:47.63]呼んでいる 胸のどこか奥で
            [01:53.56]いつも何度でも 夢を描こう
            [01:59.14]かなしみの数を 言い尽くすより
            [02:05.16]同じくちびるで そっとうたおう
            [02:11.48]
            [02:12.96]閉じていく思い出の そのなかにいつも
            [02:19.31]忘れたくない ささやきを聞く
            [02:24.47]こなごなに砕かれた 鏡の上にも
            [02:31.12]新しい景色が 映される
            [02:35.89]
            [02:38.27]はじまりの朝の静かな窓
            [02:43.72]ゼロになるからだ 充たされてゆけ
            [02:49.70]海の彼方には もう探さない
            [02:55.66]輝くものは いつもここに
            [03:01.66]わたしのなかに 見つけられたから
            [03:08.20]
            [03:14.13]nananan lalala lululu
        
            [by:嘶哑音符]
            [00:00.12]二人の間 通り過ぎた風は
            [00:07.15]どこから寂しさを運んできたの
            [00:13.66]泣いたりしたそのあとの空は
            [00:19.82]やけに透き通っていたりしたんだ
            [00:26.02]music
            [00:37.03]いつもは尖ってた父の言葉が
            [00:42.82]今日は暖かく感じました
            [00:48.47]優しさも笑顔も夢の語り方も
            [00:54.07]知らなくて全部 君を真似たよ
            [00:59.48]もう少しだけでいい あと少しだけでいい
            [01:05.39]もう少しだけでいいから
            [01:11.20]もう少しだけでいい あと少しだけでいい
            [01:16.97]もう少しだけ くっついていようか
            [01:21.11]music
            [01:25.69]僕らタイムフライヤー 時を駆け上がるクライマー
            [01:30.74]時のかくれんぼ はぐれっこはもういやなんだ
            [01:37.51]嬉しくて泣くのは 悲しくて笑うのは
            [01:41.74]君の心が 君を追い越したんだよ
            [01:46.78]music
            [02:08.22]星にまで願って 手にいれたオモチャも
            [02:14.10]部屋の隅っこに今 転がってる
            [02:19.83]叶えたい夢も 今日で100個できたよ
            [02:25.53]たった一つといつか 交換こしよう
            [02:31.26]music
            [02:37.14]いつもは喋らないあの子に今日は
            [02:42.95]放課後「また明日」と声をかけた
            [02:48.50]慣れないこともたまにならいいね
            [02:54.25]特にあなたが 隣にいたら
            [02:59.69]もう少しだけでいい あと少しだけでいい
            [03:05.39]もう少しだけでいいから
            [03:11.09]もう少しだけでいい あと少しだけでいい
            [03:16.69]もう少しだけくっついていようよ
            [03:21.02]music
            [03:25.75]僕らタイムフライヤー 君を知っていたんだ
            [03:30.42]僕が 僕の名前を 覚えるよりずっと前に
            [03:43.62]君のいない 世界にも 何かの意味はきっとあって
            [03:49.00]でも君のいない 世界など 夏休みのない 八月のよう
            [03:55.15]君のいない 世界など 笑うことない サンタのよう
            [04:00.60]君のいない 世界など
            [04:08.70]music
            [04:34.00]僕らタイムフライヤー 時を駆け上がるクライマー
            [04:39.32]時のかくれんぼ はぐれっこはもういやなんだ
            [04:46.03]なんでもないや やっぱりなんでもないや
            [04:50.46]今から行くよ
            [04:55.00]僕らタイムフライヤー 時を駆け上がるクライマー
            [04:59.98]時のかくれんぼ はぐれっこ はもういいよ
            [05:06.45]君は派手なクライヤー その涙 止めてみたいな
            [05:11.34]だけど 君は拒んだ 零れるままの涙を見てわかった
            [05:18.08]嬉しくて泣くのは 悲しくて 笑うのは
            [05:22.51]僕の心が 僕を追い越したんだよ
            [05:27.55]end
        
            [ti:Down by the salley gardens]
            [ar:藤田惠美]
            [al:挪威甘菊]
            [by:]
            [00:12.00]Down by the salley gardens my love and I did meet
            [00:25.00]She passed the salley gardens with little snow-white feet
            [00:38.00]She did me take love easy, as the leaves grow on the tree
            [00:50.00]But I, being young and foolish, with her would not agree
            [01:04.00]
            [01:15.00]In a filed by the river my love and I did stand
            [01:28.00]And on my leaning shoulder she laid her snows-white hand
            [01:40.00]She bid me take life easy, as the grass grows on the weirs
            [01:52.00]But I was young and foolish, and now I am full of tears
            [02:06.00]
            [02:54.00]Down by the salley gardens my love and I did meet
            [03:05.00]She passed the salley gardens with little snow-white feet
            [03:18.00]She did me take love easy, as the leaves grow on the tree
            [03:30.00]But I, being young and foolish, with her would not agree
        
            [00:11.860]夜明けまであと一時間 もうそろそろ行こう
            [00:23.120]聞こえるのは眠る君のかすかな寝息だけ
            [00:34.240]
            [00:37.710]目を閉じた君の横顔 とてもきれいだよ
            [00:49.440]さよなら 君の耳元にそっと囁いた
            [01:00.790]
            [01:01.480]ああ、僕は君を置いて 今ここを出て行く
            [01:12.970]外は雨 音もなく 僕の頬を濡らす
            [01:24.330]
            [01:36.410]君と出会ったのはたった半年前のこと
            [01:48.080]もうずいぶん前のことのような気がする
            [01:59.460]
            [01:59.900]今思えば僕らろくに話もしなかった
            [02:11.220]时间はいつも余るほどあったはずなのに
            [02:22.640]
            [02:23.090]ああ、僕は君を置いて 今ここを出て行く
            [02:34.740]外の雨は僕の涙、静かに降り続く
            [02:46.870]外の雨は僕の涙、静かに降り続く
            [02:59.480]
        
            [00:07.694]灯りを消したまま話を続けたら
            [00:19.901]ガラスの向こう側で星がひとつ消えた
            [00:26.559]からまわりしながら通りを駆け抜けて
            [00:37.816]砕けるその時は君の名前だけ呼ぶよ
            [00:45.374]広すぎる霊園のそばの このアパートは薄ぐもり
            [00:56.155]暖かい幻を見てた
            [01:04.441]猫になりたい 君の腕の中
            [01:13.254]寂しい夜が終わるまでここにいたいよ
            [01:22.621]猫になりたい 言葉ははかない
            [01:31.369]消えないようにキズつけてあげるよ
            [01:47.999]目を閉じて浮かべた密やかな逃げ場所は
            [01:57.880]シチリアの浜辺の絵ハガキとよく似てた
            [02:05.646]砂ぼこりにまみれて歩く 街は季節を嫌ってる
            [02:15.629]つくられた安らぎを捨てて
            [02:23.888]猫になりたい 君の腕の中
            [02:32.579]寂しい夜が終わるまでここにいたいよ
            [02:41.912]猫になりたい 言葉ははかない
            [02:50.362]消えないようにキズつけてあげるよ
        
            [by:最短距離]
            [00:08.40]落し物をしたのね そんな曇りの日に
            [00:15.78]プラネタリュウムに行って 重いドアを閉じましょうよ
            [00:23.28]いつのまに落としたの それさえわからなくて
            [00:30.55]涙をもう落とせない なら星見上げましょうよ
            [00:38.12]何がほんと作り事 答えは欲しくないけど
            [00:46.23]あなたが言う言葉は本当に したいよ
            [00:57.77]落し物をしたのね そんな曇りの日に
            [01:05.20]プラネタリュウムに行って 重いドアを閉じましょうよ
            [01:12.71]魔法信じれないけど 魔法みたいな力を
            [01:19.74]信じたくってこうして こんな星を見上げるんだ
            [01:26.89]差しわされた あなたの手を
            [01:30.27]私は今 ほら 握れなくて
            [01:34.12]通り過ぎてく 遠い答えを
            [01:37.72]言いたくても ほら言い出せない
            [01:57.90]
            [01:57.96]落し物をしたのね あなたを信じたことを
            [02:05.25]プラネタリュウム出るから 私のドア開けましょうよ
            [02:11.69]私のドア開けましょうよ
            [02:15.29]私のドア開けましょうよ
            [02:19.60]
            [02:19.67]はい!
            [02:21.60]はい!
            [02:23.47]はい!
            [02:24.89]はい!
            [02:26.91]はい!
            [02:28.74]はいはい!
            [02:30.97]はい!
            [02:32.40]はい!