使用Express与Socket.IO持久化SessionID实现多人协同绘图
我正在开发一个支持多人同时绘图的网站,核心需求是:用户首次连接时分配唯一ID,刷新页面或关闭窗口重新连接后,仍能复用同一个ID。目前卡在Cookie和SessionID的存储逻辑上。
额外问题:app.get("/")接口无法正常工作,必须改成/canvas这类其他端点才能运行。之前不用Session、仅依赖socket.id的版本是正常的。我试过把app.get里的逻辑移到io.on("connect")里,虽然能运行,但刷新页面会生成新的SessionID。查了很多资料,大部分Session示例都是登录注册场景,找不到无登录情况下复用SessionID的可行方案,卡壳了。
当前主服务器代码
const express = require("express"); const { Server } = require('socket.io'); const cookieParser = require('cookie-parser'); const { sessionMiddleware, wrap } = require('../server/sessionStore'); const { createServer } = require("node:http"); const { join } = require("node:path"); const app = express(); const server = createServer(app); const io = new Server(server, { cors: { origin: 'http://localhost:3000', credentials: true, } }); const db = require("../database/db"); app.use(cookieParser()); app.use(sessionMiddleware); app.use(express.static("public")); app.get("/canvas", (req, res) => { if (req.session) { req.session.visited console.log(req.session); console.log(req.session.id +' connected'); } else { res.send("Invalid session"); } res.sendFile(join(__dirname, "../public/index.html")); }); const clients = {}; io.use(wrap(sessionMiddleware)); io.on("connection", (socket) => { const sessionId = socket.request.session.id; clients[sessionId] = '#000000'; db.getAllDrawingData((drawingData) => { socket.emit("loadDrawingData", drawingData); }); // start drawing event socket.on('startDrawing', ({ x, y }) => { const data = { type: 'start', x, y, color: clients[sessionId] }; db.insertDrawingData(sessionId, data); socket.broadcast.emit('startDrawing', { x, y, color: clients[sessionId] }); }); // drawing event socket.on('draw', ({ x, y }) => { const data = { type: 'draw', x, y, color: clients[sessionId] }; db.insertDrawingData(sessionId, data); socket.broadcast.emit('draw', { x, y, color: clients[sessionId] }); }); // stop drawing event socket.on('stopDrawing', () => { const data = { type: 'stop' }; db.insertDrawingData(sessionId, data); socket.broadcast.emit('stopDrawing'); }); // change stroke color event socket.on('changeStrokeColor', (color) => { clients[sessionId] = color; socket.broadcast.emit('changeStrokeColor', { socketId: sessionId, color }); }); socket.on('eraseDrawings', () => { db.eraseDrawings(sessionId); socket.broadcast.emit('eraseDrawings'); }); // disconnect event socket.on('disconnect', () => { console.log('Disconnected'); delete clients[sessionId]; }); }); // Handle server shutdown gracefully process.on('SIGINT', () => { console.log('Received SIGINT signal, shutting down...'); db.close((err) => { if (err) { console.error('Error closing SQLite database:', err); } else { console.log('SQLite database connection closed successfully.'); } process.exit(0); }); }); process.on('SIGTERM', () => { console.log('Received SIGTERM signal, shutting down...'); db.close((err) => { if (err) { console.error('Error closing SQLite database:', err); } else { console.log('SQLite database connection closed successfully.'); } process.exit(0); }); }); server.listen(3000, () => { console.log("server running at http://127.0.0.1:3000"); });
SessionStore配置代码
const session = require("express-session"); const Redis = require("ioredis"); const RedisStore = require("connect-redis").default; const { v4: uuidv4 } = require("uuid"); const config = require("./config"); const redisClient = new Redis({ host: config.redis.host, port: config.redis.port, }); redisClient.on("error", function (err) { console.log("Could not establish a connection to Redis" + err); }); redisClient.on("connect", function (err) { console.log("Connected to Redis"); }); const sessionMiddleware = session({ secret: "secret", store: new RedisStore({ client: redisClient }), resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === "production", sameSite: true, maxAge: 24 * 60 * 60 * 1000, // 30 days }, genid: (req) => uuidv4() + "-" + Date.now(), }); const wrap = (expressMiddleware) => (socket, next) => expressMiddleware(socket.request, {}, next); module.exports = { sessionMiddleware, wrap };
问题分析与修复方案
1. SessionID刷新失效的核心原因
你设置了saveUninitialized: false,但无登录场景下,初始请求的session没有被修改,导致Session不会被存入Redis,也不会在客户端设置Cookie。刷新页面时,服务器找不到对应的Session,就会生成新的ID。
2. 修复步骤
(1)确保Session被初始化存储
两种可选方案:
- 把
saveUninitialized: false改成true,允许未修改的Session被存储; - 或者在首次请求时主动修改session的某个属性,触发保存逻辑:
// 在app.get("/canvas")中修改 if (req.session) { req.session.visited = (req.session.visited || 0) + 1; // 主动修改session属性,触发存储 console.log(req.session); console.log(req.session.id +' connected'); }
这样服务器会把Session存入Redis,客户端也会收到SessionID的Cookie,刷新时就能复用。
(2)修复app.get("/")无法工作的问题
app.use(express.static("public"))会优先匹配静态文件,如果public目录下有index.html,访问/时会直接返回该文件,不会走到你的路由。解决方法:
- 把
app.get("/")放在app.use(express.static("public"))之前,让路由优先匹配:
app.get("/", (req, res) => { if (req.session) { req.session.visited = (req.session.visited || 0) + 1; console.log(req.session.id +' connected'); } res.sendFile(join(__dirname, "../public/index.html")); }); app.use(express.static("public"));
- 或者删除
public目录下的index.html,让路由处理返回文件。
(3)确保Socket连接携带Cookie
前端初始化Socket时必须开启withCredentials,否则不会携带Session Cookie:
// 前端Socket初始化示例 const socket = io('http://localhost:3000', { withCredentials: true });
(4)优化SessionID生成逻辑
express-session默认会生成唯一ID,不需要额外拼接时间戳,简化genid配置:
genid: (req) => uuidv4(),
3. 测试验证
修改后,首次访问页面时服务器会设置Session Cookie;刷新页面或关闭窗口重新打开,服务器能通过Cookie找到对应的SessionID,不会生成新ID;Socket连接时也能拿到同一个SessionID,实现用户身份的持久化。
内容的提问来源于stack exchange,提问作者Isaac Del Castillo

