功能说明
跨站请求伪造利用浏览器自动携带 Cookie 的行为,让用户在已登录状态下执行非预期写操作。双提交模式在 Cookie 中放一个随机令牌,同时要求前端从页面读取该值并放到自定义请求头。攻击者的站点可以触发请求并带上 Cookie,但受同源策略限制无法读取 Cookie 值,也就无法伪造请求头。服务端比较两者是否一致即可拒绝伪造请求。
中间件在 GET、HEAD、OPTIONS 等安全方法上检查 Cookie,缺失时生成三十二字节随机数并写入。写方法必须同时具备 Cookie 与匹配的请求头,否则返回四百零三。前端需要用 JavaScript 读取 Cookie 并设置请求头,因此该 Cookie 不能设为 HttpOnly。
关键代码
newToken 用 crypto/rand 生成三十二字节,再以无填充的 URL 安全 Base64 编码,避免加号和斜杠带来的转义问题。CSRFMiddleware 先取 Cookie,对安全方法仅在缺失时补发,并设置 Path、SameSite 和按是否 TLS 决定 Secure。写方法先拒绝缺失 Cookie 的请求,再用 subtle.ConstantTimeCompare 比较请求头与 Cookie 值,长度不同也返回零,避免逐字节提前退出泄露信息。main 把中间件包在业务处理器外。
使用注意
双提交只适用于同源前端;如果前端与接口跨站,需要正确配置 CORS 与凭据,且 Cookie 的 SameSite 策略要匹配。应同时校验 Origin 或 Referer,作为纵深防御。令牌必须每次会话随机生成,不能使用可预测值。不要在 URL 查询参数中传递令牌,它会进入日志和浏览器历史。登出时应清除 Cookie。若站点存在 XSS,攻击者可以读取令牌,CSRF 防护不能替代输出编码和内容安全策略。
验证方式
启动服务后先请求 GET /submit,响应会带上 csrf_token Cookie。再发 POST 请求并复制该 Cookie 值到 X-CSRF-Token 请求头,应返回 accepted。省略请求头或改成错误值应返回四百零三和 invalid csrf token,说明写操作被拒绝。
代码示例
package main
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt"
"net/http"
)
const (
csrfCookie = "csrf_token"
csrfHeader = "X-CSRF-Token"
)
func newToken() (string, error) {
buffer := make([]byte, 32)
if _, err := rand.Read(buffer); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(buffer), nil
}
func CSRFMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie, cookieErr := r.Cookie(csrfCookie)
switch r.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions:
if cookieErr != nil {
token, err := newToken()
if err != nil {
http.Error(w, "cannot issue csrf token", http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: csrfCookie,
Value: token,
Path: "/",
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
Secure: r.TLS != nil,
})
}
next.ServeHTTP(w, r)
return
}
if cookieErr != nil {
http.Error(w, "missing csrf cookie", http.StatusForbidden)
return
}
header := r.Header.Get(csrfHeader)
if subtle.ConstantTimeCompare([]byte(header), []byte(cookie.Value)) != 1 {
http.Error(w, "invalid csrf token", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
handler := CSRFMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "accepted")
}))
http.Handle("/submit", handler)
_ = http.ListenAndServe(":8080", nil)
}