news 2026/10/14 7:51:48

CJoy 框架 CSRF 防护实战:CsrfProtect 中间件配置、Token 生成与完整接入指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CJoy 框架 CSRF 防护实战:CsrfProtect 中间件配置、Token 生成与完整接入指南
  • 后端
  • Web框架
  • MCP 服务

【免费下载链接】cjoy

一个高性能、可扩展、轻量、省心的仓颉应用开发框架。IoC,Rest,宏路由,Json,中间件,参数绑定与校验,文件上传下载,OAuth2,MCP......

项目地址:https://gitcode.com/Cangjie-SIG/cjoy
点击查看免费下载

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的完整校验流程分为生成与验证两个阶段:

  1. 服务器生成随机 Secret:在中间件首次处理请求时,将构造函数传入的secret写入当前 Session(对应源码中的csrfSecretCtxKey = "__csrf_secret__");
  2. 生成 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);
  3. 客户端携带 Token:客户端在表单隐藏域、请求头或查询参数中携带此 Token;
  4. 服务器校验:对需要保护的请求,从指定位置提取 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:

参数类型默认值说明
secretString-用于生成 Token 的密钥
ignoreMethodsArray<String>["GET", "HEAD", "OPTIONS"]不需要 CSRF 保护的 HTTP 方法
lookupKeyString"header:X-CSRF-TOKEN"从请求中提取 Token 的位置
filterHandlerJoyRequestFilterFuncNone用于跳过 CSRF 验证的过滤函数
errorHandlerJoyRequestHandlerFunc返回 401CSRF 验证失败时的处理函数

其中默认的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 等)中携带。


八、注意事项与安全实践

  1. Session 依赖:CsrfProtect必须配合SessionManager使用,且需先注册 Session 再注册 CSRF 中间件,否则getSession(ctx).getOrThrow()会因取不到 Session 而抛出异常;
  2. Secret 密钥:使用强随机密钥,不要在代码中硬编码(建议通过配置中心或环境变量注入);secret是 Token 计算的关键输入,泄露后攻击者可自行推导合法 Token;
  3. Token 生命周期:Token 与 Session 绑定,Session 失效后 Token 也会失效;Token 在同一 Session 内首次生成后即被缓存复用,无需每次请求重新生成;
  4. GET 请求:默认 GET、HEAD、OPTIONS 请求不需要 CSRF 验证,因为 CSRF 攻击通常依赖表单 POST 或 AJAX 提交等状态改变方法;
  5. 前端框架:使用前端框架时,需要将 Token 注入到每个状态改变请求中(请求头、表单字段或查询参数均可,注意与lookupKey保持一致);
  6. HTTPS:生产环境必须使用 HTTPS,防止 Token 在传输中被中间人窃取;
  7. SameSite Cookie:配合HttpOnly和SecureCookie 使用增强安全性——JoySessionConfig默认已开启cookieHttpOnly,生产环境应将cookieSecure设为true;设置SameSite属性可进一步阻止跨站携带 Cookie;
  8. 日志记录:验证失败会通过@JoyLogError记录错误日志(含客户端 IP、请求方法与 URL),可用于监控和识别攻击行为;
  9. 公开接口豁免:对无需防护的公开 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......

项目地址:https://gitcode.com/Cangjie-SIG/cjoy
点击查看免费下载
上一篇:Langfuse 摄取事件重放(S3 Replay)运维指南:基于 Athena 访问日志与 BullMQ 队列的失败补偿
下一篇:FastGPT 项目代码执行沙盒(Code Sandbox)架构与安全隔离实践

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 7:51:44

云灾备规划与落地:从RPO/RTO到跨地域容灾实战指南

2015年我在一家传统制造业公司负责IT基础架构&#xff0c;那时候的灾备就是两台服务器加一台磁带机&#xff0c;每天凌晨两点跑一次全量备份&#xff0c;磁带定期送银行保管箱。后来公司业务上云&#xff0c;我原以为云平台自带的高可用能让我省心不少&#xff0c;直到有一次生…

作者头像 李华
网站建设 2026/10/14 7:51:35

华容道求解算法:状态压缩与BFS/A*搜索实现指南

简介&#xff1a;华容道算法的Java与Android实现源码包&#xff0c;面向算法爱好者、移动开发初学者及高校学生&#xff0c;以经典滑块谜题为载体&#xff0c;演示深度优先搜索&#xff08;DFS&#xff09;与A*搜索两种求解思路的工程落地。压缩包大小约14.4MB&#xff0c;内含…

作者头像 李华
网站建设 2026/10/14 7:51:20

光场调控三要素:波前、相位与振幅解析

1. 光场三要素的物理意义解析当一束光穿过实验室的空气&#xff0c;打在光学平台的探测器上时&#xff0c;我们看到的远不止是一个亮斑。这个看似简单的光斑背后&#xff0c;隐藏着三个决定光场行为的核心物理量&#xff1a;波前(wavefront)、相位(phase)和振幅(amplitude)。就…

作者头像 李华
网站建设 2026/10/14 7:51:07

AnyPS5:跨平台轻量级图像处理工具的设计与实现

1. 从“AnyPS5”这个标题说起&#xff1a;一个跨平台图像处理工具的设计与实现第一次看到“AnyPS5”这个标题&#xff0c;很多人会下意识联想到游戏主机&#xff0c;但在图像处理圈子里&#xff0c;这个名字其实指向的是另一件事——一个能在任意平台上运行、对标经典图像处理软…

作者头像 李华
网站建设 2026/10/14 7:50:44

Linux内核IRQ子系统核心机制:irq_desc、irq_chip与irq_domain实战

1. 三层中断编号的换算&#xff1a;先搞懂hwirq、virq和irq number做嵌入式Linux这些年&#xff0c;几乎每个外设驱动开发者都会在中断这堵墙上撞几次。前阵子接手一个“外设中断偶尔不来”的调试任务&#xff0c;板子看起来什么都有&#xff0c;中断就是丢。那天从下午查到深夜…

作者头像 李华
网站建设 2026/10/14 7:50:29

Spring Boot集成MQTT手动封装Paho:动态订阅与断线重连实战

做 Spring Boot 集成 MQTT 这件事&#xff0c;我前后折腾过好几轮。最开始图省事&#xff0c;直接用 spring-integration-mqtt 的 starter&#xff0c;配置也简单&#xff0c;几行就通了。但真上了生产环境&#xff0c;面对动态订阅、连接保活、断线重连、消息补偿这些场景&…

作者头像 李华