koa-jsonwebtoken 模块学习笔记

768 阅读15分钟

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 算法的密钥或 RSAECDSAPEM 编码的私钥。对于带有密码短语的私钥,可以使用对象{ key,passphrase }(基于加密文档) ,在这种情况下,请确保传递算法选项。

  • options:

    • algorithm 加密的算法,默认情况下: (default: HS256)
    • expiresIn 日期证书有效期跨度 以秒或描述时间跨度 zeit/ms 的字符串表示. (相关语法在下面)

      例如: 60,“2 days”,“10h”,“7d”。数值被解释为秒计数。如果使用字符串,请确保提供时间单位(days、hours等) ,否则默认使用的是毫秒单位(“120”等于“120毫秒”)。

    • notBefore 日期证书有效期开始 以秒或描述时间跨度 zeit/ms 的字符串表示。
    • audience 受众
    • issuer 颁发者
    • jwtid JWT的序号
    • subject 主题
    • noTimestamp 不自动设置生成时间
    • header 设置token头对象(encode后可见)
    • KeyID 密钥id (解码后在header对象中)
    • mutatePayload 如果为真,option 标志设置值 将直接修改有效负载对象。如果在声明应用到有效负载之后,但是在将其编码到令牌之前,需要对有效负载进行原始引用,那么这种方法很有用。

expiresInnotBeforeaudiencesubjectissuer没有默认值。这些声明也可以分别通过expnbfaudsubiss直接在有效负载中提供,但不能同时包含在这两个地方。

注意: expnbfiatNumericDate类型,详情在下面的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长度可以适当调节

image.png

openssl rsa -in pri_key.pem -pubout -out pub_key.pem

image.png

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选项功能区别
expexpiresIntoken过期时间exp 属性设置的是一个秒值时间戳,但 expiresIn设置的是一个间隔秒值。
nbfnotBeforetoken起效时间nbf 属性设置的是一个秒值时间戳,但 notBefore设置的是一个间隔秒值。
audaudience受众字符串无区别
ississuer颁发者字符串无区别
subsubject主题字符串无区别
jtijwtidtoken编号无区别

jwt.verify 验证token

函数语法:

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

(异步)如果提供了回调,则函数异步执行。如果签名处于有效日期之内 现行时间 < expaudience(受众)issuer(颁发者)有效,则使用解码的payload调用回调函数。如果没有,它将被使用error 调用回调函数。

(同步)如果没有提供回调函数,函数将同步执行。如果签名处于有效日期之内 现行时间 < expaudience(受众)issuer(颁发者)有效, 则返回已解码的payload, 否则它将抛出一个错误.

参数说明:

  • token JsonWebToken字符串

  • secretOrPublicKey 是一个字符串或缓冲区,其中包含HMAC算法的秘密,或RSA和ECDSA的PEM编码的公钥。

    如果 jwt.verify 是异步的,则 secretOrPublicKey 可以是一个获取秘密密钥或公钥的函数。

    正如在本注释中提到的,还有其他库期望使用 base64编码的秘钥(使用 base64编码的随机字节) ,如果这是您的情况,您可以传递 Buffer.from (secret,‘ base64’) ,通过这样做,秘钥将使用 base64进行解码,令牌验证将使用原始的随机字节。

  • options

    • algorithms 包含允许算法名称的字符串列表。例如,[“ 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 检查NBFexp声明时容差的秒数,以处理不同服务器之间的小时钟差异

    • 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

  • token jsonWebToken字符串
  • complete 返回一个解码payloadhead的对象。
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]' 主题字段验证不匹配

NotBeforeError:

如果当前时间在nbf声明之前则抛出。

Error Object

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

JWT支持算法

支持的算法数组。目前支持的算法有:

alg参数值数字签名或MAC算法
HS256HMAC using SHA-256 hash algorithm
HS384HMAC using SHA-384 hash algorithm
HS512HMAC using SHA-512 hash algorithm
RS256RSASSA-PKCS1-v1_5 using SHA-256 hash algorithm
RS384RSASSA-PKCS1-v1_5 using SHA-384 hash algorithm
RS512RSASSA-PKCS1-v1_5 using SHA-512 hash algorithm
PS256RSASSA-PSS using SHA-256 hash algorithm (only node ^6.12.0 OR >=8.0.0)
PS384RSASSA-PSS using SHA-384 hash algorithm (only node ^6.12.0 OR >=8.0.0)
PS512RSASSA-PSS using SHA-512 hash algorithm (only node ^6.12.0 OR >=8.0.0)
ES256ECDSA using P-256 curve and SHA-256 hash algorithm
ES384ECDSA using P-384 curve and SHA-384 hash algorithm
ES512ECDSA using P-521 curve and SHA-512 hash algorithm
noneNo digital signature or MAC value included

应用案例:

koa 中使用 jwt鉴权 附 nonce 验证:

案例目录:
image.png

./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:

image.png

ctx.state.aut 输出:

image.png