★ CORS 原理与 Gin 实现(预检/凭据)

结论先行

跨域是浏览器同源策略的限制,服务端靠响应头放行(CORS)。分两类请求:简单请求直接发真实请求,服务端回 Access-Control-Allow-Origin 即可;非简单请求(自定义头、application/json、PUT/DELETE 等)浏览器先发 OPTIONS 预检,服务端要回 Allow-Methods/Allow-Headers 并通常以 204 结束。带凭据(cookie/Authorization)时,Allow-Origin 不能是 *,必须回显具体 Origin 且加 Allow-Credentials: true

要点表

场景服务端要求
简单请求Access-Control-Allow-Origin: <白名单里的 Origin>
预检 OPTIONS回 Access-Control-Allow-Methods / Allow-Headers,短路返回(不继续走业务)
带凭据Allow-Credentials: true,且 Allow-Origin 为具体源而非 *
预检缓存Access-Control-Max-Age 让浏览器少发预检

Gin 实现注意

  • CORS 中间件必须注册在鉴权之前:否则预检 OPTIONS 会先被 401 拦截。
  • 可引入 gin-contrib/cors(社区常用,非官方核心包),或手写:白名单判断 → 回显 Origin → OPTIONS 短路。
  • 手写时校验 Origin 来源,别盲目回显任意 Origin(等于放开跨域)。

代码示例

var allowedOrigins = map[string]bool{
    "https://app.example.com": true, // 示例站,替换为真实白名单
}

func CORS() gin.HandlerFunc {
    return func(c *gin.Context) {
        origin := c.GetHeader("Origin")
        if allowedOrigins[origin] {
            c.Header("Access-Control-Allow-Origin", origin)
            c.Header("Vary", "Origin")
            c.Header("Access-Control-Allow-Credentials", "true")
            c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
            c.Header("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Request-Id")
            c.Header("Access-Control-Max-Age", "86400")
        }
        if c.Request.Method == http.MethodOptions {
            c.AbortWithStatus(http.StatusNoContent) // 204 短路预检
            return
        }
        c.Next()
    }
}

func main() {
    r := gin.New()
    r.Use(CORS())      // 必须放在鉴权中间件之前
    r.Use(Auth())
    // 业务路由...
}

常见追问 / 记忆点

  • 追问:为什么 Allow-Credentials 时不能配 *?答:规范禁止——带凭据时通配 Origin 会泄漏用户上下文,浏览器必须看到精确源才放行。
  • 追问:预检为什么是 OPTIONS?答:约定俗成的“探测”方法,不携带业务语义,服务端应直接应答头并短路。
  • 追问:前端还要做什么?答:fetch 需带 credentials: 'include'(同源默认不带跨域 cookie)。
  • 记忆点:Origin 白名单回显 + OPTIONS 短路 + 凭据不开 *;中间件顺序要在鉴权前。
笔记加载中…