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进行签名。是目前最流行的跨域认证解决方案。
二、使用 node-jsonwebtoken 实现验证
node-jsonwebtoken 是基于 nodejs 的 JWT 鉴权库,接下来我们将使用它为我们的Koa服务增添token验证的功能。
(一)、接口详解
磨刀不误砍柴工,我们要先熟悉 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 : 在检查 exp 和 nbf 时可容忍的秒数,用于忽略不同服务器之间存在的微小时差
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 =