Files
photography/backend/internal/middleware/cors.go
xujiang 5b3fc9bf9c feat: 完成后端中间件系统完善
## 🛡️ 新增功能
- 实现完整的CORS中间件,支持开发/生产环境配置
- 实现请求日志中间件,完整的请求生命周期记录
- 实现全局错误处理中间件,统一错误响应格式
- 创建中间件管理器,支持链式中间件和配置管理

## 🔧 技术改进
- 更新配置系统支持中间件配置
- 修复go-zero日志API兼容性问题
- 创建完整的中间件测试用例
- 编译测试通过,功能完整可用

## 📊 进度提升
- 项目总进度从42.5%提升至50.0%
- 中优先级任务完成率达55%
- 3个中优先级任务同时完成

## 🎯 完成的任务
14. 实现 CORS 中间件
16. 实现请求日志中间件
17. 完善全局错误处理

Co-authored-by: Claude Code <claude@anthropic.com>
2025-07-11 13:55:38 +08:00

175 lines
4.2 KiB
Go

package middleware
import (
"net/http"
"strings"
"time"
"github.com/zeromicro/go-zero/core/logx"
)
// CORSConfig CORS 配置
type CORSConfig struct {
AllowOrigins []string // 允许的来源
AllowMethods []string // 允许的方法
AllowHeaders []string // 允许的头部
ExposeHeaders []string // 暴露的头部
AllowCredentials bool // 是否允许携带凭证
MaxAge time.Duration // 预检请求缓存时间
}
// DefaultCORSConfig 默认 CORS 配置
func DefaultCORSConfig() CORSConfig {
return CORSConfig{
AllowOrigins: []string{
"http://localhost:3000",
"http://localhost:3001",
"http://localhost:5173",
"http://localhost:8080",
},
AllowMethods: []string{
"GET",
"POST",
"PUT",
"DELETE",
"OPTIONS",
"HEAD",
},
AllowHeaders: []string{
"Origin",
"Content-Type",
"Content-Length",
"Accept-Encoding",
"X-CSRF-Token",
"Authorization",
"accept",
"origin",
"Cache-Control",
"X-Requested-With",
},
ExposeHeaders: []string{
"Content-Length",
"Content-Type",
},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}
}
// ProductionCORSConfig 生产环境 CORS 配置
func ProductionCORSConfig(allowedOrigins []string) CORSConfig {
config := DefaultCORSConfig()
if len(allowedOrigins) > 0 {
config.AllowOrigins = allowedOrigins
} else {
// 生产环境默认只允许 HTTPS
config.AllowOrigins = []string{
"https://photography.iriver.top",
"https://admin.photography.iriver.top",
}
}
return config
}
// CORSMiddleware CORS 中间件
type CORSMiddleware struct {
config CORSConfig
}
// NewCORSMiddleware 创建 CORS 中间件
func NewCORSMiddleware(config CORSConfig) *CORSMiddleware {
return &CORSMiddleware{
config: config,
}
}
// Handle 处理 CORS
func (m *CORSMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// 检查来源是否被允许
if origin != "" && m.isOriginAllowed(origin) {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
// 设置允许的方法
if len(m.config.AllowMethods) > 0 {
w.Header().Set("Access-Control-Allow-Methods", strings.Join(m.config.AllowMethods, ", "))
}
// 设置允许的头部
if len(m.config.AllowHeaders) > 0 {
w.Header().Set("Access-Control-Allow-Headers", strings.Join(m.config.AllowHeaders, ", "))
}
// 设置暴露的头部
if len(m.config.ExposeHeaders) > 0 {
w.Header().Set("Access-Control-Expose-Headers", strings.Join(m.config.ExposeHeaders, ", "))
}
// 设置是否允许携带凭证
if m.config.AllowCredentials {
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
// 设置预检请求缓存时间
if m.config.MaxAge > 0 {
w.Header().Set("Access-Control-Max-Age", m.config.MaxAge.String())
}
// 添加安全头部
m.setSecurityHeaders(w)
// 处理预检请求
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
// 记录跨域请求
if origin != "" {
logx.Infof("CORS request from origin: %s, method: %s, path: %s", origin, r.Method, r.URL.Path)
}
next(w, r)
})
}
// isOriginAllowed 检查来源是否被允许
func (m *CORSMiddleware) isOriginAllowed(origin string) bool {
for _, allowedOrigin := range m.config.AllowOrigins {
if allowedOrigin == "*" {
return true
}
if allowedOrigin == origin {
return true
}
// 支持通配符匹配
if strings.HasSuffix(allowedOrigin, "*") {
prefix := strings.TrimSuffix(allowedOrigin, "*")
if strings.HasPrefix(origin, prefix) {
return true
}
}
}
return false
}
// setSecurityHeaders 设置安全头部
func (m *CORSMiddleware) setSecurityHeaders(w http.ResponseWriter) {
// 防止点击劫持
w.Header().Set("X-Frame-Options", "DENY")
// 防止 MIME 类型嗅探
w.Header().Set("X-Content-Type-Options", "nosniff")
// XSS 保护
w.Header().Set("X-XSS-Protection", "1; mode=block")
// 引用者策略
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
// 内容安全策略 (基础版)
w.Header().Set("Content-Security-Policy", "default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'")
}