【Koa】利用 JWT 实现 token验证

一、JSON Web Token 简介

  JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

  JSON Web Token(JWT)是一种基于JSON的开放标准(RFC 7519),它定义了一种紧凑(compact)且自包含(self-contained)的方式,以JSON对象的形式在各方之间安全地传输信息,而这些信息能够通过数字签名进行验证并予以信任,即可以使用密钥(secret,使用HMAC算法)或以RSA/ECDSA的公钥/私钥对的方式对JWT进行签名。是目前最流行的跨域认证解决方案。

官网Implementation of JWTs

二、使用 node-jsonwebtoken 实现验证

  node-jsonwebtoken 是基于 nodejs 的 JWT 鉴权库,接下来我们将使用它为我们的Koa服务增添token验证的功能。

GitHubJsonWebToken implementation for node.js

(一)、接口详解

磨刀不误砍柴工,我们要先熟悉 node-jsonwebtoken 到底给我们提供了什么


1. token签发 —— jwt.sign()

jwt.sign(payload, secretOrPrivateKey, [options, callback])

其中各项参数详细如下:

  • <payload> :
      作用:携带信息
      接受类型:可有效转化为JSON对象的Object对象、Buffer或String对象
      注:当传入的是Object字面量时,可以在其中指定JWT Claims
      附:JWT Claims 包含:iss(Issuer)、sub(Subject)、aud(Audience)、exp(Expiration Time)、nbf(Not Before)、iat(Issued At)、jti(JWT ID). 其中某些 claim 在接下来的 <options> 参数中还会提到

  • <secretOrPrivateKey> :
      作用:标识签名
      接受类型:Object对象、Buffer对象或String对象
      注:JWT 支持对称加解密和非对称加解密两种方式,若采用前者来加密签名,则直接传入密钥(secret)即可(默认HMAC算法 );若采用后者来加密签名,一种形式可直接传入私钥(privateKey),另一种则是附上自己指定的通行口令(passPhrase),以 { key, passphrase } 对象字面量的形式传入,并且确保要在 <options> 参数中指定算法(algorithm)

  • <options> :
      属性:
       algorithm : 算法(默认为HS256)
       expiresIn : 有效时间,若赋为数值,则默认以秒为单位;若赋为表示时间跨度的字符串,应指定时间单位(例如ms、s、h、d等等),无单位情况下将默认以毫秒为单位(看清楚了,两种情况的默认单位可不一样哦!)
       notBefore : 生效延迟时间(token将在指定时间后才开始生效),若赋为数值,则默认以秒为单位;若赋为表示时间跨度的字符串,应指定时间单位(例如ms、s、h、d等等),无单位情况下将默认以毫秒为单位
       audience : 接收者
       issuer : 签发者
       jwtid : 唯一标识
       subject : 主题
       noTimestamp : 是否禁止添加签发时间(boolean),为 true 将不会为 payload 默认添置 iat 声明
       header : 用于自定义头部信息(Object对象)
       keyid : 暂未搞清楚😁
       mutatePayload : 是否修改 <payload> 引用的对象(boolean),若 <payload> 传入的不是对象字面量,而是一个对象的引用:该参数为 true 时表示将会修改该引用对象(即允许添置默认的 iat 等声明);为 false 则保持引用对象不变。

  • <callback> :
      作用:回调处理
      接受类型:函数
      注:异步情况下传入


2. token验证 —— jwt.verify()

jwt.verify(token, secretOrPublicKey, [options, callback])

其中各项参数详细如下:

  • <token> : 客户端传递来的待验证的 token

  • <secretOrPrivateKey> : 用于验证,注意事项等 同1

  • <options> :
      属性:
       algorithms : 待用的算法列表,例如:[“HS256”, “HS384”]
       audience : 用于检查接收者,可以是字符串或正则表达式
       complete : 是否返回 token 的完整信息,为 true 将返回形式为 { payload, header, signature } 的对象,为 false 则仅返回 payload
       issuer : 用来检查签发者,可以是字符串或字符串数组
       ignoreExpiration : 是否检查token有效时间,为true则不检查
       ignoreNotBefore : 是否检查token的生效延迟时间,为true则不检查
       subject : 用于检查主题,
       clockTolerance : 在检查 expnbf 时可容忍的秒数,用于忽略不同服务器之间存在的微小时差
       maxAge : 允许token有效的最大时限,若赋为数值,则默认以秒为单位;若赋为表示时间跨度的字符串,应指定时间单位(例如ms、s、h、d等等),无单位情况下将默认以毫秒为单位
       clockTimestamp : 用于参照比较的 “当前时间(戳)”
       nonce : 用于检查 nonce 声明

  • <callback> :
      作用:回调处理
      接受类型:函数
      注:异步情况下传入


3. 负载解码 —— jwt.decode()

jwt.decode(token [, options])

在不验证签名是否有效的情况下对负载进行解码并返回,只有同步形式

其中各项参数详细如下:

  • <token> : 客户端传递来的待验证的 token

  • <options> :
      属性:
       json : 是否强制将 payload 转化为JSON格式(即使 header 中未包含 “typ”:“JWT”)
       complete : 是否返回完整的 token信息(此处仅含 payload 和 header)


4. 验证失败的错误类型

好啦,了解完三大函数我们接下来看看 JWT 定义了哪些错误类型:


(1). TokenExpiredError

致错原因:token过期

Error object:

  • name: ‘TokenExpiredError’
  • message: ‘jwt expired’
  • expiredAt: [ExpDate]
(2). JsonWebTokenError

致错原因:token格式错误、token未携带签名、token签名无效、token携带无效aud、token携带无效iss、token携带无效jti、token携带无效sub

Error object:

  • name: ‘JsonWebTokenError’
  • message:
    • ‘jwt malformed’
    • ‘jwt signature is required’
    • ‘invalid signature’
    • ‘jwt audience invalid. expected: [OPTIONS AUDIENCE]’
    • ‘jwt issuer invalid. expected: [OPTIONS ISSUER]’
    • ‘jwt id invalid. expected: [OPTIONS JWT ID]’
    • ‘jwt subject invalid. expected: [OPTIONS SUBJECT]’
(3). NotBeforeError

致错原因:token仍未生效

Error object:

  • name: ‘NotBeforeError’
  • message: ‘jwt not active’
  • date: 2018-10-04T16:10:44.000Z

(二)、安装引用

了解之后,想必各位也跃跃欲试了
首先,安装 jsonwebtoken 并添至生产环境(dependencies):

npm i jsonwebtoken -S

为了更简洁直观地看出 jsonwebtoken 的使用方法,将省略用不到的Koa中间件,服务主程序代码:

const fs = require('fs');
const Koa = require('koa');
const Router = require('koa-router');
const static = require('koa-static');
const jwt = require('jsonwebtoken'); // 引入jsonwebtoken

const app = new Koa();
const router = new Router();

router
    .get('/', ctx => {
        ctx.type = 'html';
        ctx.body = fs.createReadStream('./public/index.html');
    })
    .get('/api', ctx => {
        ctx.type = 'json';
        ctx.body = {
            msg: '你追我,如果你追到我,我就让你...'
        };
    });
    
app
    .use(router.routes())
    .use(router.allowedMethods())
	.use(static(path.join(__dirname, './public')))
	
app.listen(3000, () => {
    console.log('listening on port 3000 ...');
});

及当前项目目录:

|——node_modules/
|——public/
|———libs/
|————jquery-3.4.1.min.js
|———index.html
|——app.js
|——package-lock.json
|——package.json

即在应用 jsonwebtoken 前我们的Koa服务仅仅是提供一个html页面和响应"/api"路由并返回如上所示的json数据

(三)、实现简例

在步骤(二)的基础上继续完善出一个极简的业务场景:


客户端

  • 存在一个按钮可向服务端请求签发token,获取成功后存到本地
  • 存在一个按钮可以删除本地的token
  • 存在一个按钮用于携带token访问(ajax)服务端的 /api 路由

服务端

  • 根路由向客户端提供界面
  • /sign 路由向客户端签发token
  • /api 路由向客户端返回一段JSON数据,但存在 JWT 鉴权

客户端/前端的实现代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JWT in Koa</title>
    <script src="./libs/jquery-3.4.1.min.js"></script>
</head>
<body>
    <button class="gt-btn">get token</button>
    <button class="dt-btn">delete token</button>
    <button class="api-btn">visit /api</button>
    <script>
        const rootUrl = "http://127.0.0.1:3000";
        $(".gt-btn").on("click", () => {
            $.ajax({
                type: "get",
                url: rootUrl+'/sign',
                success (response) {
                    localStorage.setItem("token", response.token);
                }
            });
        });
        $('.dt-btn').on("click", () => {
            localStorage.removeItem("token");
        });
        $('.api-btn').on("click", () => {
            $.ajax({
                type: "get",
                url: rootUrl+'/api',
                beforeSend (request) {
                	/* 将token添至请求头中 */
                    request.setRequestHeader("Authorization", localStorage.getItem('token'));
                }
            });
        });
    </script>
</body>
</html>

服务端的实现代码:

const fs = require('fs');
const path = require('path');
const Koa = require('koa');
const Router = require('koa-router');
const static = require('koa-static');
const jwt = require('jsonwebtoken');

const app = new Koa();
const router = new Router();

router
    .get('/', ctx => {
        ctx.type = 'html';
        ctx.body = fs.createReadStream('./public/index.html');
    })
    .get('/sign', ctx => {
        /* 要在token中携带的信息 */
        let payload = {name: 'morilence'};
        /* 签发token,此处采用对称加解密来加密签名,密钥为: 'fgnb' */
        const token = jwt.sign(payload, 'fgnb', {
            notBefore: 30, // token将在签发的30s后才开始生效
            expiresIn: 90 // token的有效时间为90s
        });
        ctx.type = 'json';
        ctx.body = {
            token
        };
    })
    .use('/api', (ctx, next) => { // 自定针对 /api 路由进行处理的中间件
        /* 从请求头中取出客户端携带的token */
        const clientToken = ctx.headers.authorization;
        let decoded = null;
        /* 采用jwt.verify()函数的同步形式,函数返回解码后的内容 */
        try {
            decoded = jwt.verify(clientToken, 'fgnb');
            console.log(decoded);
            /* 验证成功 */
            next();
        } catch (err) {
            console.log(err.name+': '+err.message);
            /* 捕获错误即已说明无权,抛出401 */
            ctx.throw(401);
        }
    })
    .get('/api', ctx => {
        ctx.type = 'json';
        ctx.body = {
            msg: '你追我,如果你追到我,我就让你...'
        };
    })

app
    .use(router.routes())
    .use(router.allowedMethods())
    .use(static(path.join(__dirname, './public')))

app.listen(3000, () => {
    console.log('listening on port 3000 ...');
});

可以看出,代码关键无非就是为存在身份验证需求的路由添加前置中间件,在中间件中利用 node-jsonwebtoken 进行JWT的鉴权操作:验证成功,就将请求继续传递给目标路由;验证失败则拦截并抛出401错误,意表无权访问。如果结合其它 node-jsonwebtoken 的接口进一步对该中间件进行拓展封装,一定能打造出你自己的鉴权利器!😏

三、借助 koa-jwt 中间件简化验证

  当然,如果说你的业务场景没那么复杂或是对鉴权流程操控度的要求没那么高,不妨采用封装好的 koa-jwt 中间件来方便自己进行token验证

GitHub

首先来安装 koa-jwt:

npm i koa-jwt -S

引用:

const koaJwt = require('koa-jwt');

(一)、局部指定路由鉴权

用 koa-jwt 中间件来实现步骤 二-(三) 示例的同样效果(注意:koa-jwt 只是用来简化验证,token的签发还要用到 node-jsonwebtoken):

const fs = require('fs');
const path = require('path');
const Koa = require('koa');
const Router = require('koa-router');
const static = require('koa-static');
const jwt = require('jsonwebtoken');
const koaJwt = require('koa-jwt');

const app = new Koa();
const router = new Router();

router
    .get('/', ctx => {
        ctx.type = 'html';
        ctx.body = fs.createReadStream('./public/index.html');
    })
    .get('/sign', ctx => {
        let payload = {name: 'morilence'};
        const token = jwt.sign(payload, 'fgnb', {
            notBefore: 30,
            expiresIn: 90
        });
        ctx.type = 'json';
        ctx.body = {
            token
        };
    })
    /* 只需把koaJwt中间件写在涉及具体业务逻辑处理的中间件之前就ok啦! */
    .get('/api', koaJwt({secret: 'fgnb'}), ctx => { // 参数对象指定密钥
        ctx.type = 'json';
        ctx.body = {
            msg: '你追我,如果你追到我,我就让你...'
        };
    })

app
    .use(router.routes())
    .use(router.allowedMethods())
    .use(static(path.join(__dirname, './public')))

app.listen(3000, () => {
    console.log('listening on port 3000 ...');
});

要注意的是,koa-jwt中间件对请求头的Authorization有默认的格式要求:Bearer ${token},所以我们还要把前端发送token部分的代码改为:

beforeSend (request) {
	request.setRequestHeader("Authorization", "Bearer " + localStorage.getItem('token'));
}

到这就可以实现步骤 二-(三) 示例的同样效果了

(二)、全局配置路由鉴权

上述写法是局部地进行添加鉴权,哪个路由需要,就对该路由添加 koa-jwt 中间件,不过还有一种全局的写法:

const fs = require('fs');
const path = require('path');
const Koa = require('koa');
const Router = require('koa-router');
const static = require('koa-static');
const jwt = require('jsonwebtoken');
const koaJwt = require('koa-jwt');

const app = new Koa();
const router = new Router();

router
    .get('/', ctx => {
        ctx.type = 'html';
        ctx.body = fs.createReadStream('./public/index.html');
    })
    .get('/sign', ctx => {
        let payload = {name: 'morilence'};
        const token = jwt.sign(payload, 'fgnb', {
            notBefore: 0,
            expiresIn: 90
        });
        ctx.type = 'json';
        ctx.body = {
            token
        };
    })
    .get('/api', ctx => {
        ctx.type = 'json';
        ctx.body = {
            msg: '你追我,如果你追到我,我就让你...'
        };
    })

app
	/* 需要通过正则表达式列表表指定哪些路由不应用鉴权(此处指定为不以/api开头的路由) */
    .use(koaJwt({secret: 'fgnb'}).unless({path: [/^(?!\/api)/]}))
    .use(router.routes())
    .use(router.allowedMethods())
    .use(static(path.join(__dirname, './public')))

app.listen(3000, () => {
    console.log('listening on port 3000 ...');
});

四、结语

  好啦,Koa 关于 JWT 的部分就暂且到这。其实你们也能发现,关于 koa-jwt 中间件的用法与文档我并没有深入介绍。首要原因是个人觉得 node-jsonwebtoken 使用起来自由度更高,可以定制出更适合自己的鉴权中间件,其次才是太累了不想再研究官方文档了👀…
  最后,如果发现文中有错误或表述不当的地方还请私信指出(尤其是 node-jsonwebtoken 的接口详解部分),各位下期再见!

  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值