依据 coord-final-decisions §3.8 W1-W8 裁决与 president-final-rulings §2.15/§2.16/§2.19 完整实现网关硬化: - W1/W2: 错误码 GW_ 前缀 + ActionState 信封响应体 - W3: 全量替换为 log/slog 结构化日志 - W4: /readyz 并行 ping 9 下游 + 软失败规则 - W5: 7 个业务 Prometheus 指标 + /metrics 端点 - W6: tracer 资源属性补全(name/version/env/host) - W7: DevMode=true && ENV=production panic 防护 - W8: 保持共享 downstream 熔断 P2 RS256 升级:接入 shared-go/jwks.Fetcher(TTL 5min)。 P2.7+P3-P5 路由扩展:student/parent/messages/dashboard。 文档同步:README/01/02/known-issues,arch.db 已更新。 质量校验:go vet + build + test 均通过。
73 lines
2.1 KiB
Go
73 lines
2.1 KiB
Go
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
|
||
}
|