freeCodeCamp 进阶 Node.js 与 Express:编写 ensureAuthenticated 中间件保护 /profile 路由
本篇基于 freeCodeCamp 课程仓库中 Advanced Node and Express 模块的 "Create New Middleware" 挑战讲解一个典型的 Express 安全实践:当任何访客直接输入 URL 访问 /profile 时,无论其是否已登录都能看到页面——这正是需要自定义认证中间件的场景。你将学会实现 ensureAuthenticated(req, res, next) 函数,借助 Passport 的 isAuthenticated 方法检查会话状态,并通过 app.route API 把它挂在路由处理函数之前,同时理解该挑战源码级的验证方式(静态正则断言加运行时重定向测试),从而掌握"路由守卫"在 Node.js 全栈认证流程中的完整落地。
挑战定位:认证链路中的"路由守卫"环节
该挑战位于 curriculum 目录下的 Advanced Node and Express 模块(块名 advanced-node-and-express),其元数据见 curriculum/challenges/english/blocks/advanced-node-and-express/5895f70df9fc0f352b528e6a.md:
---
id: 5895f70df9fc0f352b528e6a
title: Create New Middleware
challengeType: 2
dashedName: create-new-middleware
---
该模块的任务顺序由 curriculum/structure/blocks/advanced-node-and-express.json 的 challengeOrder 数组定义,本挑战排在第 7 位:
| 顺序 | 挑战标题 | 与本挑战的关系 |
|---|---|---|
| 6 | How to Use Passport Strategies | 配置 /login 的 POST 认证,成功后 req.user 被填充 |
| 7 | Create New Middleware(本挑战) | 阻止未认证用户直接访问 /profile |
| 8 | How to Put a Profile Together | 使用 req.user.username 渲染个人主页 |
模块归属方面,curriculum/structure/superblocks/quality-assurance.json 将 advanced-node-and-express 列为该认证超块的第二个 block,前后分别是 quality-assurance-and-testing-with-chai 与 quality-assurance-projects。
原文档给出的问题背景很直接:
As is, any user can just go to
/profilewhether they have authenticated or not by typing in the URL. You want to prevent this by checking if the user is authenticated first before rendering the profile page. This is the perfect example of when to create a middleware.
也就是说,认证(authentication)只发生在 /login 提交时,而 /profile 的 GET 处理函数本身不做任何身份检查——浏览器里只要输入地址就能访问。解决思路就是把"是否已登录"这一横切关注点抽成中间件,在渲染之前统一拦截。
完整实现:ensureAuthenticated 中间件函数
原文档给出的标准实现如下(此处按可运行语法做了收尾修正,见下文"细节注意"):
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/');
}
逐行解析这三个参数与两条分支:
req:Express 请求对象。Passport 在会话机制(passport.session())启用后,会在请求对象上扩展出isAuthenticated()方法。原文档明确指出其语义:"...by calling Passport'sisAuthenticatedmethod on therequestwhich checks ifreq.useris defined",即该方法的本质是判断req.user是否已定义。而req.user的来源正是前一课 How to Use Passport Strategies 中的约定:"If the authentication was successful, the user object will be saved inreq.user"——/login使用passport.authenticate('local')认证成功后,用户对象才会落到req.user上。res:响应对象。未登录时调用res.redirect('/'),把请求重定向回首页(那里有登录表单),从而保证用户永远无法在未认证状态下拿到个人页面。next:Express 中间件链的推进器。isAuthenticated()为真时调用next(),控制权才会交给下一个处理函数(即渲染profile视图的回调);若不调用,请求将挂起无响应。
这里的 return next() 值得强调:显式 return 保证 next() 之后不会再继续执行 res.redirect('/'),避免"既放行又重定向"的重复响应。
细节注意:原代码片段的收尾写法
原挑战文档中的代码块以 }; 结尾(function 关键字的函数声明后带分号)。在 Node.js 中,function 声明语句末尾的分号不是合法语法,直接粘贴运行会报 SyntaxError;若写成表达式赋值 const ensureAuthenticated = function (req, res, next) { ... }; 则 ; 是必需的。课程沙盒的静态断言不校验语法(见下文),但本地实践时请去掉这个分号。
挂载方式:app.route 链式 API 与执行顺序
原文档要求把中间件传给 /profile 路由,且必须位于 GET 处理函数之前:
app
.route('/profile')
.get(ensureAuthenticated, (req, res) => {
res.render('profile');
});
要点说明:
app.route('/profile'):Express 的路由实例 API,返回一个app.Route对象,可以链式注册.get()/.post()等多个方法;相比app.get('/profile', ...),它的优势是多个 HTTP 方法可以共享同一路径与中间件。- 参数顺序即执行顺序:
ensureAuthenticated作为.get()的第一个参数先执行,只有它调用next()后,第二个参数(req, res) => res.render('profile')才会执行。把中间件放在处理函数"之前"(before the argument to the GET request)是本挑战的硬性要求。 - 视图渲染:处理函数最终执行
res.render('profile'),渲染views/profile.pug(该模块使用 Pug 模板引擎,见模块首课 Set up a Template Engine)。
验证机制:静态断言与运行时集成测试
挑战文档的 # --hints-- 部分内嵌了测试代码,它揭示了平台如何判定本挑战完成。测试通过 /_api/server.js 与 /_api/package.json 等接口在沙盒中读取项目文件,分两类:
1. 静态正则断言(检查代码结构)
const url = new URL("/_api/server.js", code);
const res = await fetch(url);
const data = await res.text();
assert.match(
data,
/ensureAuthenticated[^]*req.isAuthenticated/,
'Your ensureAuthenticated middleware should be defined and utilize the req.isAuthenticated function'
);
assert.match(
data,
/profile[^]*get[^]*ensureAuthenticated/,
'Your ensureAuthenticated middleware should be attached to the /profile route'
);
两条正则对应两条要求:其一,server.js 中 ensureAuthenticated 之后([^]* 为跨行的"任意内容")必须出现 req.isAuthenticated,即中间件必须用 req.isAuthenticated() 判断登录态;其二,profile 之后、get 之后必须出现 ensureAuthenticated,即中间件必须挂在 /profile 路由的 GET 上。
2. 运行时断言(检查真实行为)
const url = new URL("/profile", code);
const res = await fetch(url);
const data = await res.text();
assert.match(
data,
/Home page/,
'An attempt to go to the profile at this point should redirect to the homepage since we are not logged in'
);
测试在"没有任何已注册用户"的沙盒状态下对 /profile 发起 GET。期望的完整链路是:请求命中 ensureAuthenticated → req.isAuthenticated() 为假 → res.redirect('/') 将 302 重定向到首页 → 断言拿到的响应文本包含首页标记 /Home page/。如果中间件没挂上,或重定向目标写错,这条断言都会失败。
这套"静态结构 + 运行时行为"的双重验证,正好覆盖了本挑战的两个交付物:中间件的定义与中间件的挂载。
与前后课程的衔接:req.user 从产生到使用
把本挑战放回整条认证链路,可以看到 req.user 的"生产—守卫—消费"闭环:
- 生产:前课 Authentication Strategies 注册
passport.use(new LocalStrategy(...)),在findOne查到用户且密码匹配后done(null, user);How to Use Passport Strategies 配置app.post('/login', passport.authenticate('local', { failureRedirect: '/' })),认证成功后 Passport 将用户对象写入req.user。 - 守卫(本挑战):
ensureAuthenticated通过req.isAuthenticated()检查req.user是否存在,不存在则res.redirect('/')。 - 消费:后课 How to Put a Profile Together 在守卫通过的前提下,把
username: req.user.username传给render,并在profile.pug中用h2#welcome Welcome, #{username}!展示用户名——没有本挑战的中间件,这一步就暴露了越权风险。
从源码结构看,这也是 Passport 中间件模式的典型分层:passport.initialize() / passport.session() 属于全局中间件(挂在所有路由之前,负责重建 req.user),而 ensureAuthenticated 属于路由级中间件(只守卫特定路径),两者分工明确。
小结
本挑战的全部核心交付物可以浓缩为三点:实现一个调用 req.isAuthenticated() 的二分支中间函数;通过 app.route('/profile').get(ensureAuthenticated, handler) 把它挂在处理函数之前;未认证访问被重定向回 /。对应的仓库证据链完整:挑战正文与断言见 5895f70df9fc0f352b528e6a.md,任务顺序与模块归属见 advanced-node-and-express.json 和 quality-assurance.json,前后课衔接见 5895f70df9fc0f352b528e69.md 与 5895f70ef9fc0f352b528e6b.md。掌握了这套"定义守卫 → 挂载路由 → 重定向兜底"的写法,就可以把同样的模式推广到任何需要按会话状态放行或拦截的 Express 路由上。
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