Express-Session自定义genid引发req.session未定义问题求助
使用Express、express-session、ioredis和connect-redis实现会话管理,正常情况下可成功创建用户会话。希望将用户ID写入会话键名,以便后续通过用户ID获取该用户的所有会话,但自定义genid函数后,出现req.session变为undefined,且Redis中记录的会话键为Undefined的问题。
核心代码
app.js
import express from "express"; import session from "express-session"; import RedisStore from "connect-redis"; import { Redis } from "ioredis"; import dotenv from "dotenv"; dotenv.config(); const app = express(); const redisClient = new Redis(); redisClient.on("error", () => console.error("REDİS BAĞLANTISI BAŞARISIZ OLDU")); redisClient.on("connect", () => console.log("REDİS BAĞLANTISI BAŞARILI BİR ŞEKİLDE GERÇEKLEŞTİ") ); export { redisClient }; const redisStore = new RedisStore({ client: redisClient, prefix: "session:", disableTouch: true, }); app.use( session({ name: process.env.COOKIE_NAME, cookie: { maxAge: 1000 * 60 * 60 * 24, httpOnly: true, secure: false, }, secret: process.env.SECRET_KEY, resave: false, saveUninitialized: false, store: redisStore, genid: (req) => { const userId = req.session?.user?.id || ""; const userAgent = req.headers["user-agent"]; return `${userId}:${userAgent}`; }, }) );
authController.js
export const loginController = async (req, res) => { try { const { username, password } = req.body; if (username && password) { const user = await User.findOne({ username }); if (user) { const hashedPassword = CryptoJS.AES.decrypt( user.password, process.env.SECRET_KEY ); const originalPassword = hashedPassword.toString(CryptoJS.enc.Utf8); if (originalPassword !== req.body.password) { return res.status(401).json({ error: "Girdiğin şifre yanlış. Lütfen şifreni kontrol et", }); } const data = { id: user._id, username: user.username, role: user.role }; req.session.user = data; res.status(200).json({ data: data, message: "Giriş Başarılı", }); } else { res.status(401).json({ error: "Girdiğiniz kullanıcı adına ait bir hesap bulunamadı!", }); } } } catch (error) { console.error(error); } }; export const logoutController = async (req, res) => { try { req.session.destroy((err) => { if (err) { return res.status(400).json({ error: "Çıkış yapılırken bir sorun oluştu", }); } res.clearCookie(process.env.COOKIE_NAME); res.redirect("/"); }); } catch (error) { res.status(500).json({ error: "Çıkış yapılırken bir hata oluştu. Lütfen daha sonra tekrar deneyin!", }); } };
尝试的自定义genid代码
genid: (req) => { const userId = req.session.user?.id || ""; const userAgent = req.headers["user-agent"]; return `${userId}:${userAgent}`; },
genid函数的执行时机早于req.session的初始化。请求进入时,express-session首先调用genid生成会话ID,此时req.session尚未创建,无法获取req.session.user.id,最终生成的会话ID为:xxx(userId为空),甚至会干扰会话正常创建流程,导致req.session变为undefined,Redis中出现异常的Undefined记录。
另外,设置saveUninitialized: false意味着未修改的空会话不会被保存到Redis,但genid生成的无效ID会打破这个逻辑,引发异常。
不能通过genid直接获取用户ID(用户ID在登录成功后才写入会话),正确做法是:先让会话正常初始化,登录成功后手动重写会话ID,将用户ID加入其中,并同步更新Redis记录。
步骤1:修正genid逻辑
先生成有效的临时会话ID,保证会话正常初始化:
genid: (req) => { // 使用UUID生成随机会话ID,也可以用其他唯一标识规则 return require('crypto').randomUUID(); },
步骤2:登录成功后重写会话ID
在loginController中,写入req.session.user后,手动更新会话ID并同步到Redis:
export const loginController = async (req, res) => { try { const { username, password } = req.body; if (username && password) { const user = await User.findOne({ username }); if (user) { const hashedPassword = CryptoJS.AES.decrypt( user.password, process.env.SECRET_KEY ); const originalPassword = hashedPassword.toString(CryptoJS.enc.Utf8); if (originalPassword !== req.body.password) { return res.status(401).json({ error: "Girdiğin şifre yanlış. Lütfen şifreni kontrol et", }); } const data = { id: user._id, username: user.username, role: user.role }; req.session.user = data; // 生成包含用户ID的新会话ID const newSessionId = `${data.id}:${req.headers["user-agent"]}`; // 将当前会话数据保存到新ID await redisStore.set(newSessionId, req.session); // 删除旧会话ID对应的Redis记录 await redisStore.destroy(req.sessionID); // 更新req.sessionID并设置新的会话cookie req.sessionID = newSessionId; res.cookie(process.env.COOKIE_NAME, newSessionId, { maxAge: 1000 * 60 * 60 * 24, httpOnly: true, secure: false, }); res.status(200).json({ data: data, message: "Giriş Başarılı", }); } else { res.status(401).json({ error: "Girdiğiniz kullanıcı adına ait bir hesap bulunamadı!", }); } } } catch (error) { console.error(error); res.status(500).json({ error: "Giriş işlemi sırasında bir hata oluştu" }); } };
步骤3:通过用户ID查询所有会话
会话ID格式为userId:userAgent,可通过Redis的SCAN命令(生产环境推荐)或KEYS命令查询用户的所有会话:
// 示例:获取指定用户的所有会话数据 export const getUserSessions = async (userId) => { let sessions = []; let cursor = '0'; // 使用SCAN遍历,避免阻塞Redis do { const [newCursor, keys] = await redisClient.scan(cursor, 'MATCH', `session:${userId}:*`); cursor = newCursor; if (keys.length > 0) { const sessionDataList = await Promise.all(keys.map(key => redisClient.get(key))); sessions = sessions.concat(sessionDataList.map(data => JSON.parse(data))); } } while (cursor !== '0'); return sessions; };
注意事项
- 生产环境禁止使用
KEYS命令,改用SCAN遍历键,防止Redis阻塞。 - 重写会话ID时,务必删除旧会话记录,避免遗留无效数据。
- 不同设备的userAgent不同,会生成不同的会话ID,支持用户多设备登录的会话管理需求。
内容的提问来源于stack exchange,提问作者ahmet ilboga

