mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-08-12 07:54:14 +08:00
支持受限网络、正向代理、私有 CA 与 SSH 堡垒机部署 Agent。 加固 Docker、systemd、Nginx、安装器、Release 校验与可信代理边界,并完善命令队列索引、前端安装向导及中英文运维文档。
166 lines
5.0 KiB
Go
166 lines
5.0 KiB
Go
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
|
||
}
|