★ 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 短路 + 凭据不开 *;中间件顺序要在鉴权前。