Skip to content

CSRF 防护

CSRF(跨站请求伪造)攻击利用浏览器自动携带 Cookie 的特性,诱导已登录用户在不知情的情况下向目标站点发送恶意请求。ACE Framework 提供 csrfMiddleware 开箱即用,基于 Double Submit Cookie 模式实现防护。

何时需要 CSRF 防护

CSRF 仅针对基于 Cookie 的会话认证场景(浏览器表单提交)。如果你的 API 接口使用 JWT Bearer Token 鉴权,浏览器不会自动携带 token,天然免疫 CSRF,无需启用本中间件

防护原理

服务端只需验证 Cookie 中的 csrf_token 与请求体 _csrf 字段(或 Header X-CSRF-Token)是否一致。攻击者无法读取受 SameSite 保护的 Cookie,因此无法伪造合法请求。

快速开始

1. 挂载中间件

csrfMiddleware 依赖 Session 存储 token,需先挂载 sessionMiddleware

cangjie
import ace_web.*
import ace_http.*
import ace_security.csrf.*
import ace_security.session.*

let app = App()

// session 必须在 csrf 之前挂载
app.use(sessionMiddleware(SessionConfig(
    secret: "session-secret-key",
    maxAge: 86400
)))
app.use(csrfMiddleware())
app.use(router.routes())

2. 在表单页面注入 token

cangjie
router.get("/form") { ctx =>
    let token = csrfToken(ctx)   // 读取或生成当次 token
    ctx.html("""
        <form method="POST" action="/submit">
            <input type="hidden" name="_csrf" value="${token}">
            <input type="text" name="username">
            <button type="submit">提交</button>
        </form>
    """)
}

3. 提交校验(自动完成)

csrfMiddleware 拦截所有 POST / PUT / PATCH / DELETE 请求,自动从以下位置读取 token 并校验:

优先级来源字段/Header 名
1请求 HeaderX-CSRF-Token
2表单 Body_csrf
3JSON BodycsrfToken

校验失败时返回 403 Forbidden,响应体:

json
{"error": "invalid csrf token"}

配置项

cangjie
app.use(csrfMiddleware(CsrfConfig(
    cookieName:  "csrf_token",   // Cookie 名称,默认 "_csrf"
    headerName:  "X-CSRF-Token", // Header 名称
    fieldName:   "_csrf",        // 表单字段名
    cookiePath:  "/",
    secure:      true,           // 生产环境开启(仅 HTTPS 发送 Cookie)
    sameSite:    "Strict"        // 推荐 Strict 或 Lax
)))

CsrfConfig 参数说明

参数类型默认值说明
cookieNameString"_csrf"存储 token 的 Cookie 名
headerNameString"X-CSRF-Token"前端 Ajax 请求携带的 Header
fieldNameString"_csrf"表单隐藏字段名
secureBoolfalse仅 HTTPS 环境发送 Cookie
sameSiteString"Lax"Cookie SameSite 策略
tokenLengthInt32随机 token 字节长度

豁免路径

对于纯 API 路由(使用 JWT 鉴权),可通过 except 参数跳过 CSRF 校验:

cangjie
app.use(csrfMiddleware(CsrfConfig(
    except: ["/api/", "/webhook/"]
)))

前缀匹配:所有以 /api/ 开头的路径均不进行 CSRF 校验。

与 Ajax 配合

前端 SPA 可在请求拦截器中从 Cookie 读取 token 并附加到 Header:

javascript
// 前端 JavaScript(仅供参考)
const csrfToken = document.cookie
  .split('; ')
  .find(row => row.startsWith('_csrf='))
  ?.split('=')[1]

fetch('/api/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken
  },
  body: JSON.stringify(data)
})

多标签页场景

Double Submit Cookie 方案下,同一浏览器多标签页共享同一 Cookie token,不存在 token 竞争问题。但若同时打开多个表单且 token 刷新频率过高,可能出现旧表单 token 失效的情况——建议将 token 有效期与 Session 有效期保持一致。

与 sessionMiddleware 的关系

csrfMiddleware 将 token 存入当前 Session,以实现跨请求持久化:

cangjie
// 内部等价逻辑(框架自动处理,无需手写)
let token = session.get("csrf_token") ?? generateToken(32)
session.set("csrf_token", token)
ctx.setCookie("_csrf", token)

若未挂载 sessionMiddleware,则降级为仅验证 Cookie 与请求体的一致性(无服务端存储),安全性略低但仍有效。

基于 Apache-2.0 许可证发布