freeCodeCamp 课程实战:在 Node.js + Express 项目里用 passport.socketio 为 Socket.IO 实现身份认证
这篇指南基于 freeCodeCamp 课程仓库中 "Advanced Node and Express" 模块的 "Authentication with Socket.IO" 编程挑战(挑战文档),讲解为什么 WebSocket 连接上拿不到 req.user,以及如何用 passport.socketio + connect-mongo + cookie-parser 三件套,让 Socket.IO 复用 Express 会话中的 Passport 身份,最终在 socket.request.user 上直接读取到已登录用户对象。读完后,你将掌握在已有 Express/Passport 认证体系上为 Socket.IO 连接做授权的标准流程,理解 key(会话 cookie 名)、secret、store 三个核心配置项的由来,并知道测试脚本是如何验证这套配置的。
背景问题:WebSocket 连接上没有 req.user
在标准 Express 请求/响应模型中,Passport 认证完成后,req.user 里就存着当前用户对象,这是前面若干挑战(如 Passport 序列化用户对象)建立起来的基础。但一旦引入 Socket.IO 长连接,情况就不一样了:
- 浏览器与服务器之间的 WebSocket 通道不经过 Express 的 HTTP 请求管线,因此没有
req(request)对象,自然也就没有req.user; - 此时服务器无法判断"这个 socket 是谁的",对于聊天室这类需要区分用户的场景是致命的。
挑战文档给出的思路是:浏览器在发起 Socket.IO 握手请求时,仍会携带 HTTP 请求头里的 Cookie,其中就包含 express-session 写入的会话 Cookie。只要把这个 Cookie 解析出来、用会话密钥解码出会话 id,再到 session store 中反序列化出用户对象,问题就解决了。passport.socketio 这个 npm 包正是把这条"复杂链路"封装成了一步配置。
依赖准备与 Require 方式
挑战说明中,以下三个包已经被添加为项目依赖(学员无需手动 npm install):
| 包名 | 版本范围 | require 后的变量名 | 职责 |
|---|---|---|---|
passport.socketio |
~3.7.0 |
passportSocketIo |
提供 Socket.IO 认证中间件 authorize() |
connect-mongo |
~3.2.0 |
MongoStore |
将 express-session 的会话数据持久化到 MongoDB |
cookie-parser |
~1.4.5 |
cookieParser |
解析请求 Cookie,供 passport.socketio 读取会话 id |
三个包分别 require 为 passportSocketIo、MongoStore、cookieParser 即可,其中后两者是工厂式的引入方式。
创建 MongoDB 会话存储 MongoStore
passport.socketio 需要一个能根据会话 id 找到会话数据的存储,这里使用 connect-mongo 提供的 MongoStore。挑战文档要求"初始化一个新的 memory store(即 session store),来自之前已经 require 的 express-session",代码如下:
const MongoStore = require('connect-mongo')(session);
const URI = process.env.MONGO_URI;
const store = new MongoStore({ url: URI });
几个要点:
require('connect-mongo')(session)是connect-mongo@~3.2.0的典型用法:先调用工厂函数并传入express-session模块,返回MongoStore构造函数;- 连接串来自环境变量
MONGO_URI,与项目中 mongoose 使用的数据库保持一致,因此会话数据与用户数据落在同一个 MongoDB 实例上; - 这个
store稍后会被同时交给session中间件和passport.socketio使用——这正是它能"认得"会话的关键。
用 io.use 注册 passport.socketio 认证中间件
在 Socket.IO 侧,需要用 io.use() 注册认证中间件。注意挑战文档强调:这段代码必须加在已有的 socket 代码之前,而不是放在现有的 connection 监听器内部。 完整配置如下:
io.use(
passportSocketIo.authorize({
cookieParser: cookieParser,
key: 'express.sid',
secret: process.env.SESSION_SECRET,
store: store,
success: onAuthorizeSuccess,
fail: onAuthorizeFail
})
);
逐项解析 authorize() 的选项:
| 选项 | 取值 | 说明 |
|---|---|---|
cookieParser |
cookieParser |
passport.socketio 用它从握手请求中解析出 Cookie 字符串 |
key |
'express.sid' |
会话 Cookie 的名称,必须与 session 中间件使用的 key 完全一致 |
secret |
process.env.SESSION_SECRET |
签名/校验会话所需的密钥,与 session 中间件配置的 secret 同源 |
store |
上面的 store |
会话存储实例,用于根据会话 id 反序列化出 user |
success / fail |
onAuthorizeSuccess / onAuthorizeFail |
认证通过/失败时的回调函数 |
挑战文档特别指出:这套配置与之前给 API 配置 session 中间件的方式非常相似——因为它们本质上使用同一种认证机制:从 Cookie 中取会话 id,再到 store 中验证。
关键坑:key 必须在两处显式声明
这是本挑战最容易踩的坑,文档用了整整两段来强调:
- 之前配置
session中间件时,没有显式设置会话 Cookie 的名称(key),因为express-session使用了默认值; - 现在又加入了一个需要从同一枚 Cookie 读取相同值的包(
passport.socketio),两边必须显式声明同一个key。
因此需要做两件事:
- 给
session中间件补上key: 'express.sid',与上面 Socket.IO 配置中的key保持一致; - 在
session中间件的 options 中、靠近设置saveUninitialized: true的位置,补上store: store。
这样 Socket.IO 认证时才知道"该去哪个 store 里、按哪枚 Cookie 关联到哪个会话"。express.sid 正是 express-session 默认的会话 Cookie 名,显式写出来只是让两个包"说同一种语言"。
定义 success 与 fail 回调
中间件认证完成后会调用你提供的回调,挑战文档要求的实现如下:
function onAuthorizeSuccess(data, accept) {
console.log('successful connection to socket.io');
accept(null, true);
}
function onAuthorizeFail(data, message, error, accept) {
if (error) throw new Error(message);
console.log('failed connection to socket.io:', message);
accept(null, false);
}
回调约定的参数与行为:
onAuthorizeSuccess(data, accept):data是解析出的用户数据;必须调用accept(null, true)放行该连接。这里的accept(null, true)第一个参数为null表示无错误,第二个布尔值决定连接是否被接受;onAuthorizeFail(data, message, error, accept):如果error存在,直接抛出携带message的异常(例如会话密钥不匹配、store 中查不到会话等"硬错误");否则记录message并调用accept(null, false)拒绝这条 socket 连接——未认证用户将无法与服务器建立 Socket.IO 通道。
验证成果:socket.request.user 可用
配置完成后,用户对象会挂在 socket 对象上:
console.log('user ' + socket.request.user.username + ' connected');
把这一行放进 io.on('connection', ...) 监听器,服务器控制台就会打印出"谁"连接进来了。从源码结构看,这个能力正是后续挑战直接依赖的——紧随其后的 Announce New Users 挑战 就是基于 socket.request.user.username 向全体在线用户广播"某用户加入/离开了聊天室":
io.emit('user', {
username: socket.request.user.username,
currentUsers,
connected: true
});
而本挑战又建立在 Set up the Environment 之上:那个挑战先用 http.createServer(app) + require('socket.io')(http) 把 Socket.IO 挂到 Express 应用上,并注册了第一个 io.on('connection', ...) 监听器——本挑战的 io.use(...) 就加在它之前。这三个挑战在 advanced-node-and-express 模块的 challengeOrder 中依次排为 589fc830f9fc0f352b528e74(Set up the Environment)、589fc831f9fc0f352b528e77(Authentication with Socket.IO)、589fc832f9fc0f352b528e78(Announce New Users),构成"搭建通道 → 通道加身份 → 用身份做广播"的完整链路。
测试脚本如何验证你的实现
该挑战的 hints(同样写在挑战文档末尾)是服务端断言,从四个角度检查学员提交的项目,值得照此自查:
package.json依赖检查:断言dependencies中同时存在passport.socketio和cookie-parser(分别通过/_api/package.json获取);- require 检查:断言
server.js中匹配require('passport.socketio')或require("passport.socketio")(正则require\((['"])passport\.socketio\1\),引号成对); - 中间件注册检查:断言
server.js中匹配io.use(\s*\w+\.authorize(,即必须以 Socket.IO 中间件形式调用authorize(); - (文档描述层面还要求)
key、secret、store选项齐全,且session中间件与 Socket.IO 的key一致。
小结
本挑战的完整落地清单可以概括为六步:
require三个包:passportSocketIo、MongoStore、cookieParser;- 用
new MongoStore({ url: process.env.MONGO_URI })创建会话存储store; - 在
session中间件中显式补上key: 'express.sid'与store: store; - 在 socket 代码之前注册
io.use(passportSocketIo.authorize({...})),key/secret/store与 session 侧对齐; - 实现
onAuthorizeSuccess/onAuthorizeFail,分别accept(null, true)与accept(null, false); - 在
connection监听器里通过socket.request.user读取用户名,验证认证生效。
这套方案的通用价值在于:它没有另造一套 WebSocket 认证(如 JWT 放 Authorization 头),而是完全复用了浏览器端已有的 express-session + Passport 体系,改动面最小、与 HTTP 侧行为天然一致——这也是它被选入 freeCodeCamp 课程的原因。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00