- 后端
- Web框架
【免费下载链接】thinkjs
Use full ES2015+ features to develop Node.js applications, Support TypeScript.
本指南围绕 ThinkJS 官方安全中间件 think-csrf 展开,讲解如何在 ThinkJS 3.x 项目中为表单提交、接口请求等写操作接入基于 Session 的 CSRF Token 校验机制。读完本文,你将掌握该中间件的安装接入、参数配置、三种 Token 提交方式,以及其底层请求拦截与 Token 生成/校验的完整实现原理。
安装
think-csrf 是一个独立的 npm 包,通过 npm 安装即可:
$ npm install think-csrf --save从 package.json 可以看到,该包运行时只依赖think-helper(用于生成 Token 的 UUID 能力),而测试则依赖koa、supertest、think-payload、think-session与think-session-file,其声明的运行环境要求为 Node.js>=20.19.0。
快速接入:在中间件配置中启用
think-csrf 以 Koa 风格中间件的形式工作。ThinkJS 3.x 项目的中间件统一在应用目录下的src/config/middleware.js文件中声明,引入方式如下:
// src/config/middleware.js const csrf = require('think-csrf'); module.exports = [{ handle: csrf, options: { session_name: 'csrf_token', form_name: '_csrf', header_name: 'x-csrf-token' } }];配置项说明:
| 字段 | 含义 | 默认值 |
|---|---|---|
session_name | CSRF Token 在 Session 中存储的字段名 | 'csrf_token' |
form_name | 请求中携带 Token 的参数名(Body 与 Query 通用) | '_csrf' |
header_name | 请求 Header 中携带 Token 的字段名 | 'x-csrf-token' |
errno | 校验失败时的错误状态码 | 403 |
errmsg | 校验失败时的错误提示信息 | 'invalid csrf token' |
配置生效后,中间件会为每个「安全方法」的请求自动生成 Token,并通过ctx.csrf暴露给业务代码使用。例如在模板或接口响应中把 Token 输出给前端:
// 控制器中获取当前请求的 CSRF Token const token = this.ctx.csrf;工作原理:从源码看请求如何被拦截与放行
think-csrf 的核心实现位于 lib/csrf.js,它对外导出一个工厂函数:调用时接收options配置对象,返回一个标准的 Koa 中间件(ctx, next) => {...}。入口 index.js 只是module.exports = require('./lib/csrf.js')的转发。
整个中间件的请求分流逻辑如下:
module.exports = (options = {}) => { options = Object.assign(config, options); return (ctx, next) => { const method = ctx.method; const methods = ['GET', 'HEAD', 'OPTIONS', 'TRACE', 'WEBSOCKET', 'CLI']; if (methods.some(item => item === method)) { // 安全方法:保证 Token 存在并暴露到 ctx.csrf return utils .ensureCsrfToken(ctx, options) .then(utils.assignCsrf(ctx)) .then(_ => next()); } // 写方法:执行 Token 校验,失败则直接抛错 return utils .checkCsrf(ctx, options) .then(_ => next()); }; };可以看到两条关键路径:
- 安全方法白名单:
GET、HEAD、OPTIONS、TRACE以及WEBSOCKET、CLI请求不会校验 Token,而是走「生成/取回 Token」流程,为页面渲染或接口返回提供ctx.csrf; - 写方法校验:其余方法(如
POST、PUT、DELETE等)必须携带正确的 Token,否则抛出异常,请求被拒绝。
这种「读方法发 Token、写方法验 Token」的同步式校验,是 CSRF 防护的标准做法:攻击者无法读取到受害者的 Token,也就无法在跨站请求中伪造出合法的 Token 值。
Token 的生成与 Session 持久化
Token 的生成与读取逻辑在 lib/utils.js 的ensureCsrfToken中:
ensureCsrfToken(ctx, {session_name}) { const token = helper.uuid(32); return ctx .session(session_name) .then(value => value ? Promise.resolve(value) : ctx.session(session_name, token) .then(() => token) ); }- 首次请求时,通过
think-helper的helper.uuid(32)生成 32 位随机 Token(底层使用 Node.js 的crypto.randomUUID(),见 think-helper 的 uuid 实现),并写入 Session; - 后续请求若 Session 中已存在 Token,则直接复用,保证同一个浏览器会话内 Token 稳定不变;
- 校验时从
ctx.session(session_name)读取,若 Session 中无 Token 或与请求携带值不一致,都会调用ctx.throw(errno, errmsg)抛出错误(默认 403 /'invalid csrf token')。
需要特别说明的是:think-csrf 本身不负责 Session 与 Cookie 的建立,它依赖 ThinkJS 的ctx.session能力(由 think-session 提供),因此项目中必须预先启用并配置好 Session 组件,例如使用think-session-file或think-session-redis,否则 Token 无法持久化、校验将无法成立。
ctx.csrf 的暴露方式
在安全方法路径中,Token 通过assignCsrf注入到上下文:
assignCsrf(ctx) { return (value) => { Object.defineProperty(ctx, 'csrf', { get() { return value; } }); }; }它利用Object.defineProperty为ctx定义只读的csrfgetter,因此业务代码中可以直接通过ctx.csrf(控制器中为this.ctx.csrf)拿到当前会话的 Token,而无需再次读取 Session。
校验细节:三种 Token 携带方式
写请求的 Token 校验集中在checkCsrf中,其取值优先级为:
checkCsrf(ctx, {session_name, form_name, header_name, errno, errmsg}) { return ctx.session(session_name).then(value => { if (!value) ctx.throw(errno, errmsg); const token = ctx.query[form_name] || (ctx.request.body.post && ctx.request.body.post[form_name]) || ctx.get(header_name); if (token !== value) ctx.throw(errno, errmsg); }); }请求方携带 Token 有三种途径,按顺序匹配:
- Query 参数:
ctx.query[form_name],即 URL 中的?_csrf=xxx; - 请求体(Body):
ctx.request.body.post[form_name],即表单/JSON 请求体中携带_csrf字段; - 请求头:
ctx.get(header_name),即自定义 Headerx-csrf-token。
其中第 2 种依赖think-payload中间件解析请求体:think-payload会按 Content-Type 把请求体解析为ctx.request.body,其中post字段保存解析后的键值数据(表单用querystring.parse、JSON 用JSON.parse、multipart 用 formidable,见 think-payload 及其 form 实现、json 实现、multipart 实现)。因此在middleware.js中,payload中间件必须排在csrf之前,否则请求体尚未解析,Token 无法从 Body 中取到。
三个名称参数session_name、form_name、header_name完全可自定义,例如服务端返回的 Token 字段、前端表单字段、Header 名称都可以按项目约定修改;errno与errmsg则决定校验失败时返回的状态码与提示文案。
中间件配置机制:handle / options / enable / match
在 ThinkJS 3.x 中,middleware.js导出的数组会被 think-loader 的 Middleware 解析器 处理,每一项支持以下字段:
handle:中间件处理函数(可以是函数、字符串形式的包名,或可被调用的工厂函数),think-csrf 导出的正是「接收 options、返回中间件」的工厂函数形态;options:传给handle的配置对象,即上文的session_name等参数;enable:布尔值,为false时该中间件被过滤跳过(filter逻辑见解析器代码);match/ignore:正则或函数,用于限定中间件仅对部分路径生效。
解析器会调用handle(options, app)完成初始化,因此 think-csrf 的options会被Object.assign(config, options)与默认配置合并——你传入的配置会覆盖 config/config.default.js 中的默认值:
module.exports = { session_name: 'csrf_token', form_name: '_csrf', header_name: 'x-csrf-token', errno: 403, errmsg: 'invalid csrf token' };这也是「不传任何 options 也能直接使用」的原因所在。
测试用例:验证拦截与放行的完整行为
仓库的 test/index.js 基于node:test+supertest模拟了完整的运行环境(Koa 应用 + think-session 文件会话 + think-payload 请求体解析),覆盖了四条核心行为,可直接作为接入后的验收清单:
- GET 返回 Token:
GET /请求返回 200,响应体即ctx.csrf的字符串 Token; - 无 Token 的写请求被拦截:
POST /不带任何 Token,返回 403 且响应体为invalid csrf token; - Body 携带 Token 放行:先用
GET拿到 Token,再POST并send({_csrf_: token})(测试中自定义了form_name: '_csrf_'以验证 options 覆盖),返回 200; - Header 携带 Token 放行:同样先取 Token,再通过
set('x-csrf-token', token)放入请求头提交,返回 200。
这组测试同时也印证了:Token 取自 Session 且在同一会话(supertest的agent自动维护 Cookie)内保持稳定,form_name等选项确实可以按需覆盖。
实战建议与注意事项
- 务必先启用 Session:think-csrf 的 Token 存储在 Session 中,请确保项目已配置
think-session系列组件(文件、Redis 或 MySQL 等),并在middleware.js中让 session 扩展先于 csrf 生效; - payload 中间件顺序:若希望支持在请求体中携带 Token,
payload中间件必须位于csrf之前,保证ctx.request.body.post已被解析; - 前端配套:页面或接口返回
ctx.csrf后,前端在提交表单时把 Token 放入_csrf字段(或 Query、Header),三选一即可; - 仅保护写方法:
GET、HEAD、OPTIONS、TRACE以及WEBSOCKET、CLI请求默认免校验,符合 CSRF 攻击面仅限于「会改变状态的请求」的防护原则; - 错误处理:校验失败时中间件通过
ctx.throw(errno, errmsg)抛出错误,可被 ThinkJS 的 trace 中间件统一捕获并渲染为 403 响应,无需在业务代码中额外处理。
如果需要在真实的 ThinkJS 3.x 项目里查看中间件的整体编排(meta、resource、trace、payload、router、logic、controller 的先后顺序),可以参考仓库中的 think-demo 示例配置。按上述步骤接入并验证后,你的项目即可获得完整的、基于 Session 的 CSRF 防护能力。
- 后端
- Web框架
【免费下载链接】thinkjs
Use full ES2015+ features to develop Node.js applications, Support TypeScript.
相关推荐
Fresh 框架 CSRF 防护中间件 `csrf()` 实战指南:基于 Sec-Fetch-Site 与 Origin 的跨站请求伪造防护
Fresh 框架 CSRF 防护中间件 csrf 实战指南:基于 Sec Fetch Site 与 Origin 的跨站请求伪造防护 导读 本文聚焦 Fresh
后端前端ThinkJS 3.x 路由中间件 think-router 实战指南:从配置到源码解析
ThinkJS 3.x 路由中间件 think router 实战指南:从配置到源码解析 导读 think router 是 ThinkJS 3.x 的核心路由
后端Web框架Go Web 应用 CSRF 防护实战:从攻击原理到 gorilla/csrf 中间件落地
Go Web 应用 CSRF 防护实战:从攻击原理到 gorilla/csrf 中间件落地 本文基于 Go SCP(Golang Secure Coding P
文档教程应用安全
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考