Files
cmautobuy/admin/handler/web/csrf.go
T

96 lines
2.7 KiB
Go
Raw Normal View History

package web
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"net/http"
"github.com/gin-gonic/gin"
)
// CSRF 防护,用的是「双提交 Cookie」这个最简单的方案:
//
// 1. 给浏览器种一个随机 token 的 Cookie;
// 2. 每个表单里带一个同值的隐藏字段;
// 3. 提交时比对两者,不一致就拒绝。
//
// 原理:攻击者的页面可以诱导浏览器带上 Cookie 发请求,
// 但**读不到** Cookie 的值,所以拼不出正确的隐藏字段。
//
// 没有引第三方库,是因为这个方案本身就几十行,
// 而且多一个依赖就多一个可能要求 Go >= 1.25 的风险。
//
// **只给页面路由用。** 给 Client 的 /api/v1/client/* 绝不能加——
// 它不是浏览器、没有 Cookie,加了会直接把它挡在门外。
const (
csrfCookieName = "cmautobuy_csrf"
csrfFieldName = "csrf_token"
csrfTokenBytes = 32
)
// CSRFMiddleware 返回 Gin 中间件。
//
// GET 等安全方法只负责发 token;POST 等写操作要校验。
func CSRFMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token, err := c.Cookie(csrfCookieName)
if err != nil || token == "" {
token, err = newCSRFToken()
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
// HttpOnly 必须为 false —— 双提交方案要让页面把值填进表单。
http.SetCookie(c.Writer, &http.Cookie{
Name: csrfCookieName, Value: token, Path: "/", HttpOnly: false,
Secure: requestIsHTTPS(c.Request), SameSite: http.SameSiteLaxMode,
MaxAge: 12 * 3600,
})
}
// 交给模板渲染成隐藏字段
c.Set(csrfFieldName, token)
switch c.Request.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions:
c.Next()
return
}
submitted := c.PostForm(csrfFieldName)
if submitted == "" {
submitted = c.GetHeader("X-CSRF-Token")
}
// 用常数时间比较,避免通过响应快慢猜 token
if subtle.ConstantTimeCompare([]byte(submitted), []byte(token)) != 1 {
c.HTML(http.StatusForbidden, "partials/error", gin.H{
"Title": "请求被拒绝",
"Message": "表单校验失败(CSRF token 不匹配)。" +
"通常是页面开太久过期了,返回上一页刷新后重试即可。",
})
c.Abort()
return
}
c.Next()
}
}
// newCSRFToken 生成一个随机 token。
func newCSRFToken() (string, error) {
buf := make([]byte, csrfTokenBytes)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(buf), nil
}
// csrfToken 从上下文取出当前 token,供页面渲染隐藏字段。
func csrfToken(c *gin.Context) string {
if v, ok := c.Get(csrfFieldName); ok {
if s, ok := v.(string); ok {
return s
}
}
return ""
}