package middleware import ( "net/url" "strings" "github.com/gin-gonic/gin" ) // Cors 跨域中间件(L6 收紧): // - 公开接口:维持 allow_origins=*(Bearer Token 认证,无 Cookie CSRF 面); // - 管理端(/admin/*):当请求携带 Origin 且既不同源也不在允许域名列表时, // 不回 CORS 头(浏览器将拦截跨域读取)。防止管理端 token 泄露后 // 被任意第三方页面直接跨域调用。无 Origin 的非浏览器请求不受影响。 // // extraAllowedOrigins:管理端额外允许的来源(如 site_domain 配置的对外域名)。 func Cors(extraAllowedOrigins ...string) gin.HandlerFunc { allowedHosts := map[string]bool{} for _, o := range extraAllowedOrigins { if o == "" { continue } raw := strings.TrimSpace(o) if !strings.Contains(raw, "://") { raw = "https://" + raw } if u, err := url.Parse(raw); err == nil && u.Host != "" { allowedHosts[u.Host] = true } } // adminCrossOriginBlocked 判断 /admin 请求是否应拒绝跨域: // 仅在「带 Origin 且 Origin 既不同源也不在白名单」时为 true。 adminBlocked := func(c *gin.Context) bool { p := c.Request.URL.Path if p != "/admin" && !strings.HasPrefix(p, "/admin/") { return false } origin := c.GetHeader("Origin") if origin == "" { return false } o, err := url.Parse(origin) if err != nil || o.Host == "" { return true // Origin 非法:按跨域拒绝处理 } if o.Host == c.Request.Host || allowedHosts[o.Host] { return false } return true } return func(c *gin.Context) { if adminBlocked(c) { // 不回 ACAO;预检直接 204(浏览器会因无 CORS 头拦截后续请求) if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() return } c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD") c.Header("Access-Control-Allow-Headers", "Authorization, Content-Type, Content-Disposition, X-Requested-With") c.Header("Access-Control-Expose-Headers", "Content-Disposition, Content-Length") c.Header("Access-Control-Max-Age", "86400") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() } }