131 lines
3.1 KiB
Go
131 lines
3.1 KiB
Go
package api
|
||
|
||
import (
|
||
"crypto/subtle"
|
||
"encoding/base64"
|
||
"net/http"
|
||
"os"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
const agentAuthRealm = "FLNXI Agent"
|
||
const agentAuthConfigRequiredMessage = "Agent 管理认证未配置,管理面默认关闭"
|
||
|
||
type AgentAuthConfig struct {
|
||
Username string
|
||
Password string
|
||
IsConfigured bool
|
||
IsPartial bool
|
||
}
|
||
|
||
func LoadAgentAuthConfig() AgentAuthConfig {
|
||
username := strings.TrimSpace(os.Getenv("AGENT_AUTH_USERNAME"))
|
||
password := os.Getenv("AGENT_AUTH_PASSWORD")
|
||
|
||
return AgentAuthConfig{
|
||
Username: username,
|
||
Password: password,
|
||
IsConfigured: username != "" && password != "",
|
||
IsPartial: (username != "" && password == "") || (username == "" && password != ""),
|
||
}
|
||
}
|
||
|
||
func LogAgentSecurityConfig(config AgentAuthConfig) {
|
||
if config.IsPartial {
|
||
logMessage := "[Security] AGENT_AUTH_USERNAME 和 AGENT_AUTH_PASSWORD 需要同时设置;当前仅保留 /api/health,其余 Agent 管理面已关闭"
|
||
println(logMessage)
|
||
return
|
||
}
|
||
|
||
if config.IsConfigured {
|
||
println("[Security] Agent 管理认证已启用")
|
||
return
|
||
}
|
||
|
||
println("[Security] Agent 管理认证未配置;当前仅保留 /api/health,其余 Agent 管理面默认关闭")
|
||
}
|
||
|
||
func safeEqual(left, right string) bool {
|
||
leftBytes := []byte(left)
|
||
rightBytes := []byte(right)
|
||
|
||
if len(leftBytes) != len(rightBytes) {
|
||
return false
|
||
}
|
||
|
||
return subtle.ConstantTimeCompare(leftBytes, rightBytes) == 1
|
||
}
|
||
|
||
func parseBasicAuth(header string) (string, string, bool) {
|
||
if !strings.HasPrefix(header, "Basic ") {
|
||
return "", "", false
|
||
}
|
||
|
||
decoded, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(header, "Basic "))
|
||
if err != nil {
|
||
return "", "", false
|
||
}
|
||
|
||
parts := strings.SplitN(string(decoded), ":", 2)
|
||
if len(parts) != 2 {
|
||
return "", "", false
|
||
}
|
||
|
||
return parts[0], parts[1], true
|
||
}
|
||
|
||
func isAuthorized(header string, config AgentAuthConfig) bool {
|
||
if !config.IsConfigured {
|
||
return false
|
||
}
|
||
|
||
username, password, ok := parseBasicAuth(header)
|
||
if !ok {
|
||
return false
|
||
}
|
||
|
||
return safeEqual(username, config.Username) && safeEqual(password, config.Password)
|
||
}
|
||
|
||
func AgentSecurityMiddleware(config AgentAuthConfig) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
c.Header("X-Frame-Options", "SAMEORIGIN")
|
||
c.Header("X-Content-Type-Options", "nosniff")
|
||
c.Header("Referrer-Policy", "same-origin")
|
||
c.Header("Cache-Control", "no-store")
|
||
|
||
if c.Request.URL.Path == "/api/health" {
|
||
c.Next()
|
||
return
|
||
}
|
||
|
||
if !config.IsConfigured {
|
||
if strings.HasPrefix(c.Request.URL.Path, "/api/") {
|
||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": agentAuthConfigRequiredMessage})
|
||
} else {
|
||
c.String(http.StatusServiceUnavailable, agentAuthConfigRequiredMessage)
|
||
}
|
||
|
||
c.Abort()
|
||
return
|
||
}
|
||
|
||
if !isAuthorized(c.GetHeader("Authorization"), config) {
|
||
c.Header("WWW-Authenticate", `Basic realm="`+agentAuthRealm+`", charset="UTF-8"`)
|
||
|
||
if strings.HasPrefix(c.Request.URL.Path, "/api/") {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"error": "需要 Agent 管理认证"})
|
||
} else {
|
||
c.String(http.StatusUnauthorized, "需要 Agent 管理认证")
|
||
}
|
||
|
||
c.Abort()
|
||
return
|
||
}
|
||
|
||
c.Next()
|
||
}
|
||
}
|