diff --git a/config/config.go b/config/config.go index fca869d..7e1adcf 100644 --- a/config/config.go +++ b/config/config.go @@ -14,10 +14,10 @@ import ( // ServerConfig 服务器配置结构体 // 包含服务器运行相关的配置信息 type ServerConfig struct { - Host string `json:"host" mapstructure:"host"` // 服务器监听地址 - Port int `json:"port" mapstructure:"port"` // 服务器监听端口 - Mode string `json:"mode" mapstructure:"mode"` // 运行模式(debug/release) - Dist string `json:"dist" mapstructure:"dist"` // 静态文件目录 + Host string `json:"host" mapstructure:"host"` // 服务器监听地址 + Port int `json:"port" mapstructure:"port"` // 服务器监听端口 + Dist string `json:"dist" mapstructure:"dist"` // 静态文件目录 + DevMode bool `json:"dev_mode" mapstructure:"dev_mode"` // 开发模式(跳过验证码等) } // DatabaseConfig 数据库配置结构体 @@ -78,10 +78,10 @@ type CookieConfig struct { // SecurityConfig 安全配置结构体 // 包含应用程序安全相关的配置信息 type SecurityConfig struct { - JWTSecret string `json:"jwt_secret" mapstructure:"jwt_secret"` // JWT签名密钥 - EncryptionKey string `json:"encryption_key" mapstructure:"encryption_key"` // 数据加密密钥 - JWTRefreshThresholdHours int `json:"jwt_refresh_threshold_hours" mapstructure:"jwt_refresh_threshold_hours"` // JWT令牌刷新阈值(小时) - Cookie CookieConfig `json:"cookie" mapstructure:"cookie"` // Cookie配置 + JWTSecret string `json:"jwt_secret" mapstructure:"jwt_secret"` // JWT签名密钥 + EncryptionKey string `json:"encryption_key" mapstructure:"encryption_key"` // 数据加密密钥 + JWTRefresh int `json:"jwt_refresh" mapstructure:"jwt_refresh"` // JWT令牌刷新阈值(小时) + Cookie CookieConfig `json:"cookie" mapstructure:"cookie"` // Cookie配置 } // AppConfig 应用配置结构体 @@ -97,10 +97,10 @@ type AppConfig struct { func GetDefaultAppConfig() *AppConfig { return &AppConfig{ Server: ServerConfig{ - Host: "0.0.0.0", - Port: 8080, - Mode: "debug", - Dist: "", + Host: "0.0.0.0", + Port: 8080, + Dist: "", + DevMode: false, }, Database: DatabaseConfig{ Type: "sqlite", @@ -132,9 +132,9 @@ func GetDefaultAppConfig() *AppConfig { MaxAge: 30, }, Security: SecurityConfig{ - JWTSecret: "", - EncryptionKey: "", - JWTRefreshThresholdHours: 6, + JWTSecret: "", + EncryptionKey: "", + JWTRefresh: 6, Cookie: CookieConfig{ Secure: true, SameSite: "Lax", diff --git a/config/validator.go b/config/validator.go index 69c507e..4f141aa 100644 --- a/config/validator.go +++ b/config/validator.go @@ -75,12 +75,6 @@ func validateServerConfig(config *ServerConfig) error { return fmt.Errorf("无效的端口号: %d,端口号必须在1-65535之间", config.Port) } - // 验证运行模式 - validModes := []string{"debug", "release", "test"} - if !contains(validModes, config.Mode) { - return fmt.Errorf("无效的运行模式: %s,支持的模式: %s", config.Mode, strings.Join(validModes, ", ")) - } - return nil } @@ -200,7 +194,7 @@ func validateSecurityConfig(config *SecurityConfig) error { return errors.New("加密密钥长度不能少于16个字符") } - if config.JWTRefreshThresholdHours < 1 || config.JWTRefreshThresholdHours > 23 { + if config.JWTRefresh < 1 || config.JWTRefresh > 23 { return errors.New("JWT令牌刷新阈值必须在1-23小时之间") } diff --git a/controllers/admin/auth.go b/controllers/admin/auth.go index d86a4f6..4444712 100644 --- a/controllers/admin/auth.go +++ b/controllers/admin/auth.go @@ -185,13 +185,15 @@ func clearInvalidJWTCookie(w http.ResponseWriter) { http.SetCookie(w, cookie) } -// JWT密钥(生产环境应从配置文件或环境变量读取) -var jwtSecret = []byte(viper.GetString("security.jwt_secret")) +// getJWTSecret 动态获取当前的JWT密钥 +// 修复安全漏洞:确保每次都从最新配置中获取密钥,而不是使用启动时的全局变量 +func getJWTSecret() []byte { + return []byte(viper.GetString("security.jwt_secret")) +} // JWTClaims JWT载荷结构 type JWTClaims struct { Username string `json:"username"` - IsAdmin bool `json:"is_admin"` // 是否为管理员 PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改 jwt.RegisteredClaims } @@ -206,7 +208,6 @@ func generateJWTTokenForAdmin(adminUser models.User) (string, error) { claims := JWTClaims{ Username: adminUser.Username, - IsAdmin: true, // 管理员 PasswordHash: passwordHashDigest, // 包含密码哈希摘要 RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), @@ -218,7 +219,7 @@ func generateJWTTokenForAdmin(adminUser models.User) (string, error) { } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString(jwtSecret) + return token.SignedString(getJWTSecret()) } // parseJWTToken 解析并验证JWT令牌 @@ -230,7 +231,7 @@ func parseJWTToken(tokenString string) (*JWTClaims, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } - return jwtSecret, nil + return getJWTSecret(), nil }) if err != nil { @@ -244,11 +245,48 @@ func parseJWTToken(tokenString string) (*JWTClaims, error) { return nil, fmt.Errorf("invalid token") } +// getJWTCookie 获取JWT cookie的通用函数 +func getJWTCookie(r *http.Request) (*http.Cookie, error) { + return r.Cookie("admin_session") +} + +// validateAdminPasswordHash 验证管理员密码哈希的通用函数 +func validateAdminPasswordHash(claims *JWTClaims, r *http.Request) bool { + // 【安全修复】验证数据库中的当前密码哈希 + // 这确保了密码修改后,旧的JWT令牌会失效 + db, err := database.GetDB() + if err != nil { + fmt.Printf("[SECURITY WARNING] Database connection failed during auth - Username=%s, IP=%s\n", + claims.Username, r.RemoteAddr) + return false + } + + // 获取当前数据库中的管理员密码 + var adminPassword models.Settings + if err := db.Where("name = ?", "admin_password").First(&adminPassword).Error; err != nil { + fmt.Printf("[SECURITY WARNING] Admin password not found in database - Username=%s, IP=%s\n", + claims.Username, r.RemoteAddr) + return false + } + + // 生成当前数据库密码的哈希摘要 + currentPasswordHash := utils.GenerateSHA256Hash(adminPassword.Value) + + // 验证JWT中的密码哈希是否与当前数据库中的密码哈希一致 + if claims.PasswordHash != currentPasswordHash { + fmt.Printf("[SECURITY WARNING] Password hash mismatch - JWT token invalidated - Username=%s, IP=%s\n", + claims.Username, r.RemoteAddr) + return false + } + + return true +} + // IsAdminAuthenticated 判断管理员是否已认证(导出) // - 检查admin_session Cookie中的JWT令牌 // - 验证令牌签名、过期时间和用户角色 func IsAdminAuthenticated(r *http.Request) bool { - cookie, err := r.Cookie("admin_session") + cookie, err := getJWTCookie(r) if err != nil || cookie.Value == "" { return false } @@ -259,27 +297,17 @@ func IsAdminAuthenticated(r *http.Request) bool { return false } - // 验证用户角色(只允许管理员) - if !claims.IsAdmin { - return false - } + // 注释:由于这是管理员专用认证函数,不需要额外的角色验证 - // 对于管理员,不需要验证数据库中的用户记录,因为管理员信息存储在settings中 - // 只需要验证JWT中的信息即可 - if !claims.IsAdmin { - fmt.Printf("[SECURITY WARNING] Invalid admin token detected - Username=%s, IP=%s\n", - claims.Username, r.RemoteAddr) - return false - } - - return true + // 验证密码哈希 + return validateAdminPasswordHash(claims, r) } // IsAdminAuthenticatedWithCleanup 带自动清理功能的JWT校验函数 // - 当JWT校验失败时,自动清理失效的Cookie // - 适用于API接口等需要清理失效令牌的场景 func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) bool { - cookie, err := r.Cookie("admin_session") + cookie, err := getJWTCookie(r) if err != nil || cookie.Value == "" { return false } @@ -292,17 +320,10 @@ func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) boo return false } - // 验证用户角色(只允许管理员) - if !claims.IsAdmin { - clearInvalidJWTCookie(w) - return false - } + // 注释:由于这是管理员专用认证函数,不需要额外的角色验证 - // 对于管理员,不需要验证数据库中的用户记录,因为管理员信息存储在settings中 - // 只需要验证JWT中的信息即可 - if !claims.IsAdmin { - fmt.Printf("[SECURITY WARNING] Invalid admin token detected - Username=%s, IP=%s\n", - claims.Username, r.RemoteAddr) + // 验证密码哈希 + if !validateAdminPasswordHash(claims, r) { clearInvalidJWTCookie(w) return false } @@ -315,7 +336,7 @@ func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) boo // - 自动刷新接近过期的令牌(剩余时间少于6小时时刷新) // - 返回用户ID、用户名和角色 func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) { - cookie, err := r.Cookie("admin_session") + cookie, err := getJWTCookie(r) if err != nil { return nil, fmt.Errorf("未找到会话信息") } @@ -325,15 +346,7 @@ func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) { return nil, fmt.Errorf("无效的会话信息") } - if !claims.IsAdmin { - return nil, fmt.Errorf("权限不足") - } - - // 对于管理员,不需要验证数据库中的用户记录,因为管理员信息存储在settings中 - // 只需要验证JWT中的信息即可 - if !claims.IsAdmin { - return nil, fmt.Errorf("无效的管理员令牌") - } + // 注释:由于这是管理员专用函数,不需要额外的角色验证 return claims, nil } @@ -343,7 +356,7 @@ func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) { // - 自动刷新接近过期的令牌(剩余时间少于6小时时刷新) // - 返回用户ID、用户名、角色和是否刷新了令牌 func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JWTClaims, bool, error) { - cookie, err := r.Cookie("admin_session") + cookie, err := getJWTCookie(r) if err != nil { return nil, false, fmt.Errorf("未找到会话信息") } @@ -353,19 +366,16 @@ func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JW return nil, false, fmt.Errorf("无效的会话信息") } - if !claims.IsAdmin { - return nil, false, fmt.Errorf("权限不足") - } + // 注释:由于这是管理员专用函数,不需要额外的角色验证 - // 对于管理员,不需要验证数据库中的用户记录,因为管理员信息存储在settings中 - // 只需要验证JWT中的信息即可 - if !claims.IsAdmin { - return nil, false, fmt.Errorf("无效的管理员令牌") + // 验证密码哈希 + if !validateAdminPasswordHash(claims, r) { + return nil, false, fmt.Errorf("会话已失效,请重新登录") } // 检查是否需要刷新令牌(根据配置的阈值) refreshed := false - refreshThreshold := time.Duration(viper.GetInt("security.jwt_refresh_threshold_hours")) * time.Hour + refreshThreshold := time.Duration(viper.GetInt("security.jwt_refresh")) * time.Hour if time.Until(claims.ExpiresAt.Time) < refreshThreshold { // 为管理员生成新的JWT令牌 adminUser := models.User{ diff --git a/controllers/admin/captcha.go b/controllers/admin/captcha.go index 6c7d471..56d65c1 100644 --- a/controllers/admin/captcha.go +++ b/controllers/admin/captcha.go @@ -11,6 +11,7 @@ import ( "networkDev/utils" "github.com/mojocn/base64Captcha" + "github.com/spf13/viper" ) // 全局验证码存储器 @@ -91,6 +92,11 @@ func CaptchaHandler(w http.ResponseWriter, r *http.Request) { // 这个函数将在登录处理中被调用 // 支持大小写不敏感匹配 func VerifyCaptcha(r *http.Request, captchaValue string) bool { + // 检查是否为开发模式,如果是则跳过验证码验证 + if viper.GetBool("server.dev_mode") { + return true + } + // 从cookie中获取验证码ID cookie, err := r.Cookie("captcha_id") if err != nil { diff --git a/controllers/admin/handlers.go b/controllers/admin/handlers.go index a5dc7e3..163c7a0 100644 --- a/controllers/admin/handlers.go +++ b/controllers/admin/handlers.go @@ -3,6 +3,7 @@ package admin import ( "net/http" "networkDev/database" + "networkDev/models" "networkDev/services" "networkDev/utils" "networkDev/utils/timeutil" @@ -10,7 +11,24 @@ import ( "github.com/spf13/viper" ) -// AdminIndexHandler /admin 与 /admin/ 根路径入口 +// formatDBType 格式化数据库类型显示 +// 将配置文件中的小写类型转换为友好的显示格式 +func formatDBType(dbType string) string { + switch dbType { + case "mysql": + return "MySQL" + case "sqlite": + return "SQLite" + case "postgresql", "postgres": + return "PostgreSQL" + case "sqlserver": + return "SQL Server" + default: + return "SQLite" // 默认显示 + } +} + +// AdminIndexHandler 后台首页处理器/admin 与 /admin/ 根路径入口 // - 未登录:重定向到 /admin/login // - 已登录:渲染后台布局页(或重定向到 /admin/layout) // - 自动清理失效的JWT Cookie @@ -71,10 +89,10 @@ func AdminLayoutHandler(w http.ResponseWriter, r *http.Request) { } // DashboardFragmentHandler 仪表盘片段渲染 -// - 展示系统信息:版本、运行模式、数据库类型、启动时长 +// - 展示系统信息:版本、开发模式、数据库类型、启动时长 func DashboardFragmentHandler(w http.ResponseWriter, r *http.Request) { version := "1.0.0" - mode := viper.GetString("server.mode") + mode := viper.GetBool("server.dev_mode") dbType := viper.GetString("database.type") if dbType == "" { dbType = "sqlite" @@ -84,7 +102,7 @@ func DashboardFragmentHandler(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Version": version, "Mode": mode, - "DBType": dbType, + "DBType": formatDBType(dbType), "Uptime": uptime, } @@ -100,7 +118,7 @@ func SystemInfoHandler(w http.ResponseWriter, r *http.Request) { } version := "1.0.0" - mode := viper.GetString("server.mode") + mode := viper.GetBool("server.dev_mode") dbType := viper.GetString("database.type") if dbType == "" { dbType = "sqlite" @@ -110,9 +128,64 @@ func SystemInfoHandler(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "version": version, "mode": mode, - "db_type": dbType, + "db_type": formatDBType(dbType), "uptime": uptime, } utils.JsonResponse(w, http.StatusOK, true, "ok", data) } + +// DashboardStatsHandler 仪表盘统计数据API接口 +// - 返回应用统计数据的JSON数据,包括全部/启用/禁用/变量数量 +func DashboardStatsHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + // 获取数据库连接 + db, err := database.GetDB() + if err != nil { + utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil) + return + } + + // 统计应用数据 + var totalApps int64 + var enabledApps int64 + var disabledApps int64 + var totalVariables int64 + + // 统计全部应用数量 + if err := db.Model(&models.App{}).Count(&totalApps).Error; err != nil { + utils.JsonResponse(w, http.StatusInternalServerError, false, "统计应用数量失败", nil) + return + } + + // 统计启用应用数量 + if err := db.Model(&models.App{}).Where("status = ?", 1).Count(&enabledApps).Error; err != nil { + utils.JsonResponse(w, http.StatusInternalServerError, false, "统计启用应用数量失败", nil) + return + } + + // 统计禁用应用数量 + if err := db.Model(&models.App{}).Where("status = ?", 0).Count(&disabledApps).Error; err != nil { + utils.JsonResponse(w, http.StatusInternalServerError, false, "统计禁用应用数量失败", nil) + return + } + + // 统计变量数量 + if err := db.Model(&models.Variable{}).Count(&totalVariables).Error; err != nil { + utils.JsonResponse(w, http.StatusInternalServerError, false, "统计变量数量失败", nil) + return + } + + data := map[string]interface{}{ + "total_apps": totalApps, + "enabled_apps": enabledApps, + "disabled_apps": disabledApps, + "total_variables": totalVariables, + } + + utils.JsonResponse(w, http.StatusOK, true, "ok", data) +} diff --git a/controllers/admin/user.go b/controllers/admin/user.go index 3c44059..d6387a9 100644 --- a/controllers/admin/user.go +++ b/controllers/admin/user.go @@ -81,11 +81,7 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) { return } - // 确认是管理员 - if !claims.IsAdmin { - utils.JsonResponse(w, http.StatusForbidden, false, "权限不足", nil) - return - } + // 注释:由于使用了AdminAuthRequired中间件,已确保是管理员用户 // 获取数据库连接 db, err := database.GetDB() @@ -176,7 +172,7 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) { return } - claims, _, err := GetCurrentAdminUserWithRefresh(w, r) + _, _, err := GetCurrentAdminUserWithRefresh(w, r) if err != nil { utils.JsonResponse(w, http.StatusUnauthorized, false, "未登录或会话已过期", nil) return @@ -207,11 +203,7 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) { return } - // 确认当前用户是管理员 - if !claims.IsAdmin { - utils.JsonResponse(w, http.StatusForbidden, false, "权限不足", nil) - return - } + // 注释:由于使用了AdminAuthRequired中间件,已确保是管理员用户 // 获取所有管理员相关设置 var adminSettings []models.Settings diff --git a/database/settings.go b/database/settings.go index d43bd89..0ebc393 100644 --- a/database/settings.go +++ b/database/settings.go @@ -144,7 +144,7 @@ func initDefaultAdmin(db *gorm.DB) error { // 如果密码已设置,跳过初始化 if passwordSetting.Value != "" { - logrus.Info("管理员密码已设置,跳过默认密码初始化") + logrus.Debug("管理员密码已设置,跳过默认密码初始化") return nil } diff --git a/server/admin.go b/server/admin.go index dc8c871..752bafd 100644 --- a/server/admin.go +++ b/server/admin.go @@ -54,6 +54,9 @@ func RegisterAdminRoutes(mux *http.ServeMux) { // 系统信息API(用于仪表盘定时刷新) mux.HandleFunc("/admin/api/system/info", adminctl.AdminAuthRequired(adminctl.SystemInfoHandler)) + // 仪表盘统计数据API + mux.HandleFunc("/admin/api/dashboard/stats", adminctl.AdminAuthRequired(adminctl.DashboardStatsHandler)) + // 个人资料API mux.HandleFunc("/admin/api/user/profile", adminctl.AdminAuthRequired(adminctl.UserProfileQueryHandler)) mux.HandleFunc("/admin/api/user/profile/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.UserProfileUpdateHandler))) diff --git a/web/template/admin/apis.html b/web/template/admin/apis.html index 4430ba2..2f6c540 100644 --- a/web/template/admin/apis.html +++ b/web/template/admin/apis.html @@ -1,16 +1,16 @@ {{ define "apis.html" }}
-

接口管理

d -
-
筛选
d -
+

接口管理

+
+

筛选

+
@@ -30,9 +30,9 @@
-
-
接口列表
-
+
+

接口列表

+
{{ end }} \ No newline at end of file diff --git a/web/template/admin/settings.html b/web/template/admin/settings.html index 44d718a..4cc266b 100644 --- a/web/template/admin/settings.html +++ b/web/template/admin/settings.html @@ -2,9 +2,9 @@

系统设置

-
-
基本信息设置
-
+
+

基本信息设置

+
@@ -35,9 +35,9 @@
-
-
系统配置
-
+
+

系统配置

+
@@ -72,9 +72,9 @@
-
-
页脚与备案
-
+
+

页脚与备案

+
diff --git a/web/template/admin/user.html b/web/template/admin/user.html index 785e60f..aa15e56 100644 --- a/web/template/admin/user.html +++ b/web/template/admin/user.html @@ -9,9 +9,9 @@
-
-
修改密码
-
+
+

修改密码

+
@@ -51,9 +51,9 @@
-
-
修改用户名
-
+
+

修改用户名

+
diff --git a/web/template/admin/variables.html b/web/template/admin/variables.html index 92edf6f..c8a5caf 100644 --- a/web/template/admin/variables.html +++ b/web/template/admin/variables.html @@ -7,9 +7,9 @@ 批量删除
-
-
筛选
-
+
+

筛选

+
@@ -35,9 +35,9 @@
-
-
变量列表
-
+
+

变量列表

+
diff --git a/web/template/layui-theme-dark b/web/template/layui-theme-dark new file mode 160000 index 0000000..a89e678 --- /dev/null +++ b/web/template/layui-theme-dark @@ -0,0 +1 @@ +Subproject commit a89e6787f40f512de9e2ee538bc527a48dfddefa