CSRF
保护浏览器表单和 Cookie 登录场景
CSRF 中间件用于防止跨站请求伪造。它主要保护浏览器页面、表单提交、Cookie 登录态接口这类场景。
如果接口只给非浏览器客户端使用,并且认证完全走 Authorization: Bearer ... 这类 Header,CSRF 通常不是主要风险。只要请求会自动带 Cookie,就应该考虑启用 CSRF。
安装
go get github.com/duxweb/runa/middleware
基本用法
package main
import (
"context"
"github.com/duxweb/runa"
"github.com/duxweb/runa/middleware/csrf"
"github.com/duxweb/runa/route"
)
func main() {
app := runa.New()
app.Install(route.Provider(route.Addr(":8080")))
web := route.Default().Group("")
web.Use(csrf.New())
web.Get("/form", form)
web.Post("/submit", submit)
if err := app.Run(context.Background()); err != nil {
panic(err)
}
}
csrf.New() 返回 route.Middleware。它只属于 HTTP 中间件,不是能力包,也不需要 app.Install。
工作方式
默认使用双提交 Cookie 模式:
GET、HEAD、OPTIONS等安全方法会签发 CSRF token。- token 会写入 Cookie,默认 Cookie 名是
runa_csrf_token。 POST、PUT、PATCH、DELETE等非安全方法必须提交同一个 token。- token 可以通过
X-CSRF-TokenHeader 或_csrf表单字段提交。 - 缺少 token 或 token 不一致时返回
403。
这种方式不依赖 session,适合先作为默认防护起步。
在模板中输出 token
在处理表单页面时,用 csrf.Token(ctx) 读取当前请求的 token。
func form(ctx *route.Context) error {
return ctx.Render("form.html", map[string]any{
"csrf": csrf.Token(ctx),
})
}
模板里放隐藏字段:
<form method="post" action="/submit">
<input type="hidden" name="_csrf" value="{{ .csrf }}">
<button type="submit">提交</button>
</form>
如果你使用 Ajax,可以把 token 放到请求头:
await fetch('/submit', {
method: 'POST',
headers: {
'X-CSRF-Token': token,
},
})
保护哪些请求
默认只校验非安全方法:
| 方法 | 行为 |
|---|---|
GET |
放行并签发 token。 |
HEAD |
放行并签发 token。 |
OPTIONS |
放行并签发 token。 |
POST |
校验 token。 |
PUT |
校验 token。 |
PATCH |
校验 token。 |
DELETE |
校验 token。 |
所以普通页面访问不会被拦截,真正修改数据的请求会被保护。
常用配置
web.Use(csrf.New(
csrf.Cookie("runa_csrf_token"),
csrf.Header("X-CSRF-Token"),
csrf.Field("_csrf"),
csrf.Path("/"),
csrf.SameSite(http.SameSiteLaxMode),
csrf.Secure(true),
csrf.HTTPOnly(false),
csrf.MaxAge(3600),
))
常用选项:
| 选项 | 说明 |
|---|---|
csrf.Cookie(name) |
设置 Cookie 名。 |
csrf.Header(name) |
设置 Header token 名。 |
csrf.Field(name) |
设置表单字段名。 |
csrf.Path(path) |
设置 Cookie Path。 |
csrf.Domain(domain) |
设置 Cookie Domain。 |
csrf.Secure(true) |
只在 HTTPS 下发送 Cookie。 |
csrf.HTTPOnly(true) |
禁止前端 JS 读取 Cookie。 |
csrf.SameSite(mode) |
设置 SameSite。 |
csrf.MaxAge(seconds) |
设置 Cookie 生命周期。 |
如果使用 __Secure- 或 __Host- Cookie 前缀,中间件会自动补齐浏览器要求的 Cookie 属性。__Host- 会强制 Secure、Path=/,并清空 Domain。
跳过特定请求
Webhook、第三方回调、开放 API 不一定适合 CSRF 校验,可以跳过。
web.Use(csrf.New(
csrf.SkipPaths("/webhook/stripe", "/webhook/github"),
))
也可以用 Next 写自定义逻辑:
web.Use(csrf.New(
csrf.Next(func(ctx *route.Context) bool {
return strings.HasPrefix(ctx.Request().URL.Path, "/api/")
}),
))
Next 返回 true 表示跳过 CSRF 中间件。
自定义错误响应
默认错误会进入 route 的统一错误渲染流程,状态码是 403。
web.Use(csrf.New(
csrf.OnError(func(ctx *route.Context) error {
return ctx.Status(403).JSON(map[string]any{
"code": "csrf_token_mismatch",
"message": "页面已过期,请刷新后重试",
})
}),
))
和 session / auth 的关系
CSRF 不负责登录,也不保存用户状态。它只负责确认“这个修改请求确实来自当前页面”。
推荐顺序:
web.Use(
sessionmw.New(...),
authmw.New(...),
csrf.New(),
)
实际顺序可以按业务调整。重点是:处理 Cookie 登录态的页面或接口,应该挂 CSRF。
常见错误
只在 POST 接口挂 CSRF
如果 GET 表单页没有经过 CSRF 中间件,页面拿不到 token。推荐把 CSRF 挂在同一个 web 分组上,让表单页和提交接口都经过它。
前端发 Ajax 但没有带 Header
非表单请求不会自动带 _csrf 字段。前端需要把 token 放进 X-CSRF-Token。
把 CSRF 用在纯 Token API 上
纯移动端、CLI、服务间调用通常不用 Cookie 自动认证,CSRF 不是主要风险。不要为了“安全感”给所有 API 盲目加 CSRF,否则只会增加调用复杂度。