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 许可证发布