ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Express 表单处理实战:用 express-validator 完成验证、净化与安全的表单 CRUD

2026/9/15 13:41:49 拓冰建站 浏览量
Express 表单处理实战:用 express-validator 完成验证、净化与安全的表单 CRUD Express 表单处理实战用 express-validator 完成验证、净化与安全的表单 CRUD【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum本指南基于 GitHub 推荐项目精选 / cu / curriculum 开源课程中 nodeJS/express/forms_and_data_handling.md 一课展开系统讲解在 Node.js / Express 应用中如何正确处理 HTML 表单数据从表单的action/method语义到使用express-validator对用户输入做验证validation与净化sanitization再到防止 XSS 的转义输出最后构建一个包含创建、更新、删除、搜索的完整用户管理示例。学完本篇你将掌握一套可复制、可运行的 Express 表单处理范式并理解数据完整性保障与 Web 安全的核心思路。HTML 表单回顾action、method 与 name 属性在处理任何后端逻辑之前先明确浏览器端表单是如何把数据送上来的。以一个收集用户全名的简单表单为例form action/create methodPOST label forfullNameFull Name:/label input placeholderJohn Doe typetext namefullName idfullName button typesubmitSubmit/button /form这里包含三层信息form元素本身通过action与method两个属性决定数据发给谁以及用什么方式发input的name属性是数据在后端的身份标识——提交后它决定了该字段在表单数据中的键名这是后端解析req.body时依赖的关键约定typesubmit的按钮负责触发提交把用户输入的数据上传到服务器。其中action与method的含义如下action提交时数据被发送去处理的目标资源/URL。如果未设置或为空字符串表单会提交回当前页面 URLmethod定义要使用的 HTTP 方法POST或GET。POST通常更安全因为敏感信息不会出现在 URL 中也就不会进入服务器日志是创建或更新服务端数据的标准选择。GET适用于不修改数据的表单如搜索表单或希望提交结果可收藏、可通过 URL 分享的场景——此时表单数据会以查询字符串的形式出现在请求 URL 中。课程 intermediate_html_css/forms/form_basics.md 也强调name属性相当于输入的变量名没有name的输入会被忽略而不参与提交同时label的for与input的id对应可以提升表单可访问性。表单处理流程与 Post/Redirect/Get 模式表单的action指向服务器上的某个端点对应的控制器controller接收请求后与数据库通信处理数据最后用控制器的响应生成新的或更新后的视图并把客户端重定向。这一流程即Post/Redirect/Get (PRG)设计模式其核心价值是防止用户刷新页面时重复提交POST请求。在 Express 中路由本质上就是把请求的 HTTP 动词GET/POST和 URL 路径匹配到一组中间件函数也就是控制器正如课程 nodeJS/express/routing.md 所描述的app.get(/, ...)匹配到/路径的 GET 请求app.post(/messages, ...)匹配到/messages路径的 POST 请求且路由按定义顺序匹配。而控制器是 MVC 模式中知道该问模型什么问题、该渲染哪个视图的中间人详见 nodeJS/express/controllers.md。验证与净化两道必须的关卡在表单数据进入服务器之前需要思考两个重要步骤验证Validation确保用户输入满足指定标准例如必填字段、正确的格式净化Sanitization通过移除或编码潜在恶意字符清理用户输入防止恶意数据被处理。二者并非必须同时进行——有时候在数据即将被使用的时刻再做净化反而更合理。净化Sanitization的定义说明有时净化被用于更严格的含义仅指移除输入数据中的字符与编码encoding即替换某些字符相区分有时它又被宽松地用于指代以某种方式改变输入数据的整个过程同时包含移除与编码字符。本课使用后一种宽泛定义。安装 express-validator在项目根目录安装依赖npm install express-validator然后引入所需函数const { body, validationResult } require(express-validator);express-validator提供了覆盖几乎所有表单操作的函数本课聚焦其中两个body()与validationResult()在完整示例中还会用到matchedData()。body() 函数指定要校验的字段body()用于指定请求体中哪些字段需要被验证和净化以及如何处理它们。它返回一个可链式调用的验证链每个body()调用构成数组中的一个元素整个数组可作为中间件数组直接传给路由[ body(birthdate, Must be a valid date.) .optional({ values: falsy }) .isISO8601() // Enforce a YYYY-MM-DD format. ];这个例子把birthdate字段标记为可选但仍对输入强制 ISO8601 日期格式。{ values: falsy }的含义是只要值不是undefined、null、false、0或空字符串就仍然执行验证——即可选但若填了就必须合法。链式验证组合多个规则与独立错误消息多个验证方法可以链式组合并为每个失败项指定独立的错误消息[ body(name) .trim() .notEmpty() .withMessage(Name can not be empty.) .isAlpha() .withMessage(Name must only contain alphabet letters.), ];这段链式规则确保name不仅存在且被trim()去除首尾空白还只能包含字母字符。withMessage()可以紧跟在任意验证方法之后覆写该规则失败时的默认错误消息这在实际项目中非常常用。类似的常见链还包括isEmail()、isInt({ min, max })、isLength({ min, max })等它们可以让校验规则表达力更强在下面的完整示例中会看到isLength的实际用法。转义用户输入防御 XSS 攻击链式验证适合确定不会包含特殊字符的字段如姓名、年龄但有些场景必须允许任意字符例如用户的About Me个人简介。此时若客户端注入 JavaScript 代码会怎样div About Me: %- description %! /div // The client then inputs the following as their pages About Me: scriptalert(Hacked!);/script未转义时它会被渲染成div About Me: scriptalert(Hacked!);/script! /div这正是典型的跨站脚本攻击Cross-Site Scripting, XSS。要防范它可以对输出做转义也称作编码转义后的 HTML 会把特殊字符如替换为对应的 HTML 实体如lt;。在 EJS 中使用% %即可转义输出div About Me: % username %! /div // The escaped output is now rendered harmless: // About Me: lt;scriptgt;alert(quot;Hacked!quot;);lt;/scriptgt;!那么为什么不干脆在接收数据时给body()验证链末尾加上.escape()一劳永逸呢原因有二危险字符只在使用它的上下文中才危险且只对特定上下文构成威胁。对 HTML 危险的内容对 SQL 未必危险反之亦然在数据进入这些上下文之前它们不会构成风险如果数据被转义后含有 HTML 实体再使用% %转义输出lt;不会变回而是作为文本lt;原样输出。此时要么先反转义再通过转义输出重新转义要么使用不转义的%- %——而这正如上面的例子所示与直接使用.innerHTML一样不可取。结论验证链中的净化如.trim()、.escape()处理的是输入时的问题输出端的转义是使用时的防线两者职责不同应按需分层使用。处理验证结果validationResult 与 400 响应验证规则应用之后用validationResult收集并处理错误const controller (req, res, next) { const errors validationResult(req); if (!errors.isEmpty()) { return res.status(400).render(index, { errors: errors.array(), }); } // do stuff if successful res.redirect(/success); };这段逻辑检查是否有任何验证失败只要errors数组非空服务器就返回400 状态码Bad Request并把错误数组传给index视图渲染否则重定向到路由中的/success。errors.array()中的每个错误对象都包含msg错误消息、path字段名与value出错的原始值等信息视图层可以直接遍历渲染。值得注意的是res.status(400)只是设置状态码并不会终止请求-响应周期必须继续链式调用res.render/res.send等方法真正发送响应这与 nodeJS/express/controllers.md 中对res.status的说明一致。表单与 Express 路由端点设计表单总得有地方发送数据。控制器先定义成普通函数exports.userUpdateGet (req, res, next) {}; exports.userUpdatePost (req, res, next) {};在路由文件中把路由分配给这些控制器函数const { Router } require(express); const usersRouter Router(); const usersController require(../controllers/usersController); // User update routes usersRouter.get(/:id/update, usersController.userUpdateGet); usersRouter.post(/:id/update, usersController.userUpdatePost); module.exports usersRouter;表单中的action则对应如下EJS 语法!-- Example using EJS with POST to submit an update to our Express server. -- form action/users/% user.userId %/update methodPOST/form/users/:id/update就是在 Express 服务器上创建的一个端点endpoint:id是路由参数可在控制器中通过req.params.id获取。注意 GET 与 POST 分别注册了同路径的不同处理器——这正是同一资源的不同操作的典型路由设计。综合实战构建带验证的用户 CRUD 应用下面把上述知识组装成一个完整的 Express 应用演示客户端如何通过表单向服务器发送数据并实现创建、更新、删除与搜索用户。1. 初始化项目与目录结构npm init -y npm install express ejs express-validator创建routes、views、controllers、storages四个目录和一个app.js入口文件// app.js const express require(express); const app express(); const path require(node:path); const usersRouter require(./routes/usersRouter); app.set(views, path.join(__dirname, views)); app.set(view engine, ejs); app.use(express.urlencoded({ extended: true })); app.use(/, usersRouter); const PORT process.env.PORT || 3000; app.listen(PORT, (error) { if (error) { throw error; } console.log(Express app listening on port ${PORT}!); });这里有一个关键知识点绝大多数简单表单在发送数据时使用Content-Type: application/x-www-form-urlencodedHTTP 头但 Express 本身无法原生解析这种数据必须借助express.urlencoded()中间件把表单数据自动填充到req.body中。当extended为false时服务器只接受string或数组类型的数据因此示例设置为true以获得更多灵活性。另外需要注意如果请求的Content-Type与application/x-www-form-urlencoded不匹配req.body会显示为空对象{}。这一点在迷你留言板项目 nodeJS/express/project_mini_message_board.md 中也有同样的实践app.use(express.urlencoded({ extended: true }))是让req.body可用的前置条件。2. 路由usersRouter// routes/usersRouter.js const { Router } require(express); const usersController require(../controllers/usersController); const usersRouter Router(); usersRouter.get(/, usersController.usersListGet); usersRouter.get(/create, usersController.usersCreateGet); usersRouter.post(/create, usersController.usersCreatePost); module.exports usersRouter;3. 视图用户列表与创建表单创建两个视图index.ejs列出所有用户createUser.ejs展示创建表单!-- views/index.ejs -- !DOCTYPE html html head title% title %/title /head body h1% title %/h1 ul % if (locals.users) { % % users.forEach(user { % li ID: % user.id %, Name: % user.firstName % % user.lastName % /li % }); % % } % /ul a href/createCreate a user/a /body /html!-- views/createUser.ejs -- !DOCTYPE html html head title% title %/title /head body h1% title %/h1 form action/create methodPOST label forfirstNameFirst Name: /label input typetext namefirstName idfirstName required label forlastNameLast Name: /label input typetext namelastName idlastName required button typesubmitSubmit/button /form a href/Back to home/a /body /html注意这里的required属性属于浏览器端 HTML 原生验证详见课程 intermediate_html_css/forms/form_validations.md它提升用户体验但绝不能替代服务端验证——任何客户端验证都可以被绕过。4. 控制器与内存存储// controllers/usersController.js const usersStorage require(../storages/usersStorage); exports.usersListGet (req, res) { res.render(index, { title: User list, users: usersStorage.getUsers(), }); }; exports.usersCreateGet (req, res) { res.render(createUser, { title: Create user, }); }; exports.usersCreatePost (req, res) { const { firstName, lastName } req.body; usersStorage.addUser({ firstName, lastName }); res.redirect(/); };控制器通过解构req.body获取表单字段——字段名正是由name属性决定的。存储层用一个类来模拟数据库交互真实项目中几乎总是使用真正的数据库这在后续课程和库存管理项目 nodeJS/express/project_inventory_application.md 中会进一步展开// storages/usersStorage.js // This class lets us simulate interacting with a database. class UsersStorage { constructor() { this.storage {}; this.id 0; } addUser({ firstName, lastName }) { const id this.id; this.storage[id] { id, firstName, lastName }; this.id; } getUsers() { return Object.values(this.storage); } getUser(id) { return this.storage[id]; } updateUser(id, { firstName, lastName }) { this.storage[id] { id, firstName, lastName }; } deleteUser(id) { delete this.storage[id]; } } // Rather than exporting the class, we can export an instance of the class by instantiating it. // This ensures only one instance of this class can exist, also known as the singleton pattern. module.exports new UsersStorage();通过module.exports new UsersStorage()导出一个实例而非类本身实现单例singleton模式——整个应用共享同一份内存数据。用node --watch app.js启动服务器后即可在http://localhost:3000/create添加新用户在http://localhost:3000/查看所有用户。5. 为创建流程接入验证与净化下面给usersController.js增加验证与净化逻辑// This just shows the new stuff were adding to the existing contents const { body, validationResult, matchedData } require(express-validator); const alphaErr must only contain letters.; const lengthErr must be between 1 and 10 characters.; const validateUser [ body(firstName).trim() .isAlpha().withMessage(First name ${alphaErr}) .isLength({ min: 1, max: 10 }).withMessage(First name ${lengthErr}), body(lastName).trim() .isAlpha().withMessage(Last name ${alphaErr}) .isLength({ min: 1, max: 10 }).withMessage(Last name ${lengthErr}), ]; // We can pass an entire array of middleware validations to our controller. exports.usersCreatePost [ validateUser, (req, res) { const errors validationResult(req); if (!errors.isEmpty()) { return res.status(400).render(createUser, { title: Create user, errors: errors.array(), }); } const { firstName, lastName } matchedData(req); usersStorage.addUser({ firstName, lastName }); res.redirect(/); } ];这里展现了几个重要的 Express 惯用法整个验证链数组validateUser作为中间件数组直接传给路由处理器先于业务逻辑执行——这正是中间件机制在验证场景的典型应用通过matchedData()取出所有已验证的数据确保拿到的数据包含净化结果例如trim()去空白后的值而不是原始req.body出错时返回 400 并重新渲染表单视图而不是重定向这样用户能立刻看到错误消息并原地修正。6. 错误消息局部模板在views下新建partials目录并创建errors.ejs局部模板!-- views/partials/errors.ejs -- % if (locals.errors) {% ul % errors.forEach(function(error) { % li% error.msg %/li % }); % /ul % } %然后在createUser.ejs的表单上方引入它%- include(partials/errors.ejs) %locals.errors判断避免了未传errors时渲染报错。表单填写不正确时页面会列出所有验证错误消息。7. 更新用户创建updateUser.ejs视图允许指定 ID 并更新字段值!-- views/updateUser.ejs -- !DOCTYPE html html head title% title %/title /head body h1% title %/h1 %- include(partials/errors.ejs) % form action/% user.id %/update methodPOST input typetext namefirstName value% user.firstName % required input typetext namelastName value% user.lastName % required button typesubmitUpdate User/button /form a href/Back to home/a /body /html修改index.ejs为每个用户加上 Update 链接li ID: % user.id %, Name: % user.firstName % % user.lastName % a href/% user.id %/updateUpdate/a /li像创建用户一样为更新添加路由usersRouter.get(/:id/update, usersController.usersUpdateGet); usersRouter.post(/:id/update, usersController.usersUpdatePost);控制器中实现 GET渲染预填表单与 POST验证后更新逻辑exports.usersUpdateGet (req, res) { const user usersStorage.getUser(req.params.id); res.render(updateUser, { title: Update user, user: user, }); }; exports.usersUpdatePost [ validateUser, (req, res) { const user usersStorage.getUser(req.params.id); const errors validationResult(req); if (!errors.isEmpty()) { return res.status(400).render(updateUser, { title: Update user, user: user, errors: errors.array(), }); } const { firstName, lastName } matchedData(req); usersStorage.updateUser(req.params.id, { firstName, lastName }); res.redirect(/); } ];更新逻辑与创建高度相似差异仅在于通过req.params.id定位要更新的用户以及失败时重新渲染updateUser视图并保留user数据用于回填表单。8. 删除用户删除操作同样在index.ejs中以表单形式实现。与更新不同删除通过POST请求发送因为DELETE方法无法由 HTML 表单原生发起所以需要一个内联表单!-- In views/index.ejs -- ul % if (locals.users) { % % users.forEach(function(user) { % li ID: % user.id %, Name: % user.firstName % % user.lastName % a href/% user.id %/updateUpdate/a !-- This time were sending a POST request to our route, so we need a form. -- form action/% user.id %/delete methodPOST styledisplay:inline; button typesubmit onclickreturn confirm(Are you sure you want to delete this user?);Delete/button /form /li % }); % % } % /ulonclickreturn confirm(...)提供了浏览器端的二次确认。控制器处理删除并重定向回首页exports.usersDeletePost (req, res) { usersStorage.deleteUser(req.params.id); res.redirect(/); };不要忘记在路由中注册新端点usersRouter.post(/:id/delete, usersController.usersDeletePost);删除场景不需要GET路由因为操作完成后直接重定向到/。至此一个完整的用户管理应用已经跑通验证链拦截非法输入validationResult驱动 400 错误渲染matchedData保证取到净化后的数据PRG 模式避免重复提交。借助express-validator配合req.body对象几乎可以处理任何形式的表单数据。实战扩展为表单增加字段与搜索能力express-validator的能力远不止于此。以下两个练习能进一步巩固所学增加用户详情字段扩展现有的User模型加入更多字段与对应验证规则Email必填格式必须正确——可使用.isEmail()验证配合.normalizeEmail()净化Age可选必须是 18 到 120 之间的数字——使用.optional({ values: falsy })配合.isInt({ min: 18, max: 120 })其语义与前面birthdate的optional({ values: falsy })完全一致Bio可选最多 200 字符——使用.isLength({ max: 200 })。同时更新视图以展示这些新字段。实现用户搜索当用户列表有成百上千条记录时需要搜索能力在视图中添加一个GET方法的表单接受name或email或两者作为搜索条件创建接受GET请求的新路由/search在控制器中编写搜索逻辑。关键点通过GET发送的表单数据不会出现在req.body中而要从req.query中读取GET 表单数据以查询字符串形式拼接在 URL 上这正是前面表单回顾部分所述methodGET的语义新建search.ejs视图展示搜索结果。这一实践也呼应了仓库中成员专属项目 nodeJS/authentication/project_members_only.md 的用法——它要求对注册表单字段做净化和验证并使用自定义验证器校验confirmPassword与密码一致、用bcrypt加密密码。自定义验证器是express-validator的重要进阶能力当内置链式方法无法表达业务规则如两次密码一致日期不能早于今天时可以用.custom()实现自己的校验逻辑。知识自检表单元素的属性有哪些它们各自的作用是什么如何在项目中安装并导入 express-validator如何使用 express-validator 验证并净化表单输入验证validation与净化sanitization的区别是什么如何在 Express 路由中处理验证错误在表单中转义 HTML 字符为什么重要进一步学习建议深入理解净化 vs 转义前者在输入阶段改变数据后者在输出阶段编码数据二者分别守护不同的防线阅读express-validator的完整 API 文档重点了解验证链Validation Chains的机制——本课用到的.trim()、.isAlpha()、.isLength()、.withMessage()、.optional()都只是验证链能力的冰山一角探索自定义验证器custom validator的官方指南它解决了内置验证器表达力不足的问题也是成员专属项目中的必备技能结合课程 nodeJS/express/controllers.md 复习中间件执行顺序、错误处理中间件与next()的语义能帮助你理解验证链作为中间件数组被注入路由时的执行时机参考课程 nodeJS/express/views.md 回顾 EJS 的% %、%- %与include语法它们是安全渲染表单与错误消息的基础。【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考