保持无状态——在Node.js中使用Redis进行令牌黑名单管理
JSON Web Token (JWT) 是无状态的。这意味着服务器不会维护用户状态。数据库或会话中不会保存任何关于特定请求发送者的信息。JWT 提供了一个令牌来处理此类身份验证。
不维护状态的一个主要缺点是,令牌会一直有效,直到其设定的过期日期为止。因此,即使用户已在前端注销且本地存储已清空,任何拥有该令牌的人仍然可以访问该用户的已认证路由,直到令牌过期为止。
针对这一缺点,已经提出了一些解决方案:
-
将 JWT 的有效期缩短至很短,并使用有效期为两周或更长的刷新令牌。当 JWT 过期时,刷新令牌会在用户仍处于登录状态时为其生成新的 JWT。
这种方法的一个主要缺点是管理员可以随时撤销刷新令牌。如果在 JWT 过期时刷新令牌被撤销,用户将不得不重新登录,因为此时用户同时拥有一个已被撤销的刷新令牌和一个已过期的 JWT。 -
第二种方法是在注销时将黑名单令牌保存在用户表的列中,并用它来进行验证,并在先前的令牌过期时将其销毁。
- 这种方法不具备可扩展性。
- 这完全违背了 JWT 无状态的初衷,因为状态仍然被维护着。
-
第三种方法是通过更改 JWT 密钥来使所有用户的令牌失效。这可能会惹恼所有用户,因为他们都必须重新登录。
-
第四种方法是在 Redis 中注销时将该令牌列入黑名单。
Redis是什么?
Redis 是一个内存数据结构存储系统,可用作数据库、缓存或消息代理。您可以使用字符串、哈希表、列表、集合、有序集合等数据结构。
为什么使用内存数据库而不是数据库
内存管理系统依靠主内存进行计算机数据存储,速度比使用磁盘管理系统的数据库管理系统更快,因为其内部优化算法更简单,执行的 CPU 指令更少。将数据加载到内存中消除了查询数据时的寻道时间,从而提供比磁盘更快、更可预测的性能。
与使用数据库管理系统相比,内存管理系统更具可扩展性。
在 Node.js 中使用 Redis 验证令牌
-
安装 npm redis 包
npm install redis -
导入 Redis 并使用 redisClient
import redis from redis // ES6 + import { exec} from 'child_process';// to start the redis database in development /*// for windows user import {execFile} from 'child_process'; // for ES5 users const redis = require('redis')*/ // if in development mode use Redis file attached // start redis as a child process if (process.env.NODE_ENV === 'development') { const puts = (error, stdout) =>{ console.log(error) console.log(stdout) } exec('redis/src/redis-server redis/redis.conf', puts); } /* for window implementation execFile('redis/redis-server.exe',(error,stdout)=>{ if(error){ throw error } console.log(stdout) }) */ export const redisClient = redis.createClient(process.env.REDIS_URL); // process.env.REDIS_URL is the redis url config variable name on heroku. // if local use redis.createClient() redisClient.on('connect',()=>{ console.log('Redis client connected') }); redisClient.on('error', (error)=>{ console.log('Redis not connected', error) }); -
使用 Redis 进行令牌验证。
将您的 Redis 操作作为身份验证的一部分传递
import jwt from 'jsonwebtoken'; import pool from '../path/to/file'; import { redisClient } from '../path/to/file'; import 'dotenv'; const auth = { // eslint-disable-next-line consistent-return async checkToken(req, res, next) { const token = req.headers.authorization.split(' ')[1]|| req.params.token; // check if token exists if (!token) { return res.status(401).send({ status: 401, error: 'You need to Login', }); } /* .......redis validation .........*/ try { const result = await redisClient.lrange('token',0,99999999) if(result.indexOf(token) > -1){ return res.status(400).json({ status: 400, error: 'Invalid Token' }) } /* const invalid = (callback) => { redisClient.lrange('token', 0, 999999999, (err, result) => callback(result)); }; invalid((result) => { // check if token has been blacklisted if (result.indexOf(token) > -1){ return res.status(400).json({ status: 400, error: 'Invalid Token', }); } }) */ /* ...... ............................*/ const decrypt = await jwt.verify(token, process.env.SECRET); const getUser = 'SELECT * FROM users WHERE id= $1'; const { rows } = await pool.query(getUser, [decrypt.id]); // check if token has expired if (!rows[0]) { return res.status(403).json({ status: 403, error: ' Token Not accessible', }); } next(); } catch (error) { return res.status(501).json({ status: 501, error: error.toString(), }); } }, }; export default auth;
解释:
Redis 的 lrange 函数返回一个数组,其中包含已列入黑名单的令牌。如果使用的令牌已被列入黑名单,则 indexOf 方法返回 0 或更大的值,并返回 400 响应。
在 Node.js 中使用 Redis 进行黑名单
使用 Redis 将令牌列入黑名单
- 在后端创建登出路由:
通常情况下,只需
在注销时从本地存储中清除令牌即可,但要将令牌列入黑名单,则需要
在注销时调用一个端点。
static async logout(req, res) {
// logout user
// save token in redis
const token = req.headers.authorization.split(' ')[1];
try {
await redisClient.LPUSH('token', token);
return res.status(200).json({
'status': 200,
'data': 'You are logged out',
});
} catch (error) {
return res.status(400).json({
'status': 500,
'error': error.toString(),
});
}
}
解释:
Redis 的 LPUSH 方法类似于数组的 push 方法。简单来说,就是将令牌添加到名为“token”的数组中。点击注销按钮后,会调用注销端点,将令牌加入黑名单,然后清除本地存储。
结论:
Redis 是一个非常有用的工具。想要了解更多 Redis 的用法,请阅读其
文档,特别是关于缓存的部分。
此Redis文件夹中提供了一个适用于 Windows 的 Redis 实现。如有任何疑问,请随时通过ob_cea或下方帖子
联系我。