package http import ( "context" stdhttp "net/http" "net/netip" "strings" "backupx/server/internal/apperror" "backupx/server/internal/security" "backupx/server/pkg/response" "github.com/gin-gonic/gin" ) // ForwardedHeadersMiddleware 只允许配置中的反向代理提供转发头。 // Gin 的 trusted_proxies 保护 ClientIP;这里同步保护安装命令使用的 // X-Forwarded-Host 与 X-Forwarded-Proto,避免直连请求伪造 Agent 地址。 func ForwardedHeadersMiddleware(trustedProxies []string) gin.HandlerFunc { trustedPrefixes := make([]netip.Prefix, 0, len(trustedProxies)) for _, raw := range trustedProxies { raw = strings.TrimSpace(raw) if prefix, err := netip.ParsePrefix(raw); err == nil { trustedPrefixes = append(trustedPrefixes, prefix) continue } if addr, err := netip.ParseAddr(raw); err == nil { trustedPrefixes = append(trustedPrefixes, netip.PrefixFrom(addr, addr.BitLen())) } } return func(c *gin.Context) { remote, err := netip.ParseAddrPort(c.Request.RemoteAddr) trusted := false if err == nil { remoteAddr := remote.Addr().Unmap() for _, prefix := range trustedPrefixes { if prefix.Contains(remoteAddr) { trusted = true break } } } if !trusted { for _, header := range []string{ "Forwarded", "X-Forwarded-For", "X-Forwarded-Host", "X-Forwarded-Port", "X-Forwarded-Proto", "X-Real-IP", } { c.Request.Header.Del(header) } } c.Next() } } // CORSMiddleware handles Cross-Origin Resource Sharing for the API. func CORSMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization") c.Header("Access-Control-Max-Age", "86400") if c.Request.Method == stdhttp.MethodOptions { c.AbortWithStatus(stdhttp.StatusNoContent) return } c.Next() } } // ApiKeyAuthenticator 抽象 API Key 验证能力,避免 middleware 直接依赖 service 包。 // 实现方:service.ApiKeyService。未注入时 AuthMiddleware 仍然支持 JWT。 type ApiKeyAuthenticator interface { Authenticate(ctx context.Context, rawKey string) (subject string, role string, err error) } // AuthMiddleware 支持两种认证方式: // - JWT (Authorization: Bearer ):交互式用户 // - API Key (Authorization: Bearer bax_xxx 或 X-Api-Key: bax_xxx):第三方脚本 // // JWT 会在 context 中写入 userSubject / userRole / username; // API Key 会写入 authSubject=api_key: / userRole=。 func AuthMiddleware(jwtManager *security.JWTManager, apiKeyAuth ApiKeyAuthenticator) gin.HandlerFunc { return func(c *gin.Context) { rawToken := extractAuthToken(c) if rawToken == "" { response.Error(c, apperror.Unauthorized("AUTH_REQUIRED", "请先登录", nil)) c.Abort() return } if apiKeyAuth != nil && strings.HasPrefix(rawToken, "bax_") { subject, role, err := apiKeyAuth.Authenticate(c.Request.Context(), rawToken) if err != nil { response.Error(c, err) c.Abort() return } c.Set(contextAuthSubjectKey, subject) c.Set(contextUserRoleKey, role) c.Set(contextUserSubjectKey, subject) c.Set(contextUsernameKey, subject) c.Next() return } claims, err := jwtManager.Parse(rawToken) if err != nil { response.Error(c, apperror.Unauthorized("AUTH_INVALID_TOKEN", "登录状态已失效,请重新登录", err)) c.Abort() return } c.Set(contextUserSubjectKey, claims.Subject) c.Set(contextUserRoleKey, claims.Role) c.Set(contextUsernameKey, claims.Username) c.Set(contextAuthSubjectKey, "user:"+claims.Subject) c.Next() } } // extractAuthToken 从 Authorization: Bearer 或 X-Api-Key 中提取原始 token。 func extractAuthToken(c *gin.Context) string { header := strings.TrimSpace(c.GetHeader("Authorization")) if strings.HasPrefix(header, "Bearer ") { return strings.TrimSpace(strings.TrimPrefix(header, "Bearer ")) } if key := strings.TrimSpace(c.GetHeader("X-Api-Key")); key != "" { return key } return "" } // RequireRole 仅放行指定角色,否则返回 403。 // 必须用在 AuthMiddleware 之后。viewer 只读保护、admin 管理端都靠它。 func RequireRole(roles ...string) gin.HandlerFunc { allowed := make(map[string]bool, len(roles)) for _, r := range roles { allowed[strings.ToLower(r)] = true } return func(c *gin.Context) { role, _ := c.Get(contextUserRoleKey) roleStr := "" if v, ok := role.(string); ok { roleStr = strings.ToLower(v) } if !allowed[roleStr] { response.Error(c, apperror.New(403, "AUTH_FORBIDDEN", "当前角色无权执行此操作", nil)) c.Abort() return } c.Next() } } // RequireNotViewer 是 RequireRole(admin, operator) 的快捷方式, // 用于任何"写入/变更"类端点,禁止 viewer 触发。 func RequireNotViewer() gin.HandlerFunc { return RequireRole("admin", "operator") } func ClientKey(c *gin.Context) string { ip := strings.TrimSpace(c.ClientIP()) if ip == "" { return "unknown" } return ip }