gin-contrib/cors不支持.example.com是因为其AllowOrigins采用严格字符串匹配而非正则或通配符引擎,浏览器发送的Origin(如https://admin.example.com)不会自动展开,导致配置无效;手写中间件需提取Origin、验证HTTPS、截取主机名并用strings.HasSuffix(host, ".example.com")判断子域名,同时必须动态设置Access-Control-Allow-Origin为具体origin、添加Vary: Origin头,并对OPTIONS请求调用c.AbortWithStatus(204)终止流程。

为什么 gin-contrib/cors 不支持 *.example.com 这种通配符?
因为 gin-contrib/cors 的 AllowOrigins 是严格字符串匹配,不是正则或通配符引擎。你写 []string{"https://*.example.com"},它会原样比对请求头里的 Origin 值,而浏览器发来的 Origin 是完整域名(如 https://admin.example.com),不会自动展开 *。所以该配置完全无效,等价于没加白名单。
怎么手写中间件实现 https://*.example.com 子域名匹配?
核心逻辑是:从 c.Request.Header.Get("Origin") 取值 → 验证是否为 HTTPS 协议 → 提取主机名 → 判断是否以 .example.com 结尾(注意开头的点,避免匹配到 myexample.com)。示例代码片段:
func corsWithSubdomain() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
if origin == "" {
c.Next()
return
}
// 只处理 https 开头的 Origin(生产环境应强制)
if !strings.HasPrefix(origin, "https://") {
c.Next()
return
}
host := strings.TrimPrefix(origin, "https://")
host = strings.Split(host, "/")[0] // 去掉路径和端口
// 允许 *.example.com,但禁止 example.com 本身(可选)
if strings.HasSuffix(host, ".example.com") && host != "example.com" {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Vary", "Origin")
} else {
c.Next()
return
}
c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
c.Header("Access-Control-Expose-Headers", "Content-Length")
c.Header("Access-Control-Allow-Credentials", "true")
// OPTIONS 预检直接返回 204,不往下走
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
-
strings.HasSuffix(host, ".example.com")是关键,确保子域名合法,且防误匹配 - 必须在
c.Next()前设置响应头,否则预检失败;但又不能提前c.Abort(),否则后续 handler 不执行 -
Vary: Origin必须加上,否则 CDN 或代理可能缓存错误的 CORS 响应 - 若需支持 HTTP(开发环境),可额外加
http://判断,但线上务必禁用
使用 AllowCredentials: true 时,Access-Control-Allow-Origin 为什么不能是 *?
这是浏览器硬性限制:只要响应头里有 Access-Control-Allow-Credentials: true,Access-Control-Allow-Origin 就**必须是具体域名**,不能是 *。否则浏览器直接丢弃响应,控制台报错 The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'。
所以你在中间件里动态写 c.Header("Access-Control-Allow-Origin", origin) 是唯一安全做法——既满足凭证要求,又避免硬编码白名单。
注册顺序和 c.Next() 位置为什么致命?
常见崩溃点:把跨域中间件放在 r.POST("/api/login", handler) 之后,或在中间件里调用 c.JSON() / c.String() 后没 c.Abort(),导致 OPTIONS 请求被后续 handler 处理,返回 404 或 500,预检失败。
- 中间件必须用
r.Use(corsWithSubdomain()),且放在所有r.GET/POST调用之前 - 对 OPTIONS 方法,必须用
c.AbortWithStatus(204)终止流程,不能只c.Status(204)然后继续c.Next() - 不要在中间件里调用
c.Render()、c.Redirect()等终止响应的方法,除非你明确要拦截请求
子域名通配本质是字符串校验逻辑,没有魔法——写错一个 . 或漏掉 Vary 头,前端就卡在预检阶段,别指望浏览器给你提示细节。


















