Files
BackupX/server/internal/http/middleware.go
Wu Qing 5827074334 feat: 优化集群部署与堡垒机接入 (#106)
支持受限网络、正向代理、私有 CA 与 SSH 堡垒机部署 Agent。

加固 Docker、systemd、Nginx、安装器、Release 校验与可信代理边界,并完善命令队列索引、前端安装向导及中英文运维文档。
2026-08-09 02:45:17 +08:00

166 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 <jwt>):交互式用户
// - API Key (Authorization: Bearer bax_xxx 或 X-Api-Key: bax_xxx):第三方脚本
//
// JWT 会在 context 中写入 userSubject / userRole / username
// API Key 会写入 authSubject=api_key:<id> / userRole=<key role>。
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
}