Remix 3 的 CSRF 防护中间件怎么配置
【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix
在一个基于fetch-router的 Remix 3 应用中,POST、PUT、PATCH、DELETE这类会改变状态的请求默认没有任何防跨站请求伪造(CSRF)的屏障。本文的目标是完成一件具体的事:在路由的中间件链里启用remix/middleware/csrf提供的csrf()中间件,让携带 session cookie 的危险方法请求必须同时通过"来源校验 + 同步化令牌比对"才能放行,并知道怎样验证配置确实生效。
仓库中 remix 包的版本 当前为3.0.0-rc.2,要求 Node>=24.3.0;安装命令是文档给出的npm i remix,CSRF、Session、Cookie 相关模块都随remix包一起提供。
前提:session 中间件必须先于 csrf 运行
csrf()的令牌存储在服务端 session 里,因此它硬性依赖session()中间件先执行。如果你把csrf()放在session()前面,或完全漏掉 session 中间件,中间件会直接抛出错误:
csrf middleware requires session() middleware to run before it见 csrf 中间件实现。session cookie 还必须是签名的,session-middleware 的 README 明确说明"session cookies must be signed"以防止客户端篡改会话数据。
如果令牌通过隐藏表单字段提交,还需要formData()中间件把请求体解析出来,否则_csrf字段读不到——token 表单字段来源在 README 中标注了这条依赖。
配置步骤:挂载中间件并注入令牌
完整的最小配置来自 csrf-middleware README 的 Usage 示例:
import { createCookie } from 'remix/cookie' import { createRouter } from 'remix/router' import { createCookieSessionStorage } from 'remix/session-storage/cookie' import { session } from 'remix/middleware/session' import { csrf, getCsrfToken } from 'remix/middleware/csrf' let sessionCookie = createCookie('__session', { secrets: ['secret1'] }) let sessionStorage = createCookieSessionStorage() let router = createRouter({ middleware: [session(sessionCookie, sessionStorage), csrf()], }) router.get('/form', (context) => { let token = getCsrfToken(context) return new Response(` <form method="post" action="/submit"> <input type="hidden" name="_csrf" value="${token}" /> <button type="submit">Submit</button> </form> `) })需要按自己项目替换的只有两处:secrets: ['secret1']换成你自己的签名密钥(session-middleware 的示例 用了secrets: ['s3cr3t'],并建议配置secure: true, sameSite: 'lax'),以及表单页面换成你自己的路由。getCsrfToken(context)的语义是"取 session 里的令牌,没有就创建一个",所以渲染任何含危险方法表单的页面时调用一次即可,令牌会随响应里的 session cookie 持久化。
中间件顺序方面,仓库内的 timeboxer 示例应用 展示了一个更完整的实际写法,把表单解析放在 csrf 之前:
export const router = createRouter({ middleware: [ session(sessionCookie, sessionStorage), formData(), csrf(), // ...其余业务中间件 ], })其中 session 配置 使用环境变量SESSION_SECRET作为 cookie 签名密钥,非测试环境下缺失会直接抛错SESSION_SECRET is required——如果你的会话密钥也来自环境变量,需要保证它先于应用启动就已设置。
令牌从哪里读、往哪里传
对不安全方法(POST、PUT、PATCH、DELETE),csrf()按以下顺序查找提交的令牌(见 README 的 Token Sources 一节 与 实现):
- 请求头:
X-Csrf-Token、X-Xsrf-Token、Csrf-Token(按此顺序) - 表单字段:
_csrf(依赖formData()中间件解析请求体) - 查询参数:
_csrf
文档给出的立场很明确:请求头和隐藏表单字段是推荐传输方式;查询参数仅为兼容性保留,因为令牌更容易出现在日志、历史和被复制的 URL 里,是最弱的选项。文档还提示"不要把它作为默认推荐"。
GET、HEAD、OPTIONS默认属于安全方法,不做 CSRF 校验,会直接放行;这一行为由safeMethods选项控制。
来源校验与可选配置项
csrf()在令牌比对之外还做来源校验。默认行为:当请求带Origin或Referer头时,按同源校验;allowMissingOrigin默认为true,即两个来源头都缺失时请求仍然通过(只要令牌有效)。
CsrfOptions 接口 支持的配置项:
| 选项 | 默认值 | 用途 |
|---|---|---|
tokenKey | '_csrf' | session 中存放令牌的键 |
fieldName | '_csrf' | 读取令牌的表单字段名 |
headerNames | ['X-Csrf-Token', 'X-Xsrf-Token', 'Csrf-Token'] | 依次检查的头部名 |
safeMethods | ['GET', 'HEAD', 'OPTIONS'] | 不校验的 HTTP 方法 |
origin | 同源校验 | 允许的跨域来源,可为字符串、正则、数组或回调函数 |
allowMissingOrigin | true | 是否放行缺少Origin/Referer的请求 |
value | 默认查找逻辑 | 自定义令牌提取函数 |
onError | 内置 403 响应 | 自定义拒绝响应 |
如果你的部署希望强制要求危险请求必须携带来源头,把allowMissingOrigin设为false;如果表单会来自另一个已知域名,通过origin传入该域名、匹配正则或数组。
如何验证配置生效
仓库的 timeboxer 安全测试 就是官方给出的验证方式,它覆盖了"拒绝"和"放行"两个方向,可以直接照此手工验证:
应被拒绝的请求——带合法 session cookie 但不带令牌或带错误令牌的POST/PUT/DELETE,响应应为403,且响应体精确等于(见测试中的断言 assertCsrfRejection):
Forbidden: missing CSRF token或
Forbidden: invalid CSRF token来源非法时的响应体是Forbidden: invalid CSRF origin(见 实现中的默认错误响应),以上三段文案在未配置onError时固定如此。
应被放行的请求——先从页面里提取渲染出的<input name="_csrf">令牌和 session cookie,再带着该令牌提交:测试里注册接口带有效令牌返回303,创建接口返回201。
一个可用的手工流程(等价于测试中的做法):
GET一个会渲染_csrf隐藏字段的页面,从 HTML 中提取name="_csrf"的value,并记录响应Set-Cookie中的 session cookie;- 用该 cookie 向同一动作地址发
POST,不带_csrf字段,预期得到403和Forbidden: missing CSRF token; - 再发一次并带上
_csrf字段(或X-Csrf-Token头),预期进入正常业务逻辑(测试中的成功状态码303/201取决于路由自身,不代表固定值)。
边界与限制
- 文档强调:同步化令牌才是
csrf()的主防线,Origin/Referer检查只是附加信号,不能当作唯一保护。 - 如果你的部署能稳定依赖
Sec-Fetch-Site和Origin头,cop()(cop-middleware)提供的无令牌跨源防护可能是更合适的选择,也可以在csrf()之前叠加一层cop()做提前拦截。README 的原话是"如果部署能保证无令牌模型的全部前提,这个中间件是可选的"。 - 令牌比对使用恒定时间比较实现(constantTimeEqual),避免时序侧信道;令牌由
crypto.getRandomValues生成 32 字节随机数。 - 更上层的防护思路(cookie 签名密钥轮换、session 生命周期管理、CORS 不等于 CSRF 防护)见 docs/guides 的 Auth, Sessions, and Security 章节,其中明确建议"session 中间件先于
csrf()运行,表单解析先于_csrf令牌提取"。
【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考