koa-jsonwebtoken 模块学习笔记
一个JSON Web令牌的实现。它基于 draft-ietf-oauth-json-web-token-08 鉴权方案。模块使用了 node-jws模块.
模块的安装:
npm install jsonwebtoken
jwt.sign 生成token
函数语法
jwt.sign(payload, secretOrPrivateKey,[options, callback])
(异步) 如果提供了回调,则使用 err 或者 JWT 来调用回调函数。
(同步) 返回一个 返回字符串形式的 JsonWebToken
参数说明:
-
payload Payload可以是表示有效 JSON 的对象文本、缓冲区或字符串。请注意,
exp或任何其他声明是在payload对象文本状态下进行设置的。不检查缓冲区或字符串的JSON有效性。如果有效负载不是缓冲区或字符串,则使用 JSON.stringify 将其强制为字符串。
-
secretOrPrivateKey是一个字符串、缓冲区或对象,其中包含HMAC算法的密钥或RSA和ECDSA的PEM编码的私钥。对于带有密码短语的私钥,可以使用对象{ key,passphrase }(基于加密文档) ,在这种情况下,请确保传递算法选项。 -
options:algorithm加密的算法,默认情况下:(default:HS256)expiresIn日期证书有效期跨度 以秒或描述时间跨度zeit/ms的字符串表示. (相关语法在下面)例如: 60,“2 days”,“10h”,“7d”。数值被解释为秒计数。如果使用字符串,请确保提供时间单位(days、hours等) ,否则默认使用的是毫秒单位(“120”等于“120毫秒”)。
notBefore日期证书有效期开始 以秒或描述时间跨度zeit/ms的字符串表示。audience受众issuer颁发者jwtidJWT的序号subject主题noTimestamp不自动设置生成时间header设置token头对象(encode后可见)KeyID密钥id (解码后在header对象中)mutatePayload如果为真,option 标志设置值 将直接修改有效负载对象。如果在声明应用到有效负载之后,但是在将其编码到令牌之前,需要对有效负载进行原始引用,那么这种方法很有用。
expiresIn、notBefore、audience、subject、issuer没有默认值。这些声明也可以分别通过exp、nbf、aud、sub和iss直接在有效负载中提供,但不能同时包含在这两个地方。
注意: exp、 nbf 和 iat 是 NumericDate类型,详情在下面的ExpiresIn令牌过期声明中有介绍。
可以通过 options.header 对象定制头文件。
除非指定 noTimestamp=true,否则默认情况下生生成的jwts将包括一个 iat(签发时间)声明, 如果在payload中插入 iat属性,计算jwts有效期将使用iat所提供的时间,而不是使用现行的时间来进行某些计算。 例如: expiresIn 中给定的exp到期时间,则该jwts有效期将截止 exp = expiresIn + iat。
附: zeit/ms语法
ms('2 days') // 2天
ms('1d') // 1天
ms('10h') // 10小时
ms('2.5 hrs') // 2.5小时
ms('2h') // 2小时
ms('1m') // 1分钟
ms('5s') // 5秒
ms('1y') // 一年
ms('-3 days') // 回溯3天
ms('-1h') // 回溯1小时
同步、默认加密方式生成
默认的同步签名(HMAC SHA256):
const jwt = require('jsonwebtoken');
let token = jwt.sign({foo: 'bar'}, 'key'); // 未指定 algorithm 加密方式 默认使用SHA256
console.log(token);
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE2NTE2MTA3NDF9.997lm8VWbEUGs6Sxsd3WzkkT2P9UYva-hn3S7dGv8Yk
同步、指定加密方式生成
使用 RSA SHA256 进行同步签名:
首先要有一个RSA的公私秘钥对,使用 git生成一个密钥对:
openssl genrsa -out pri_key.pem 2048 // 2048长度可以适当调节
openssl rsa -in pri_key.pem -pubout -out pub_key.pem
const jwt = require('jsonwebtoken');
const fs = require('fs');
const path = require('path');
const privateKey = fs.readFileSync(path.join(__dirname, 'private.key'));
let token = jwt.sign({ foo: 'bar' }, privateKey, {algorithm: 'RS256'}); // 指定 algorithm 加密方式
console.log(token);
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE2NTE2MTIwODZ9.OXz9dL0L7h72QrO9Smdc23HuPh5AAZZ_6W8ujEDk6SVKxcau8HZUvANrpxMuGsK1YhdQcYHSYB3NeoKOz_6ayffuvhLzTc9-HC-Av5YcaDwjpXPkVvl5046N7bpdWp0GHGDKS3Vhm59DL0QDsm6Bo_Fhm9uT3_Ht1QKa7tq9SUiJJwh5frFMgFKv1W4PB2DDM5lmpAxd53o7ZP0i6qQpO-1j5pU2dFlcvzcWdN99OzgY68Jfhsz3UOr6sdfJ0hTK3SWnZbtaG38sIJZezZ0McERuWcm-08UXWcmhNZ9C6bmAAMUvt0gSX1TyMR19_cT7aoDq-xbmCJklK6txz9q76A
异步生成
传递一个回调函数,使其异步生成:
const jwt = require('jsonwebtoken');
let token = jwt.sign({foo: 'bar'}, 'fn', function(err, token) // 回调函数
{
console.log(token)
});
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE2NTE2MTI0MzR9.04f2T1-Ib0h9H3jbRsBV8Ne5OGjPIZC2mzHs0m2uqIc
回溯30秒生成
const jwt = require('jsonwebtoken');
const older_token = jwt.sign({ foo: 'bar', iat: Math.floor(Date.now() / 1000) - 30 }, 'sh');
// 在 payload 中指定 iat 属性,计算该 jwts 的有效时间将不再使用 现行时间 与 expiresIn 来计算 有效期限.
console.log(older_token);
令牌过期时间(exp声明)
JWT 的标准定义了exp过期声明。到期表示为NumericDate: (一个 JSON 数值,表示从 1970-01-01T00:00:00Z UTC 到指定 UTC 日期/时间的秒数), 这意味着该exp字段应包含自纪元以来的秒数。
例如,签署一个有效期为1小时的token:
jwt.sign({
exp: Math.floor(Date.now() / 1000) + (60 * 60), // 现行时间戳/1000 + 1小时的秒数
data: 'foobar'
}, 'secret');
这等价于使用 option.expiresIn 签署有效期为1小时的token:
jwt.sign({data: 'foobar'}, 'secret', {expiresIn: 60 * 60});
// 使用现行时间进行计算 expiresIn 时间跨度为 1小时秒数
或使用 ms语法:
jwt.sign({data: 'foobar'}, 'secret', {expiresIn: '1h'});
payload属性与option参照表
| payload属性 | option选项 | 功能 | 区别 |
|---|---|---|---|
| exp | expiresIn | token过期时间 | exp 属性设置的是一个秒值时间戳,但 expiresIn设置的是一个间隔秒值。 |
| nbf | notBefore | token起效时间 | nbf 属性设置的是一个秒值时间戳,但 notBefore设置的是一个间隔秒值。 |
| aud | audience | 受众字符串 | 无区别 |
| iss | issuer | 颁发者字符串 | 无区别 |
| sub | subject | 主题字符串 | 无区别 |
| jti | jwtid | token编号 | 无区别 |
jwt.verify 验证token
函数语法:
jwt.verify(token, secretOrPublicKey, [options, callback])
(异步)如果提供了回调,则函数异步执行。如果签名处于有效日期之内 现行时间 < exp、audience(受众)或issuer(颁发者)有效,则使用解码的payload调用回调函数。如果没有,它将被使用error 调用回调函数。
(同步)如果没有提供回调函数,函数将同步执行。如果签名处于有效日期之内 现行时间 < exp、audience(受众)或issuer(颁发者)有效, 则返回已解码的payload, 否则它将抛出一个错误.
参数说明:
-
tokenJsonWebToken字符串 -
secretOrPublicKey是一个字符串或缓冲区,其中包含HMAC算法的秘密,或RSA和ECDSA的PEM编码的公钥。如果 jwt.verify 是
异步的,则secretOrPublicKey可以是一个获取秘密密钥或公钥的函数。正如在本注释中提到的,还有其他库期望使用 base64编码的秘钥(使用 base64编码的随机字节) ,如果这是您的情况,您可以传递 Buffer.from (secret,‘ base64’) ,通过这样做,秘钥将使用 base64进行解码,令牌验证将使用原始的随机字节。
-
optionsalgorithms包含允许算法名称的字符串列表。例如,[“ HS256”,“ HS384”]。audience如果你想检查受众(aud) ,在这里提供一个值。可以根据字符串、正则表达式, 或者由正则表达 式 和 字符串组成的列表来检查受众字符串。
例如:
"urn:foo",/urn:f[o]{2}/,[/urn:f[o]{2}/, "urn:bar"]-
complete返回一个带有解码的{ payload,header,signature }的对象,而不是仅返回payload的通常内容。 -
issuer(可选) 检查颁发者(iss)字段有效值的字符串或数组(注意不支持正则验证) -
ignoreExpiration如果为 true 则忽略检查令牌的过期时间 -
ignoreNotBefore如果为 true 同样忽略检查令牌的过期时间 -
subject如果要检查 subject (sub) ,请在此处提供一个值 -
clockTolerance检查NBF和exp声明时容差的秒数,以处理不同服务器之间的小时钟差异 -
maxAge令牌有效期最大允许时间。它以秒或描述时间跨度的字符串形式表示。例如:
1000,"2 days","10h","7d"这些数值都被解释为秒数。如果使用字符串,请确保提供时间单位(days、hours等),否则默认使用毫秒单位("120"等于"120ms")。 -
clockTimestamp以秒为单位的时间,应该用作所有必要比较的当前时间。 -
Nonce如果要检查nonce声明,在这里提供一个字符串值。它用于 ID 令牌的 Open ID.nonce参数值需要包含每个会话的状态并且攻击者无法猜测 。为 Web 服务器客户端实现此目的的一种方法是将加密随机值存储为 HttpOnly 会话 cookie,并使用该值的加密散列作为nonce参数。在这种情况下,返回的 ID 令牌中的随机数与会话 cookie 的哈希值进行比较,以检测第三方的 ID 令牌重放。适用于 JavaScript 客户端的相关方法是将加密随机值存储在 HTML5 本地存储中,并使用该值的加密哈希。
同步验证对称加密有效token
const token = jwt.sign({name: 'Is a token!'}, 'key');
console.log(token);
//eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSXMgYSB0b2tlbiEiLCJpYXQiOjE2NTE2MTcwNzd9.siWjVYIvsMyNvF3riHN1gUVhVONwOcw2ztdzIAoLQak
// 验证该 token 的有效性
const payload = jwt.verify(token, 'key');
console.log(payload); // { name: 'Is a token!', iat: 1651617183 }
异步验证对称加密有效token
jwt.verify(token, 'key', (err, payload) => {console.log(payload)});
// { name: 'Is a token!', iat: 1651617561 }
同步验证对称加密无效token
const token = jwt.sign({name: 'Is a token!'}, 'key');
let payload = null;
try
{
payload = jwt.verify(token, 'key1');
}catch(err)
{
console.log(err.toString());
}
// JsonWebTokenError: invalid signature
异步验证对称加密无效token
jwt.verify(token, 'key1', (err, payload) => {
if(err)
{
console.log('Error', err.toString())
}else
{
console.log(payload);
}
});
// Error JsonWebTokenError: invalid signature
// 签名内容不合法错误
验证RS256非对称加密的token
// 生成一个 RS256 加密的 token
const fs = require('fs');
const path = require('path');
// 使用私钥进行加密
const private_key = fs.readFileSync(path.join(__dirname, 'pri_key.pem'));
const token = jwt.sign({name: 'Is a token!'}, private_key, {algorithm: 'RS256'});
// 验证token
// 使用公钥进行解密
const public_key = fs.readFileSync(path.join(__dirname, 'pub_key.pem'));
const payload = jwt.verify(token, public_key);
console.log(payload); // { name: 'Is a token!', iat: 1651621599 }
验证 audience(受众)字段token
// 生成一个具有 audience 字段的token
const token = jwt.sign({name: 'Is a token!'}, 'sh', {audience: 'add1'});
// 同步验证正确 audience 受众字符串
const payload = jwt.verify(token, 'sh', {audience: [/^add\d$/]});
console.log(payload);
// 生成一个具有 audience 字段的token
const token1 = jwt.sign({name: 'Is a token!'}, 'sh', {audience: 'add'});
// 异步验证错误 audience 受众字符串
jwt.verify(token1, 'sh', {audience: [/^add\d$/]}, (err, payload) => {
if(err)
{
console.log(err.toString());
return
}
console.log(payload);
});
多项匹配验证 issuer(颁发者) 字段
// 同步异步 issue 颁发者字段
const token1 = jwt.sign({name: 'Is a token!'}, 'sh', {issuer: 'localhost'});
const token2 = jwt.sign({name: 'Is a token!'}, 'sh', {issuer: '127.0.0.1'});
// 定义一个验证函数
function verify_token(token)
{
return jwt.verify(token, 'sh', {issuer: ['localhost', '127.0.0.1']})
}
let payload1 = null, payload2 = null;
try
{
payload1 = verify_token(token1);
// 颁发者可以是 localhost 也可以是 127.0.0.1 但注意: issuer 验证不支持正则表达式
console.log(payload1);
}catch (err)
{
console.log(err.toString());
}
try
{
payload2 = verify_token(token2);
console.log(payload2);
}catch(err)
{
console.log(err.toString());
}
验证 issuer 颁发者 不支持正则验证 源码:
var invalid_issuer = (typeof options.issuer === 'string' && payload.iss !== options.issuer) || (Array.isArray(options.issuer) && options.issuer.indexOf(payload.iss) === -1);
验证 subject(主题) 字段
// 验证 subject 主题字段
// 生成 token
const token = jwt.sign({name: 'Is a token!'}, 'sh', {subject: 'web'});
// 验证
jwt.verify(token, 'sh', {subject: 'web'}, (err, payload) => {
if(err)
{
console.log(err.toString());
}else
{
console.log(payload); // { name: 'Is a token!', iat: 1651625457, sub: 'web' }
}
});
验证 subject 主题 不支持正则验证、也不支持数组多重验证,只能给一个字符串。
验证jwtid字段
const token = jwt.sign({name: 'Is a token!'}, 'sh', {jwtid: '1'});
// 验证
jwt.verify(token, 'sh', {jwtid: '1'}, (err, payload) => {
if(err)
{
console.log(err.toString());
}else
{
console.log(payload); //{ name: 'Is a token!', iat: 1651625938, jti: '1' }
}
});
验证 jwtid jwt编号 不支持正则验证、也不支持数组多重验证,只能给一个字符串。
默认token的时效性
// 生成 token
const token = jwt.sign({name: 'Is a token!'}, 'sh');
// 验证
jwt.verify(token, 'sh', (err, payload) => {
if(err)
{
console.log(err.toString());
}else
{
console.log(payload); // { name: 'Is a token!', iat: 1651627603 }
}
});
默认生成的 token 仅有一个 iat 参数,也就是以秒表示的"时间戳", 它具不有时效性检查标志,所以也代表着对它的检查永不过期。
expiresIn控制过期时间
// 生成 token
const token = jwt.sign({name: 'Is a token!'}, 'sh', {expiresIn: 1});
// 验证函数:
function verify_token(t)
{
jwt.verify(t, 'sh', (err, payload) => {
if(err)
{
console.log(err.toString());
}else
{
console.log(payload);
}
})
}
// 即时验证
verify_token(token); // { name: 'Is a token!', iat: 1651627938, exp: 1651627939 }
// 延时验证
setTimeout(verify_token, 2000, token); // TokenExpiredError: jwt expired
exp属性设置token时效:
// 生成 token
const token = jwt.sign({name: 'Is a token!', exp: Math.floor(Date.now() / 1000) + 1}, 'sh');
// 验证函数:
function verify_token(t)
{
jwt.verify(t, 'sh', (err, payload) => {
if(err)
{
console.log(err.toString());
}else
{
console.log(payload);
}
})
}
// 即时验证
verify_token(token); // { name: 'Is a token!', exp: 1651628132, iat: 1651628131 }
// 延时验证
setTimeout(verify_token, 2000, token); // TokenExpiredError: jwt expired
notBefore设置起效时间
// 生成 token
const token = jwt.sign({name: 'Is a token!'}, 'sh', {notBefore: '1s'});
// 验证函数:
function verify_token(t)
{
jwt.verify(t, 'sh', (err, payload) => {
if(err)
{
console.log(err.toString());
}else
{
console.log(payload);
}
})
}
// 即时验证
verify_token(token); // NotBeforeError: jwt not active
// 此时 token 还没有起效
// 延时验证
setTimeout(verify_token, 2000, token); // { name: 'Is a token!', iat: 1651628327, nbf: 1651628328 }
iat控制生成时间
const token = jwt.sign({name: 'Is a token!', iat: Math.floor(Date.now()/1000) - 10}, 'sh', {expiresIn: '5s'});
// 虽然 expiresIn 设置过期时间为5s钟,但该token使用 iat 将其生成时间修改为10秒前,即生成就直接过期了。
// 验证函数:
function verify_token(t)
{
jwt.verify(t, 'sh', (err, payload) => {
if(err)
{
console.log(err.toString());
}else
{
console.log(payload);
}
})
}
// 即时验证
verify_token(token); // TokenExpiredError: jwt expired
// 延时验证
setTimeout(verify_token, 2000, token); // TokenExpiredError: jwt expired
noTimestamp不向payload添加iat
// 生成 token
const token = jwt.sign({name: 'Is a token!'}, 'sh', {expiresIn: '5s', noTimestamp: true});
// 验证函数:
function verify_token(t)
{
jwt.verify(t, 'sh', (err, payload) => {
if(err)
{
console.log(err.toString());
}else
{
console.log(payload);
}
})
}
// 即时验证
verify_token(token); // { name: 'Is a token!', exp: 1651628860 }
// payload 中不再自动添加 iat 创建时间属性,同时也失去了token的有效期限证。
// 延时验证
setTimeout(verify_token, 2000, token); // { name: 'Is a token!', exp: 1651628860 }
// 上例中已经过期的token 设置 notTimestamp=true 后,验证成功。
手动添加iat属性,即使设置notTimestamp属性仍然验证时效性:
// 生成 token
const token = jwt.sign({name: 'Is a token!', iat: Math.floor(Date.now()/1000)}, 'sh', {expiresIn: '1s', noTimestamp: true});
// 手动在 payload 中添加 iat 属性,并设置 noTimestamp=true
// 验证函数:
function verify_token(t)
{
jwt.verify(t, 'sh', (err, payload) => {
if(err)
{
console.log(err.toString());
}else
{
console.log(payload);
}
})
}
// 即时验证
verify_token(token); // { name: 'Is a token!', exp: 1651629234 }
// 延时验证
setTimeout(verify_token, 2000, token); // TokenExpiredError: jwt expired
clickTolerance设置容差时间
const token = jwt.sign({name: 'Is a token!'}, 'sh', {expiresIn: '3s'});
// 验证函数:
function verify_token(t)
{
try
{
console.log(jwt.verify(t, 'sh', {clockTolerance: 3})); // 对于 NBF exp比较容差3秒
}catch (err)
{
console.log(err.message);
}
}
// 即时验证
verify_token(token);
// 延时验证
setTimeout(verify_token, 5000, token);
maxAge最大生效期
const token = jwt.sign({name: 'Is a token!'}, 'sh', {expiresIn: '10s'});
// 验证函数:
function verify_token(t)
{
try
{
console.log(jwt.verify(t, 'sh', {maxAge: '3s'})); // 当前 token 最大有效期已超出
}catch (err)
{
console.log(err.message);
}
}
// 即时验证
verify_token(token);
// 延时验证
setTimeout(verify_token, 4000, token);
clockTimestamp设置“现行时间”
const timestamp = Math.floor(Date.now()/1000);
const token = jwt.sign({name: 'Is a token!', iat: timestamp - 20}, 'sh', {expiresIn: '15s'});
// 回溯20创建 token 有效期15s,也就意味着创建即过期
// 验证函数
function verity_token(now=true)
{
const option = {};
option['clockTimestamp'] = now?undefined:timestamp - 10
jwt.verify(token, 'sh', option, (err, payload) => {
if(err)
{
console.log(err.message);
}else
{
console.log(payload);
}
})
}
verity_token(); // jwt expired
// 使用现行时间验证
verity_token(false); // { name: 'Is a token!', iat: 1651640251, exp: 1651640266 }
// 使用 callTimestamp 提供10秒前时间用作现行时间验证
jwt.decode解码token
jwt.decode(token [, options])
(同步) 在不验证签名是否有效的情况下返回解码后的payload。
tokenjsonWebToken字符串complete返回一个解码payload和head的对象。
const token = jwt.sign({name: 'Is a token!'}, 'sh', {header: {test: 'test'}, keyid: '1'});
console.log(jwt.decode(token));
// { name: 'Is a token!', iat: 1651631448 }
console.log(jwt.decode(token, {complete: true}));
/*
{
header: { alg: 'HS256', typ: 'JWT', kid: '1', test: 'test' },
payload: { name: 'Is a token!', iat: 1651631271 },
signature: '0ymmPreaPEiVkYAuLCR5gNhbPxYf7jAT57z9jRg5KiQ'
}
*/
console.log(jwt.decode('Error token', {complete: true}));
// 无法解码将输出为 null
JWT错误代码
JsonWebTokenError
Error object:
- name: 'JsonWebTokenError'
- message:
- 'jwt malformed'
token格式不匹配 - 'jwt signature is required'
token没有解析到signature签名序列 - 'invalid signature'
无效的签名 - 'jwt audience invalid. expected: [OPTIONS AUDIENCE]'
受众字段验证不匹配 - 'jwt issuer invalid. expected: [OPTIONS ISSUER]'
颁发都字段验证不匹配 - 'jwt id invalid. expected: [OPTIONS JWT ID]'
JWT ID 验证不匹配 - 'jwt subject invalid. expected: [OPTIONS SUBJECT]'
主题字段验证不匹配
- 'jwt malformed'
NotBeforeError:
如果当前时间在nbf声明之前则抛出。
Error Object
- name: 'NotBeforeError'
- message: 'jwt not active'
- date: 2018-10-04T16:10:44.000Z
JWT支持算法
支持的算法数组。目前支持的算法有:
| alg参数值 | 数字签名或MAC算法 |
|---|---|
| HS256 | HMAC using SHA-256 hash algorithm |
| HS384 | HMAC using SHA-384 hash algorithm |
| HS512 | HMAC using SHA-512 hash algorithm |
| RS256 | RSASSA-PKCS1-v1_5 using SHA-256 hash algorithm |
| RS384 | RSASSA-PKCS1-v1_5 using SHA-384 hash algorithm |
| RS512 | RSASSA-PKCS1-v1_5 using SHA-512 hash algorithm |
| PS256 | RSASSA-PSS using SHA-256 hash algorithm (only node ^6.12.0 OR >=8.0.0) |
| PS384 | RSASSA-PSS using SHA-384 hash algorithm (only node ^6.12.0 OR >=8.0.0) |
| PS512 | RSASSA-PSS using SHA-512 hash algorithm (only node ^6.12.0 OR >=8.0.0) |
| ES256 | ECDSA using P-256 curve and SHA-256 hash algorithm |
| ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm |
| ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm |
| none | No digital signature or MAC value included |
应用案例:
koa 中使用 jwt鉴权 附 nonce 验证:
案例目录:
./app.js:
const Koa = require('koa');
const Router = require('koa-router');
const Path = require('path');
const AUT = require(Path.join(__dirname, 'Aut'));
const bodyParser = require('koa-bodyparser');
const app = new Koa();
const router = new Router();
router.get('/login', AUT.verify_middleware,async (ctx, next) => {
// 验证用户是否登陆
if(ctx.state.aut)
{
// 已登陆用户:
ctx.redirect('/'); // 如果已经登陆则重定向至首页
}else
{
ctx.type = 'text/html';
ctx.body = `
<p>用户登陆</p>
<form action="/login" method="post">
<p><label><input type="text" name="user_name"></label></p>
<p><label><input type="text" name="pass_word"></label></p>
<p><button type="submit">登陆验证</button></p>
</form>
`;
await next()
}
});
router.post('/login', async (ctx, next) => {
// 验证是否登陆
if(ctx.state.aut)
{
ctx.redirect('/'); // 已登陆重定向至首页
}else
{
// 未登陆,获取数据
const { user_name, pass_word } = ctx.request.body;
if(user_name === 'user1' && pass_word === '123123')
{
// 用户帐户验证成功,添加 cookies
AUT.sign_aut_cookies(ctx, {user_name, pass_word, power: 3});
ctx.redirect('/'); // 登陆成功,返回至首页
}else
{
// 如果帐号密码设置错误
ctx.body = {result: 403, meg: '帐号不存在或密码错误.'};
}
}
await next();
})
// 添加验证中间件 AUT.verify_middleware 即可对页面进行鉴权处理
router.get('/', AUT.verify_middleware, async ctx => {
ctx.body = `<div>系统首页, ${ctx.state.aut?'检测到用户登陆:' + JSON.stringify(ctx.state.aut): '未检测到用户登陆<a href="/login">点击跳转到登陆页</a></div>'}`;
})
app.use(bodyParser());
app.use(router.routes());
app.listen(3000, '192.168.1.4', () => {
console.log('Example1 server running...');
})
./Aut.js
const SHA256 = require('sha256');
const JWT = require('jsonwebtoken');
const Fs = require('fs');
const Path = require('path');
const zeit_ms = require('ms');
// 设置的 cookie名称
const cookie_name = {token: 'aut-token', nonce: 'nonce'};
// cookie 有效期
const token_validity = '1m';
// 加密盐
const salt = '12/d^2dFE$~2.Al';
// 颁发者字符串
const issuer = 'xxx.com';
// 使用 RSA 非对称加密
const key = {
private_key: Fs.readFileSync(Path.join(__dirname, '/pri_key.pem')),
public_key: Fs.readFileSync(Path.join(__dirname, '/pub_key.pem'))
};
function get_nonce()
{
// 生成17位随机小数
const nonce = (Math.random() * 1E11).toFixed(7);
// 序列哈希
const sha_nonce = SHA256(nonce + salt);
return {nonce, sha_nonce};
}
function sign_token(payload, nonce)
{
payload['nonce'] = nonce;
return JWT.sign(payload, key.private_key, {algorithm: 'RS256', expiresIn: token_validity, issuer: issuer});
}
// 设置 token 函数
function sign_aut_cookies(ctx, payload)
{
// 生成 nonce 二次验证字符串
const { nonce, sha_nonce } = get_nonce();
// 生成 token
const token = sign_token(payload, sha_nonce);
// 设置浏览器 token
ctx.cookies.set(cookie_name.token, token, {maxAge: zeit_ms(token_validity)/1000}); // 存储 token
ctx.cookies.set(cookie_name.nonce, nonce, {maxAge: zeit_ms(token_validity)/1000}); // 存储哈希后的 nonce
// 设置登陆状态
ctx.state.aut = payload;
}
async function verify_token(token, nonce)
{
// 验证token 有效性
return await new Promise((resolve, reject) => {
// token验证
const sha_nonce = SHA256(nonce + salt);
JWT.verify(token, key.public_key, {nonce: sha_nonce, algorithm: 'RS256'}, (err, payload) => {
if(err)
{
reject(err.message);
}
resolve(payload);
});
}).then(payload =>
{
// 验证成功,设置 用户数据
return {aut: true, payload};
}).catch(err => {
return {aut: false, err};
})
}
// 创建验证 token 中间件
function create_verify_middleware() {
// 返回登陆验证中间件
return async (ctx, next) => {
// 首先获取cookie
const token = ctx.cookies.get(cookie_name.token),
nonce = ctx.cookies.get(cookie_name.nonce);
// 验证cookie是否存在
if(token && nonce)
{
// 如果值存在,验证 token 合法性:
const decode = JWT.decode(token);
if(decode && decode.nonce)
{
const result = await verify_token(token, nonce);
if(result.aut)
{
// 验证成功,设置用户数据
ctx.state.aut = result.payload;
}
}
}
await next();
}
}
module.exports = {
verify_middleware: create_verify_middleware(),
sign_aut_cookies
}
./pri_key.pem
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDp0scD2sofN7uu
ruYrcY4ZHlTtX3o1gS1HZp+mhJ3pw0Xf2rFHxDlwjMzd34wUwa703xws4iESFWV1
E2Rk+jHp5sbdfCP+muqNppFp2eYPMD3TSHlDW0aio5gJIs0GvT+vYGOtKUZJQpqt
dbgvIzv8YT9mTaxO1o/TITEPYknPKRBTApDPEWBgZzRCMswrJkd9TFjqM8HphzF9
jSr7SOkeWMeKGQJaXsMP9URHCVwYc79AJARIHGEtn8ehDnkhMYp+QAQpSIvDNknR
eAQkEEyiK4KLq58dgKR70mv+hFtcUIF/6kgi9N3aejb9IvYKmlBwTi1UZQq6vYF5
IeP9NBNpAgMBAAECggEAJjXD60pWsLFKjKpCLM6auloYO3jYdLk5AquvjTuraTxN
mojsuDNFxlxhTM9aNI4jdBqoSmFi/ByI/uUos85QV86BqEdqw7w3IOdxEzYrf/Hn
OIlltO8hOjS3r06Dk1qxIeIDh+mbIHUE59xj4+LDIQ7B7eA64eCdkj1LUh3tZ9pt
rVsss0pPssaz3A5Kw56jOXYBkp+6iHj6Z955hf0okb2F6rH947+AA6xwQBr8jKmG
6AiWKjPYE+QZz0LZGzAoDpYT3/L/j8xozRMyIWAlZahUsGNmee68NaE17j8qBx+t
pUXm6XiitMoR0I/eVxizgQlzMfKemDw+vY5osP4uLQKBgQDrTbKXbBi5Nza8wHKt
3uenlLtYm3bCqJMt1SBIWigzYCj1/lLb4MCogWKlCrarttpAAqxA6FOPME313Kv8
ggY+XkWWRJh0FoEvruf/XvIbqFtTFhNK+az/XEFE+Ex8FBsPicGwq42fTw/GNUtA
ZcslKDriGdcOR2z4CBrz247A2wKBgQD+Y8BR5gz0lSsc9zOLnHzNPkuXmkApGqvW
D7c/ggNJgnkpwM93eYtmnTkKfHmGU8P55GgyebAUznjgi/8J9IPIY+CWHVGQtip+
lRvfuLy5xeizxdBCKhenyrL0aMtqMfYlV/WLhYh/fRQ2shT6FrWHru8O8TTunyZU
499fH1J+CwKBgHG42YmpasGJV+Educ8inh82GN2YNWubAZbzHGkbebZq0SEiy3UN
SM+nyL8vUF3pP84T/KBaKJhZpjQfdNfTF6Vo9oTLnkrs3X7oL6QShTysv4aGcsiX
pu677aehwYVuSi3WTFBQeV/3XA5Vj4sOJ4JL0TmZd4ClMJsnf68/+9xlAoGBAOzN
UmNqcLjkeqOWAad+XjZ4PMm8zuj0aOz06DH4BrNQ/xS85ONkgz1cMZLvV/iQ1Scz
VUNXP2++24q7BwfvUxUg1Bko5Zp92ITIrI/PWe7cqcde5oQQxsBs0bTuMByEsYsF
nA7qmKuEdWKM1zYOGaFaWDJimlFeWMJA9NeJQrMNAoGAWp0y+9Rd0jh1dyvXYX3p
DVMxsGU7PmjgXhM+m3iu6no/VG9UpS4sDP++cL3MCOflo+nqtGay7FKrrGSgO/nJ
G2rj7DXBJTHR0jg+rvKyPcanDzhnjh21Y6JmpgJ2kITydx9QBUjXHZ+ZEW7ftslQ
wjdpQPw+Tou6Qk8JbiFOJK0=
-----END PRIVATE KEY-----
./pub_key.pem
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6dLHA9rKHze7rq7mK3GO
GR5U7V96NYEtR2afpoSd6cNF39qxR8Q5cIzM3d+MFMGu9N8cLOIhEhVldRNkZPox
6ebG3Xwj/prqjaaRadnmDzA900h5Q1tGoqOYCSLNBr0/r2BjrSlGSUKarXW4LyM7
/GE/Zk2sTtaP0yExD2JJzykQUwKQzxFgYGc0QjLMKyZHfUxY6jPB6YcxfY0q+0jp
HljHihkCWl7DD/VERwlcGHO/QCQESBxhLZ/HoQ55ITGKfkAEKUiLwzZJ0XgEJBBM
oiuCi6ufHYCke9Jr/oRbXFCBf+pIIvTd2no2/SL2CppQcE4tVGUKur2BeSHj/TQT
aQIDAQAB
-----END PUBLIC KEY-----
浏览器测试 cookie:
ctx.state.aut 输出: