发布于 2026-01-06 8 阅读
0

保持无状态——在Node.js中使用Redis进行令牌黑名单管理

保持无状态——在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(),
      });
    }
  }
Enter fullscreen mode Exit fullscreen mode

解释:

Redis 的 LPUSH 方法类似于数组的 push 方法。简单来说,就是将令牌添加到名为“token”的数组中。点击注销按钮后,会调用注销端点,将令牌加入黑名单,然后清除本地存储。

结论:

Redis 是一个非常有用的工具。想要了解更多 Redis 的用法,请阅读其
文档,特别是关于缓存的部分

此Redis文件夹中提供了一个适用于 Windows 的 Redis 实现。如有任何疑问,请随时通过ob_cea或下方帖子
联系我。

文章来源:https://dev.to/mr_cea/using-redis-for-token-blacklisting-in-node-js-42g7