package middleware import ( "log/slog" "net/http" "strconv" "strings" "github.com/edu-cloud/api-gateway/internal/config" "github.com/gin-gonic/gin" ) // corsMaxAge 预检缓存时长:12 小时 const corsMaxAge = 12 * 60 * 60 // devCORSOrigins 是未配置 CORS_ORIGINS 时的开发环境默认白名单 const devCORSOrigins = "http://localhost:3000,http://localhost:3001" // CORS 返回跨域资源共享中间件(从 Config 读取白名单,W3 裁决:slog 结构化日志)。 // 允许来源从 cfg.CORSOrigins 读取(逗号分隔); // 未配置时使用开发环境白名单(localhost:3000/3001)并打印 warning。 // 允许方法:GET POST PUT DELETE OPTIONS PATCH // 允许头:Authorization Content-Type X-Request-Id X-Trace-Id // 暴露头:X-Request-Id X-Trace-Id func CORS(cfg *config.Config) gin.HandlerFunc { allowed := parseCORSOrigins(cfg.CORSOrigins) if len(allowed) == 0 { slog.Warn("CORS_ORIGINS not set, using dev default whitelist") allowed = parseCORSOrigins(devCORSOrigins) } return func(c *gin.Context) { origin := c.GetHeader("Origin") allowOrigin := "" if allowed[origin] { allowOrigin = origin } if allowOrigin != "" { h := c.Writer.Header() h.Set("Access-Control-Allow-Origin", allowOrigin) h.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH") h.Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Request-Id, X-Trace-Id") h.Set("Access-Control-Expose-Headers", "X-Request-Id, X-Trace-Id") h.Set("Access-Control-Max-Age", strconv.Itoa(corsMaxAge)) // 非通配来源需标注 Vary,便于缓存正确区分 h.Add("Vary", "Origin") } // 预检请求直接返回 204 if c.Request.Method == http.MethodOptions { c.AbortWithStatus(http.StatusNoContent) return } c.Next() } } // parseCORSOrigins 解析逗号分隔的来源列表为集合,空字符串返回空 map func parseCORSOrigins(raw string) map[string]bool { allowed := map[string]bool{} if raw == "" { return allowed } for _, o := range strings.Split(raw, ",") { o = strings.TrimSpace(o) if o != "" { allowed[o] = true } } return allowed }