dcfd24b624
v1.0.3 定位为 bug fix release,收口 v1.0.2 引入的破坏性清理并修复 4 个轻量 bug + 依赖复查 + 版本号治理 + 文档对齐。同时包含此前未提交 的 v1.0.2 全部工作。 v1.0.3 变更: Fixed: - generateJTI 忽略 rand.Read 错误(jwt)— 改为 (string, error) 并传播 - QueryBuilder.Page Count 受残留 Limit 截断(repository)— countDB 加 Limit(-1).Offset(-1) - OSS/本地存储文件名冲突(storage)— 新增 uniqueFilename 加 8 字节随机后缀 - 数据库重试策略对不可恢复错误无效(database)— 新增 isTransientDBError Dependencies: - go mod tidy 补全 postgres 方言传递依赖;安全补丁升级 x/crypto v0.49→v0.53、golang-jwt/jwt/v5 v5.2.1→v5.3.1、gorilla/websocket v1.5.1→v1.5.3 Removed: - Breaking: 清理 v1.0.2 兼容别名(InitMySQL* / driverName) - 死代码 database.DBResolver - 代码中 292 行"评分/理由"自夸注释(#26,23 个文件) Changed: - Breaking: 错误码体系重构 CodeSuccess 1→0、CodeFail 0→1,删除 CodeInvalidParams,加编译期防撞码 - database/mysql.go → manager.go;Logger 拆分三独立 core 修复 Tee 重复写入 - 版本号常量化:app.go 新增 const Version 作为唯一来源,CLI/脚手架模板引用之,不再散落字面量 Security: - CORS 中间件按 W3C 规范修复 Allow-Credentials / Vary: Origin / 非白名单不回显 Added: - console 包显式 level 控制(SetLevel/WithLevel/LevelSilent,atomic.Int32 并发安全) - 新增测试 jwt/repository/storage/database/router/middleware/app - 文档:新增 CHANGELOG.md、Version_v1.0.2_report.md;更新 README/GUIDE 顶部更新日志 - 文档对齐:删除对不存在的 config.DefaultManager() getter 的描述(保持 API 与文档一致) 详见 CHANGELOG.md 与 README.md 更新日志。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
167 lines
4.8 KiB
Go
167 lines
4.8 KiB
Go
package middleware
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/EthanCodeCraft/xlgo-core/config"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// CORS 跨域中间件
|
||
// 支持从配置文件读取 CORS 配置;遵循 W3C CORS 规范:
|
||
// 当 AllowCredentials=true 时,Access-Control-Allow-Origin 必须回显为具体 Origin,
|
||
// 不能使用 "*"(浏览器会拒绝携带凭证的响应)。
|
||
func CORS() gin.HandlerFunc {
|
||
return CORSWithConfig(nil)
|
||
}
|
||
|
||
// CORSWithConfig 使用自定义配置的 CORS 中间件
|
||
func CORSWithConfig(corsCfg *config.CORSConfig) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
origin := c.GetHeader("Origin")
|
||
|
||
// 获取配置
|
||
cfg := config.Get()
|
||
var corsConfig *config.CORSConfig
|
||
if corsCfg != nil {
|
||
corsConfig = corsCfg
|
||
} else if cfg != nil {
|
||
corsConfig = &cfg.CORS
|
||
}
|
||
|
||
// 是否允许携带凭证(影响 Origin 回显策略)
|
||
allowCredentials := corsConfig != nil && corsConfig.AllowCredentials
|
||
|
||
// 获取允许的域名列表
|
||
allowedOrigins := getAllowedOrigins(cfg, corsConfig)
|
||
|
||
// 匹配 Origin
|
||
// 注意:当 allowCredentials=true 且匹配到通配符时,
|
||
// 必须回显具体 Origin 而非 "*",否则浏览器会拒绝响应。
|
||
allowedOrigin := ""
|
||
matchedWildcard := false
|
||
if origin != "" {
|
||
for _, ao := range allowedOrigins {
|
||
if ao == "*" {
|
||
matchedWildcard = true
|
||
break
|
||
}
|
||
// 支持通配符匹配(如 *.example.com)
|
||
if strings.HasPrefix(ao, "*.") {
|
||
domain := ao[2:]
|
||
if strings.HasSuffix(origin, domain) {
|
||
allowedOrigin = origin
|
||
break
|
||
}
|
||
}
|
||
if ao == origin {
|
||
allowedOrigin = origin
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理通配符 + 兜底策略
|
||
if allowedOrigin == "" {
|
||
if matchedWildcard {
|
||
if allowCredentials && origin != "" {
|
||
// AllowCredentials=true 时 spec 禁止 "*",回显具体 Origin
|
||
allowedOrigin = origin
|
||
} else {
|
||
allowedOrigin = "*"
|
||
}
|
||
} else if cfg != nil && cfg.IsDevelopment() && origin != "" {
|
||
// 开发环境兜底:回显具体 Origin(兼容 credentials)
|
||
allowedOrigin = origin
|
||
}
|
||
}
|
||
|
||
// 设置 CORS 响应头
|
||
if allowedOrigin != "" {
|
||
c.Header("Access-Control-Allow-Origin", allowedOrigin)
|
||
// Origin 不是 "*" 时,下游缓存(CDN / 网关)必须按 Origin 区分缓存
|
||
if allowedOrigin != "*" {
|
||
c.Header("Vary", "Origin")
|
||
}
|
||
}
|
||
|
||
// 从配置获取允许的方法、请求头等
|
||
methods := corsConfig.GetAllowedMethods()
|
||
headers := corsConfig.GetAllowedHeaders()
|
||
exposedHeaders := corsConfig.GetExposedHeaders()
|
||
maxAge := corsConfig.GetMaxAge()
|
||
|
||
c.Header("Access-Control-Allow-Methods", strings.Join(methods, ", "))
|
||
c.Header("Access-Control-Allow-Headers", strings.Join(headers, ", "))
|
||
c.Header("Access-Control-Expose-Headers", strings.Join(exposedHeaders, ", "))
|
||
c.Header("Access-Control-Max-Age", strconv.Itoa(maxAge))
|
||
|
||
// 仅在显式启用且 Origin 不是 "*" 时才发 Allow-Credentials
|
||
// (CORS 规范:Allow-Origin: * 时禁止携带凭证)
|
||
if allowCredentials && allowedOrigin != "" && allowedOrigin != "*" {
|
||
c.Header("Access-Control-Allow-Credentials", "true")
|
||
}
|
||
|
||
// 处理预检请求
|
||
if c.Request.Method == "OPTIONS" {
|
||
c.AbortWithStatus(http.StatusNoContent)
|
||
return
|
||
}
|
||
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
// getAllowedOrigins 获取允许的域名列表
|
||
// 优先使用配置文件,生产环境必须显式配置,开发环境提供 localhost 兜底。
|
||
func getAllowedOrigins(cfg *config.Config, corsConfig *config.CORSConfig) []string {
|
||
// 优先使用 CORS 配置
|
||
if corsConfig != nil && len(corsConfig.AllowedOrigins) > 0 {
|
||
return corsConfig.AllowedOrigins
|
||
}
|
||
|
||
// 生产环境:必须配置具体的域名
|
||
if cfg != nil && cfg.IsProduction() {
|
||
// 返回空列表,生产环境必须配置
|
||
return []string{}
|
||
}
|
||
|
||
// 开发环境允许 localhost
|
||
return []string{
|
||
"http://localhost:3000",
|
||
"http://localhost:5173",
|
||
"http://localhost:8080",
|
||
"http://localhost:4200",
|
||
"http://127.0.0.1:3000",
|
||
"http://127.0.0.1:5173",
|
||
"http://127.0.0.1:8080",
|
||
"http://127.0.0.1:4200",
|
||
}
|
||
}
|
||
|
||
// CORSWithOrigins 使用指定域名列表的 CORS 中间件
|
||
func CORSWithOrigins(origins []string) gin.HandlerFunc {
|
||
return CORSWithConfig(&config.CORSConfig{
|
||
AllowedOrigins: origins,
|
||
})
|
||
}
|
||
|
||
// CORSWithWildcard 允许所有来源的 CORS 中间件(仅用于开发环境)
|
||
// 注意:生产环境不建议使用
|
||
func CORSWithWildcard() gin.HandlerFunc {
|
||
return CORSWithConfig(&config.CORSConfig{
|
||
AllowedOrigins: []string{"*"},
|
||
})
|
||
}
|
||
|
||
// CORSForAPI 适用于 API 的 CORS 中间件
|
||
func CORSForAPI() gin.HandlerFunc {
|
||
return CORSWithConfig(&config.CORSConfig{
|
||
AllowedOrigins: []string{"*"},
|
||
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
|
||
AllowedHeaders: []string{"Content-Type", "Authorization", "X-Requested-With"},
|
||
AllowCredentials: false, // API 模式不允许凭证
|
||
})
|
||
} |