浏览器不支持子域名通配符,必须显式列出源或手动解析origin头校验;启用凭据时需严格匹配并回写完整origin,且必须设置vary: origin。

子域名通配符在 Gin CORS 中不被浏览器原生支持
浏览器标准明确拒绝 Access-Control-Allow-Origin: "https://*.example.com" 这类写法——它会被当作非法值忽略,等效于没设头。哪怕你在 gin-contrib/cors 里硬写进去,中间件可能接受,但最终响应发到浏览器时,CORS 检查直接失败,前端请求卡在 pending,控制台无报错。
真正起作用的只有两种方式:显式列出所有合法源(如 []string{"https://app.example.com", "https://admin.example.com"}),或手动解析 Origin 头做运行时匹配。
用 strings.HasSuffix 实现子域名白名单校验
开发中常见需求是允许 app.example.com、api.example.com、dev.example.com 等同根域名下的所有子域,这时得自己写逻辑校验,不能依赖中间件的静态配置。
关键点:
-
Origin头必须完整(含协议+域名+端口),比如https://app.example.com:8080,不能只截取 host - 匹配前要先验证协议是否为
http或https,防止伪造 - 避免误匹配
notexample.com→ 必须用strings.HasSuffix(origin, ".example.com"),而非strings.Contains - 主域名本身(如
example.com)也要单独加进白名单,否则不匹配
示例片段:
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
if origin != "" && (strings.HasPrefix(origin, "http://") || strings.HasPrefix(origin, "https://")) {
host := strings.TrimPrefix(strings.TrimPrefix(origin, "http://"), "https://")
host = strings.Split(host, ":")[0] // 去掉端口
if host == "example.com" || strings.HasSuffix(host, ".example.com") {
c.Header("Access-Control-Allow-Origin", 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", "Content-Type,Authorization")
c.Header("Vary", "Origin")
}
}
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
AllowCredentials = true 时,子域名匹配必须严格
一旦开启凭据(cookie/token),Access-Control-Allow-Origin 就不能是 "*",也不能是模糊值;必须精确回写当前请求的 Origin 字符串。否则浏览器静默丢弃响应。
这意味着:
- 不能把
https://app.example.com和https://admin.example.com合并成一个通配规则返回 - 如果前端从
http://localhost:3000发请求,你得单独加一条localhost白名单 - HTTPS 页面禁止请求 HTTP 源,所以
http://sub.example.com对https://app.example.com无效 -
Vary: Origin必须加上,否则 CDN 或代理可能缓存错误的 CORS 响应
gin-contrib/cors 不支持动态子域,别硬套配置
gin-contrib/cors 的 cors.Config{AllowedOrigins: []string{...}} 是启动时加载的静态列表,不支持运行时计算。你往里面塞 "*.example.com",它不会自动展开,也不会帮你做字符串匹配——它只会原样写入响应头,导致浏览器拒绝。
如果你已经用了这个中间件,又需要子域支持,只能:
- 放弃
AllowedOrigins,改用AllowAllOrigins: false+ValidateOrigin回调函数 - 在回调里做
strings.HasSuffix判断,并返回true或false - 确保
AllowCredentials: true与回调逻辑同时启用,否则凭据失效
漏掉 ValidateOrigin 或把它写成恒返回 true,就等于没做校验,生产环境有风险。
子域跨域真正的难点不在写法,而在 Origin 校验的边界条件:协议、端口、斜杠、大小写、甚至 URL 编码。每多一个环境(本地、测试、预发、生产),就得同步维护对应白名单或校验逻辑。别指望一次配置打天下。











