You need to enable JavaScript to run this app. 跨端口独立部署React SPA如何对接Go端gorilla/csrf解决CSRF令牌取值为undefined的问题
优惠活动
大模型
产品
解决方案
定价
更多

跨端口独立部署React SPA如何对接Go端gorilla/csrf解决CSRF令牌取值为undefined的问题

问题根因
  • 你当前前端从DOM取gorilla.csrf.Token的逻辑,是适配Go后端直接渲染HTML模板的场景,React独立跨域部署时页面根本不存在对应的隐藏DOM元素,取值自然为undefined
  • 你虽然在后端响应中设置了X-CSRF-Token自定义头,但跨域场景下未配置CORS暴露规则,前端JS没有权限读取这个响应头的值
  • gorilla/csrf中间件使用默认配置时,跨域适配的Cookie参数缺失,后续带令牌的请求会因为Cookie被浏览器拦截导致校验失败
  • 跨域预检OPTIONS请求未被正确放行,跨域请求流程从源头就无法打通
后端配置调整

1. 调整中间件加载顺序

CORS中间件必须放在路由链最外层,优先级高于CSRF中间件,否则预检请求会被CSRF逻辑拦截
参考调整后的路由配置:

func Route(app *App) *mux.Router {
    r := mux.NewRouter()

    // 1. 自定义CORS中间件,放在最外层
    corsMiddleware := func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // 配置允许的前端跨域源
            w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
            // 允许跨域请求携带Cookie
            w.Header().Set("Access-Control-Allow-Credentials", "true")
            // 允许前端传递的请求头
            w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-CSRF-Token")
            // 允许的HTTP方法
            w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
            // 暴露X-CSRF-Token头给前端JS读取
            w.Header().Set("Access-Control-Expose-Headers", "X-CSRF-Token")

            // 直接放行OPTIONS预检请求,不进入后续校验逻辑
            if r.Method == "OPTIONS" {
                w.WriteHeader(http.StatusNoContent)
                return
            }
            next.ServeHTTP(w, r)
        })
    }

    // 2. 调整gorilla/csrf中间件的跨域适配参数
    csrfMiddleware := csrf.Protect(
        []byte("32-byte-long-auth-key"),
        csrf.Secure(false), // 本地HTTP开发用false,生产HTTPS环境改true
        csrf.HttpOnly(true), // 保持CookieHttpOnly属性,降低XSS风险
        csrf.SameSite(http.SameSiteLaxMode), // 本地开发适配,生产跨域场景改SameSiteNoneMode
        csrf.Path("/"), // 让CSRF Cookie对所有路由生效
    )

    // 3. 全局写入CSRF响应头的中间件
    csrfHeaderMiddleware := func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("X-CSRF-Token", csrf.Token(r))
            next.ServeHTTP(w, r)
        })
    }

    api := r.PathPrefix("/api").Subrouter()
    // 按顺序应用中间件:CORS -> CSRF防护 -> 写入CSRF响应头
    api.Use(corsMiddleware, csrfMiddleware, csrfHeaderMiddleware)

    // 原有业务路由保持不变
    // user
    api.HandleFunc("/users/{id:[0-9]+}", app.tokenVerifyMiddleWare(app.userGetHandler)).Methods("GET", "OPTIONS")
    api.HandleFunc("/users", app.signupHandler).Methods("POST", "OPTIONS")
    api.HandleFunc("/users/{id:[0-9]+}", app.userDeleteHandler).Methods("DELETE", "OPTIONS")
    api.HandleFunc("/users/{id:[0-9]+}", app.userUpdateHandler).Methods("PUT", "OPTIONS")
    // like
    api.HandleFunc("/users/{id:[0-9]+}/likes", app.tokenVerifyMiddleWare(app.likeGetALLHandler)).Methods("GET", "OPTIONS")
    api.HandleFunc("/users/{id:[0-9]+}/likes", app.tokenVerifyMiddleWare(app.likePostHandler)).Methods("POST", "OPTIONS")
    api.HandleFunc("/users/{id:[0-9]+}/likes/{id:[0-9]+}", app.tokenVerifyMiddleWare(app.likeDeleteHandler)).Methods("DELETE", "OPTIONS")
    // auth
    api.HandleFunc("/login", app.loginHandler).Methods("POST", "OPTIONS")
    api.HandleFunc("/logout", app.tokenVerifyMiddleWare(app.logoutHandler)).Methods("POST", "OPTIONS")
    api.HandleFunc("/refresh_token", app.refreshTokenHandler).Methods("POST", "OPTIONS")

    return r
}
前端逻辑调整
  • 删除原有从DOM取gorilla.csrf.Token的无效代码
  • 令牌仅存在内存中,不要存在localStorage/sessionStorage,避免XSS风险
  • 必须开启Axios的withCredentials配置,否则跨域请求无法自动携带CSRF Cookie,gorilla/csrf校验必然失败
  • 页面初始化时先调用一次GET类公开接口(GET请求不会触发CSRF校验),从响应头读取CSRF令牌
  • 所有需要CSRF校验的请求(POST/PUT/DELETE等),自动在请求头携带读取到的令牌

参考调整后的前端代码:

// 内存存储CSRF令牌
let csrfToken = null

// 初始化Axios实例
const instance = axios.create({
  baseURL: "http://localhost:8080/api/",
  timeout: 1000,
  withCredentials: true, // 必须开启跨域凭证携带
})

// 初始化CSRF令牌:页面加载时调用
async function initCsrfToken() {
  if (csrfToken) return
  // 调用任意公开GET接口,gorilla/csrf默认不对GET请求做校验
  const resp = await instance.get("/health")
  // 从响应头读取令牌
  csrfToken = resp.headers["x-csrf-token"]
}

// 请求拦截器:自动给需要CSRF校验的请求加头
instance.interceptors.request.use(config => {
  const needCsrf = ["POST", "PUT", "DELETE", "PATCH"].includes(config.method.toUpperCase())
  if (needCsrf && csrfToken) {
    config.headers["X-CSRF-Token"] = csrfToken
  }
  return config
})

// 页面加载完成后先初始化CSRF令牌,再执行业务逻辑
initCsrfToken().then(() => {
  // 后续正常调用接口即可
})
注意事项
  • 生产环境部署时,必须将csrf.Secure设为true,同时把SameSite配置为SameSiteNoneMode,否则跨域Cookie会被浏览器拦截
  • 首次加载页面时必须先完成CSRF令牌初始化,再发送POST类请求,否则会因为没有令牌和Cookie被403拦截
  • 不要修改CSRF Cookie的HttpOnly属性,全程保持前端不直接读取Cookie值,浏览器自动携带即可,降低XSS攻击风险

内容的提问来源于stack exchange,提问作者マットmatto

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.31 20:15:03