- 后端
- Web框架
- MCP 服务
【免费下载链接】cjoy
一个高性能、可扩展、轻量、省心的仓颉应用开发框架。IoC,Rest,宏路由,Json,中间件,参数绑定与校验,文件上传下载,OAuth2,MCP......
CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种诱导已认证用户在目标网站上执行非预期操作的攻击方式。CJoy 内置的CsrfProtect中间件基于 Session 存储的随机 Secret,通过「SHA256 哈希 + Base64 编码」生成 CSRF Token 并在请求回程中校验,为表单提交、AJAX 等状态改变请求提供开箱即用的防护。本文基于 doc/usage_guide/csrf.md 与 src/middleware/csrf.cj 源码,系统讲解 CSRF 攻击原理、中间件工作原理、全部配置选项、表单与 AJAX 两种接入方式及安全部署注意事项,读完即可在 CJoy 应用中快速落地一套可用的 CSRF 防护方案。
一、CSRF 攻击与 Token 防护机制概述
1.1 什么是 CSRF 攻击
CSRF 攻击的核心场景是:攻击者构造一个恶意页面,诱导已经登录目标网站的受害者浏览器,在不知情的情况下向目标网站发起携带合法 Cookie 的请求(例如转账、改密、发帖)。由于浏览器会自动附带 Cookie,服务器无法区分该请求是用户主动发起还是被攻击者劫持发起,从而造成非预期操作。
CJoy 的CsrfProtect中间件通过Synchronizer Token Pattern(同步令牌模式)应对此类攻击:只有请求携带的 Token 与服务器端 Session 中保存的 Secret 派生出的 Token 一致时,才认为请求真实有效。
1.2 CJoy 中间件的核心设计
CsrfProtect基于以下核心概念设计(参见 src/middleware/csrf.cj 源码):
- Token 生成:基于 SHA256 哈希 + Base64 编码生成 CSRF Token;
- Session 集成:Secret 存储在 Session 中,Token 与 Session 生命周期绑定,确保 Token 安全;
- 灵活配置:支持从 Header、Query、Form、Param 等位置获取 Token;
- 过滤函数:支持跳过特定请求的 CSRF 验证;
- 自定义错误处理:支持自定义验证失败响应,默认返回 401。
核心类为CsrfProtect,它继承JoyMiddlewareHandler,通过joy.router.use()挂载到全局或分组路由上。
二、工作原理
CsrfProtect的完整校验流程分为生成与验证两个阶段:
- 服务器生成随机 Secret:在中间件首次处理请求时,将构造函数传入的
secret写入当前 Session(对应源码中的csrfSecretCtxKey = "__csrf_secret__"); - 生成 CSRF Token:首次需要 Token 时,先从 Session 读取(或新生成)一个随机 Salt(对应
csrfSaltCtxKey = "__csrf_salt__",由 src/utils/rand.cj 的secureRandStr(16)使用SecureRandom生成),然后计算SHA256("${salt}-${secret}")并将摘要字节经 Base64 URL 安全编码(无填充,+//替换为-/_,参见 src/utils/base64.cj 的rawBase64URLEncode)得到 Token,缓存到 Session 中(csrfTokenCtxKey); - 客户端携带 Token:客户端在表单隐藏域、请求头或查询参数中携带此 Token;
- 服务器校验:对需要保护的请求,从指定位置提取 Token,用相同的 Salt 与 Secret 重新计算并比对,一致则放行,否则触发错误处理。
从源码实现看(src/middleware/csrf.cj),中间件handle的执行顺序是:先执行过滤函数(若命中则直接chain.next跳过验证)→ 写入 Secret → 判断ignoreMethods(放行无需保护的 HTTP 方法)→ 检查 Session 中是否存在 Salt → 提取 Token 并比对。由于攻击者无法读取响应内容,也就无法得知 Token 值,伪造请求自然无法通过校验。
三、基本用法:与 Session 配合注册
CsrfProtect必须配合 SessionManager 使用,因为 Token 的 Secret 与 Salt 都存放在 Session 中。注册顺序为:先注册 Session 中间件,再注册 CsrfProtect 中间件。
import cjoy.middleware.{SessionManager, CsrfProtect, getSession} import cjoy.middleware.JoySessionConfig import cjoy.session.MemorySessionStorage main(): Int64 { let joy = Joy.default() // 1. 注册 Session 中间件(CsrfProtect 依赖 Session) let sessionManager = SessionManager(MemorySessionStorage()) joy.router.use(sessionManager) // 2. 注册 CsrfProtect 中间件 let csrf = CsrfProtect( secret: "your-secret-key-here" ) joy.router.use(csrf) joy.run("127.0.0.1", 8080) return 0 }关于 Session 层,仓库提供了JoySessionConfig(见 src/middleware/session.cj)用于精细控制 Cookie 行为,例如:
let config = JoySessionConfig( cookieName: "sid", // 会话 Cookie 名称,默认 "sid" cookiePath: "/", // Cookie 路径,默认 "/" cookieSecure: true, // 生产环境建议 true(仅 HTTPS 传输) cookieHttpOnly: true, // 默认 true,禁止脚本访问,缓解 XSS 窃取 cookieMaxAge: 3600, // Cookie 生命周期(秒),默认 3600 idLength: 32 // 会话 ID 长度,默认 32 ) let sessionManager = SessionManager(MemorySessionStorage(), config: config)其中MemorySessionStorage(见 src/session/memory_session.cj)默认会话最大存活时间为 1 小时,并通过 src/session/session_base.cj 中的clean()周期清理过期会话——这意味着 Session 过期后,其内的 CSRF Secret 与 Salt 一并失效,Token 也随之失效。
四、获取 CSRF Token
在渲染表单、返回页面或初始化前端时,使用getCsrfToken(ctx)函数(src/middleware/csrf.cj)生成或获取当前会话的 CSRF Token。该函数会先读取 Session 中的 Secret,若 Session 中已缓存 Token 则直接复用;否则生成 Salt 并计算 Token 后写入 Session 返回。其典型用法是将 Token 嵌入 HTML 表单的隐藏域:
import cjoy.middleware.getCsrfToken @Get[path = "/form"] func showForm(ctx: JoyContext): Unit { // 获取 CSRF Token let token = getCsrfToken(ctx) // 在 HTML 表单中使用 let html = """ <html> <body> <form method="POST" action="/submit"> <input type="hidden" name="csrf_token" value="${token}" /> <input type="text" name="username" /> <button type="submit">提交</button> </form> </body> </html> """ ctx.html(html) }仓库中的官方示例 examples/src/csrf/main.cj 给出了更精简的两种用法:通过@Get[path = "/get_token"]路由直接返回 Token 字符串供前端获取,再在@Post[path = "/submit_form"]路由中携带 Token 提交。Token 可以放在表单、Header 等位置(位置由下文lookupKey决定)。
五、配置选项详解
CsrfProtect的构造函数支持五个参数,见 src/middleware/csrf.cj:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| secret | String | - | 用于生成 Token 的密钥 |
| ignoreMethods | Array<String> | ["GET", "HEAD", "OPTIONS"] | 不需要 CSRF 保护的 HTTP 方法 |
| lookupKey | String | "header:X-CSRF-TOKEN" | 从请求中提取 Token 的位置 |
| filterHandler | JoyRequestFilterFunc | None | 用于跳过 CSRF 验证的过滤函数 |
| errorHandler | JoyRequestHandlerFunc | 返回 401 | CSRF 验证失败时的处理函数 |
其中默认的ignoreMethods对应框架常量 src/framework/consts.cj 中的METHOD_GET、METHOD_HEAD、METHOD_OPTIONS,即默认对 GET、HEAD、OPTIONS 请求不做校验。
5.1 ignoreMethods:指定无需保护的 HTTP 方法
// 默认不保护 GET、HEAD、OPTIONS let csrf = CsrfProtect(secret: "my-secret") // 只保护 POST 方法 let csrf = CsrfProtect( secret: "my-secret", ignoreMethods: ["GET", "HEAD", "OPTIONS", "PUT", "DELETE", "PATCH"] )5.2 lookupKey:指定 Token 提取位置
lookupKey的格式为<source>:<name>,支持四种来源:
| source | 说明 | 底层取值 API |
|---|---|---|
| header | 从 HTTP 请求头获取 | ctx.getHeader(name) |
| query | 从 URL 查询参数获取 | ctx.getQuery(name) |
| form | 从 POST 表单获取 | ctx.getPostForm(name) |
| param | 从 URL 路径参数获取 | ctx.getParam(name) |
// 从请求头获取(默认) let csrf = CsrfProtect( secret: "my-secret", lookupKey: "header:X-CSRF-TOKEN" ) // 从表单获取 let csrf = CsrfProtect( secret: "my-secret", lookupKey: "form:csrf_token" ) // 从查询参数获取 let csrf = CsrfProtect( secret: "my-secret", lookupKey: "query:token" )从源码createExtractFunc(src/middleware/csrf.cj)可以确认:lookupKey必须以:分隔为恰好两段,source只能是header、query、form、param之一,否则会抛出IllegalArgumentException(格式或来源非法时均有明确的错误提示)。提取不到对应值时返回空字符串,后续校验会因「miss token」而触发错误处理。
5.3 filterHandler:按请求跳过验证
过滤函数返回true时跳过 CSRF 验证(例如公开 API、Webhook 回调等场景):
let csrf = CsrfProtect( secret: "my-secret", filterHandler: { ctx: JoyContext => // 公开 API 不需要 CSRF 保护 ctx.path.startsWith("/api/public/") } )5.4 errorHandler:自定义验证失败响应
let csrf = CsrfProtect( secret: "my-secret", errorHandler: { ctx: JoyContext => ctx.status(401).json(@ToJson({ "code": 401, "message": "CSRF 验证失败,请刷新页面重试" })) } )若未配置errorHandler,则使用内置的DefaultCsrfErrorHandler(src/middleware/csrf.cj):它通过@JoyLogError记录一条包含客户端 IP、请求方法与 URL 的错误日志,并返回 HTTP 401 状态码。源码中三类失败场景都会触发该处理器并各自记录日志:miss salt(Session 中无 Salt)、miss token in <source>(请求中未携带 Token)、invalid token(Token 不匹配),这些日志可用于监控和追踪 CSRF 攻击尝试。
六、完整示例:表单场景
以下完整示例(覆盖 doc/usage_guide/csrf.md 第五节,并保持可运行性)演示了「GET 渲染带 Token 的表单 → POST 提交校验」的完整链路:
import cjoy.Joy import cjoy.middleware.{SessionManager, CsrfProtect, getSession, getCsrfToken} import cjoy.middleware.JoySessionConfig import cjoy.session.MemorySessionStorage import cjoy.json.macros.* main(): Int64 { let joy = Joy.default() // 注册 Session let sessionManager = SessionManager(MemorySessionStorage()) joy.router.use(sessionManager) // 注册 CsrfProtect let csrf = CsrfProtect( secret: "my-csrf-secret-key", lookupKey: "form:csrf_token" ) joy.router.use(csrf) // 显示表单(GET 请求不需要 CSRF 验证) joy.router.get("/form", {ctx: JoyContext => let token = getCsrfToken(ctx) ctx.html(""" <!DOCTYPE html> <html> <head><title>CSRF 保护表单</title></head> <body> <h1>CSRF 保护表单示例</h1> <form method="POST" action="/submit"> <input type="hidden" name="csrf_token" value="${token}" /> <label>用户名: <input type="text" name="username" /></label><br> <button type="submit">提交</button> </form> </body> </html> """) }) // 处理表单提交(POST 请求需要 CSRF 验证) joy.router.post("/submit", {ctx: JoyContext => if (let Some(username) <- ctx.getPostForm("username")) { ctx.json(@ToJson({ "message": "提交成功", "username": username })) } else { ctx.json(@ToJson({"message": "用户名不能为空"})) } }) joy.run("127.0.0.1", 8080) return 0 }运行后访问http://127.0.0.1:8080/form可看到隐藏了csrf_token的表单;直接向/submit发起不含 Token 的 POST 请求会得到 401 响应,而携带合法 Token 的提交则会成功。
七、AJAX 请求接入
对于单页应用(SPA)或前端框架场景,表单隐藏域不可用,需要在请求头中携带 CSRF Token:
// 前端获取 Token 并存储 const token = document.querySelector('input[name="csrf_token"]').value; localStorage.setItem('csrfToken', token); // 发送 AJAX 请求时携带 Token fetch('/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': localStorage.getItem('csrfToken') }, body: JSON.stringify({ data: 'value' }) });服务器端配置为从请求头提取:
let csrf = CsrfProtect( secret: "my-secret", lookupKey: "header:X-CSRF-TOKEN" // 从请求头获取 ) joy.router.use(csrf)需要注意的是,Token 与 Session 绑定:若 Token 在 localStorage 中长期保存而 Session 已过期(例如超过MemorySessionStorage默认的 1 小时),校验会因 Session 中 Salt 缺失而失败,此时应引导用户重新获取 Token。因此实践中更推荐每次进入页面时通过getCsrfToken重新获取最新 Token,并在每次状态改变请求(POST、PUT、DELETE、PATCH 等)中携带。
八、注意事项与安全实践
- Session 依赖:
CsrfProtect必须配合SessionManager使用,且需先注册 Session 再注册 CSRF 中间件,否则getSession(ctx).getOrThrow()会因取不到 Session 而抛出异常; - Secret 密钥:使用强随机密钥,不要在代码中硬编码(建议通过配置中心或环境变量注入);
secret是 Token 计算的关键输入,泄露后攻击者可自行推导合法 Token; - Token 生命周期:Token 与 Session 绑定,Session 失效后 Token 也会失效;Token 在同一 Session 内首次生成后即被缓存复用,无需每次请求重新生成;
- GET 请求:默认 GET、HEAD、OPTIONS 请求不需要 CSRF 验证,因为 CSRF 攻击通常依赖表单 POST 或 AJAX 提交等状态改变方法;
- 前端框架:使用前端框架时,需要将 Token 注入到每个状态改变请求中(请求头、表单字段或查询参数均可,注意与
lookupKey保持一致); - HTTPS:生产环境必须使用 HTTPS,防止 Token 在传输中被中间人窃取;
- SameSite Cookie:配合
HttpOnly和SecureCookie 使用增强安全性——JoySessionConfig默认已开启cookieHttpOnly,生产环境应将cookieSecure设为true;设置SameSite属性可进一步阻止跨站携带 Cookie; - 日志记录:验证失败会通过
@JoyLogError记录错误日志(含客户端 IP、请求方法与 URL),可用于监控和识别攻击行为; - 公开接口豁免:对无需防护的公开 API、Webhook 等,使用
filterHandler显式跳过,而非放宽ignoreMethods或关闭整个中间件。
九、小结
CJoy 的CsrfProtect中间件以 Session 为信任根,将「密钥 + 随机 Salt → SHA256 摘要 → Base64 URL 编码」的 Token 机制与灵活的提取位置(header/query/form/param)、方法白名单(ignoreMethods)、请求过滤(filterHandler)和自定义错误处理(errorHandler)组合在一起,实现了对状态改变请求的透明防护。其核心实现集中在 src/middleware/csrf.cj,配套的 Session 基础位于 src/session/ 与 src/middleware/session.cj,可直接运行的示例见 examples/src/csrf/main.cj。按本文步骤完成注册与 Token 下发后,即可在表单、AJAX 等多种场景下获得 CSRF 防护能力。
- 后端
- Web框架
- MCP 服务
【免费下载链接】cjoy
一个高性能、可扩展、轻量、省心的仓颉应用开发框架。IoC,Rest,宏路由,Json,中间件,参数绑定与校验,文件上传下载,OAuth2,MCP......
相关推荐
Fresh 框架 CSRF 防护中间件 `csrf()` 实战指南:基于 Sec-Fetch-Site 与 Origin 的跨站请求伪造防护
Fresh 框架 CSRF 防护中间件 csrf 实战指南:基于 Sec Fetch Site 与 Origin 的跨站请求伪造防护 导读 本文聚焦 Fresh
后端前端Drogon框架安全防护指南:HTTPS配置与CSRF防护全攻略
Drogon框架安全防护指南:HTTPS配置与CSRF防护全攻略 你是否还在为Web应用的安全防护而烦恼?Drogon作为基于C++17/20的高性能Http应
后端Web框架在 Go Web 框架中集成 templ:Chi、Echo、Gin、Fiber 实战与 Go 1.25 内置 CSRF 防护指南
在 Go Web 框架中集成 templ:Chi、Echo、Gin、Fiber 实战与 Go 1.25 内置 CSRF 防护指南 templ 是一个专为在 Go
开发工具代码生成后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考