You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 05:55:57