如何使用Node.js与MongoDB实现用户登出功能?
JWT 登出功能实现思路与代码示例
由于 JWT 是无状态的,一旦签发在过期前默认始终有效,无法直接主动撤销,因此登出功能需要通过服务端的额外验证机制来拦截无效 Token。以下是两种实用的实现方案:
方案一:Token 黑名单机制(适合中小规模应用)
核心思路是维护一个已注销 Token 的黑名单,认证时先检查 Token 是否在黑名单内,再进行常规验证。
步骤1:准备存储(以 Redis 为例)
Redis 自带过期时间机制,适合存储临时的黑名单数据,无需手动清理过期条目。
npm install redis
初始化 Redis 客户端:
const redis = require('redis'); const redisClient = redis.createClient({ url: process.env.REDIS_URL // 替换为你的 Redis 地址 }); redisClient.connect();
步骤2:编写登出接口
接收当前用户的 Token,将其存入黑名单并设置与原 Token 一致的过期时长:
router.post("/admin/sign_out", authenticate, async (req, res) => { try { const token = req.body.token || req.query.token || req.headers["autherization"]; const decoded = jwt.verify(token, process.env.TOKEN_KEY); // 计算剩余过期时间(秒) const expiresIn = decoded.exp - Math.floor(Date.now() / 1000); // 将 Token 加入黑名单,自动过期 await redisClient.setEx(token, expiresIn, 'invalidated'); return res.status(200).send("登出成功"); } catch (err) { return res.status(400).send("登出失败"); } });
步骤3:修改认证中间件
新增黑名单检查逻辑,拦截已注销的 Token:
const authenticate = async (req, res, next) => { const token = req.body.token || req.query.token || req.headers["autherization"]; if (!token) { return res.status(403).send("需要 Token 进行认证"); } // 先检查 Token 是否在黑名单中 const isBlacklisted = await redisClient.get(token); if (isBlacklisted) { return res.status(401).send("Token 已失效(已登出)"); } try { const decoded = jwt.verify(token, config.TOKEN_KEY); req.user = decoded; } catch (err) { return res.status(401).send("无效 Token"); } return next(); };
方案二:数据库存储有效 Token(适合多设备管理场景)
核心思路是在用户文档中维护一个「有效 Token 列表」,登录时添加新 Token,登出时移除对应 Token,认证时验证 Token 是否在列表内。
步骤1:修改 User Schema
添加存储有效 Token 的数组字段:
const mongoose = require('mongoose'); const userSchema = new mongoose.Schema({ email: { type: String, required: true, unique: true }, password: { type: String, required: true }, activeTokens: [{ type: String }] // 新增:存储当前有效的 Token }); module.exports = mongoose.model('User', userSchema);
步骤2:修改登录接口
生成 Token 后将其存入用户的 activeTokens 数组:
router.post("/admin/sign_in", async (req, res) => { try { const { email, password } = req.body; if (!(email && password)) { return res.status(400).send("所有字段均为必填"); } const user = await User.findOne({ email }); if (user && (await bcrypt.compare(password, user.password))) { const token = jwt.sign( { user_id: user._id, email }, process.env.TOKEN_KEY, { expiresIn: "15h" } ); // 将新 Token 添加到有效列表并保存 user.activeTokens.push(token); await user.save(); return res.status(200).json(user); } return res.status(400).send("无效的账号或密码"); } catch (err) { return res.status(400).send("无效的账号或密码"); } });
步骤3:修改认证中间件
验证 Token 有效性的同时,检查其是否在用户的有效 Token 列表中:
const authenticate = async (req, res, next) => { const token = req.body.token || req.query.token || req.headers["autherization"]; if (!token) { return res.status(403).send("需要 Token 进行认证"); } try { const decoded = jwt.verify(token, config.TOKEN_KEY); const user = await User.findById(decoded.user_id); // 检查 Token 是否在有效列表内 if (!user.activeTokens.includes(token)) { return res.status(401).send("Token 已失效(已登出)"); } req.user = decoded; } catch (err) { return res.status(401).send("无效 Token"); } return next(); };
步骤4:编写登出接口
从用户的有效 Token 列表中移除当前 Token:
router.post("/admin/sign_out", authenticate, async (req, res) => { try { const token = req.body.token || req.query.token || req.headers["autherization"]; const decoded = jwt.verify(token, process.env.TOKEN_KEY); // 从用户的有效列表中移除 Token await User.findByIdAndUpdate( decoded.user_id, { $pull: { activeTokens: token } }, { new: true } ); return res.status(200).send("登出成功"); } catch (err) { return res.status(400).send("登出失败"); } });
额外注意事项
- 前端登出时,需同步删除本地存储的 Token(如 localStorage、sessionStorage 或 Cookie),避免后续请求携带无效 Token。
- 若使用 MongoDB 存储黑名单或有效 Token,需定期清理过期的无效 Token,可通过定时任务实现。
- 多设备场景下,数据库方案支持用户查看所有登录设备并选择性登出某一设备,灵活性更高。
内容的提问来源于stack exchange,提问作者sumanth.js
相关产品推荐
相关产品推荐

