更新底层架构

This commit is contained in:
2026-03-18 21:51:17 +08:00
parent b69c6ccbca
commit 68bea98b81
71 changed files with 5220 additions and 7619 deletions

View File

@@ -3,9 +3,9 @@ package admin
import (
"encoding/hex"
"net/http"
"networkDev/controllers"
"networkDev/models"
"networkDev/utils/encrypt"
"NetworkAuth/controllers"
"NetworkAuth/models"
"NetworkAuth/utils/encrypt"
"strconv"
"strings"

View File

@@ -1,12 +1,12 @@
package admin
import (
"NetworkAuth/controllers"
"NetworkAuth/models"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"net/http"
"networkDev/controllers"
"networkDev/models"
"strconv"
"strings"
@@ -247,7 +247,7 @@ func AppResetSecretHandler(c *gin.Context) {
return
}
logrus.WithField("app_uuid", app.UUID).Info("Successfully reset app secret")
logrus.WithField("app_uuid", app.UUID).Debug("Successfully reset app secret")
c.JSON(http.StatusOK, gin.H{
"code": 0,
@@ -295,7 +295,7 @@ func AppCreateHandler(c *gin.Context) {
"download_type": req.DownloadType,
"download_url": req.DownloadURL,
"force_update": req.ForceUpdate,
}).Info("Received app create request")
}).Debug("Received app create request")
// 创建应用
app := models.App{
@@ -344,8 +344,9 @@ func AppCreateHandler(c *gin.Context) {
tx.Rollback()
logrus.WithError(err).Error("Failed to create app")
c.JSON(http.StatusInternalServerError, gin.H{
"code": 1,
"msg": "创建应用失败",
"code": 1,
"msg": "创建应用失败: " + err.Error(),
"error": err.Error(),
})
return
}
@@ -384,7 +385,7 @@ func AppCreateHandler(c *gin.Context) {
return
}
logrus.WithField("app_uuid", app.UUID).Info("Successfully created app with default APIs")
logrus.WithField("app_uuid", app.UUID).Debug("Successfully created app with default APIs")
c.JSON(http.StatusOK, gin.H{
"code": 0,
@@ -460,7 +461,7 @@ func AppUpdateHandler(c *gin.Context) {
return
}
logrus.WithField("app_id", app.ID).Info("Successfully updated app")
logrus.WithField("app_id", app.ID).Debug("Successfully updated app")
c.JSON(http.StatusOK, gin.H{
"code": 0,
@@ -557,7 +558,7 @@ func AppDeleteHandler(c *gin.Context) {
logrus.WithFields(logrus.Fields{
"app_id": app.ID,
"app_uuid": app.UUID,
}).Info("Successfully deleted app and related APIs")
}).Debug("Successfully deleted app and related APIs")
c.JSON(http.StatusOK, gin.H{
"code": 0,
@@ -629,7 +630,7 @@ func AppUpdateAppDataHandler(c *gin.Context) {
logrus.WithFields(logrus.Fields{
"app_uuid": req.UUID,
"app_name": app.Name,
}).Info("App data updated successfully")
}).Debug("App data updated successfully")
c.JSON(http.StatusOK, gin.H{
"code": 0,
@@ -701,7 +702,7 @@ func AppUpdateAnnouncementHandler(c *gin.Context) {
logrus.WithFields(logrus.Fields{
"app_uuid": req.UUID,
"app_name": app.Name,
}).Info("App announcement updated successfully")
}).Debug("App announcement updated successfully")
c.JSON(http.StatusOK, gin.H{
"code": 0,
@@ -870,7 +871,7 @@ func AppUpdateMultiConfigHandler(c *gin.Context) {
logrus.WithFields(logrus.Fields{
"app_uuid": req.UUID,
"app_name": app.Name,
}).Info("App multi config updated successfully")
}).Debug("App multi config updated successfully")
c.JSON(http.StatusOK, gin.H{
"code": 0,
@@ -1023,7 +1024,7 @@ func AppUpdateBindConfigHandler(c *gin.Context) {
logrus.WithFields(logrus.Fields{
"app_uuid": req.UUID,
"app_name": app.Name,
}).Info("App bind config updated successfully")
}).Debug("App bind config updated successfully")
c.JSON(http.StatusOK, gin.H{
"code": 0,
@@ -1161,7 +1162,7 @@ func AppUpdateRegisterConfigHandler(c *gin.Context) {
logrus.WithFields(logrus.Fields{
"app_uuid": req.UUID,
"app_name": app.Name,
}).Info("App register config updated successfully")
}).Debug("App register config updated successfully")
c.JSON(http.StatusOK, gin.H{
"code": 0,
@@ -1265,7 +1266,7 @@ func AppsBatchDeleteHandler(c *gin.Context) {
logrus.WithFields(logrus.Fields{
"app_ids": req.IDs,
"app_uuids": appUUIDs,
}).Info("Successfully batch deleted apps and related APIs")
}).Debug("Successfully batch deleted apps and related APIs")
c.JSON(http.StatusOK, gin.H{
"code": 0,

View File

@@ -1,16 +1,16 @@
package admin
import (
"NetworkAuth/controllers"
"NetworkAuth/database"
"NetworkAuth/models"
"NetworkAuth/services"
"NetworkAuth/utils"
"fmt"
"net/http"
"strings"
"time"
"networkDev/controllers"
"networkDev/database"
"networkDev/models"
"networkDev/utils"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/spf13/viper"
@@ -40,9 +40,9 @@ func LoginPageHandler(c *gin.Context) {
// 获取或生成CSRF令牌
var token string
if existingToken := utils.GetCSRFTokenFromCookie(c); existingToken != "" {
// 重用现有的Cookie令牌
token = existingToken
// 尝试从Cookie获取
if cookie, err := c.Cookie(CSRFCookieName); err == nil && cookie != "" {
token = cookie
} else {
// 生成新的CSRF令牌并设置到Cookie
newToken, err := utils.GenerateCSRFToken()
@@ -53,21 +53,14 @@ func LoginPageHandler(c *gin.Context) {
return
}
token = newToken
utils.SetCSRFToken(c, token)
setCSRFToken(c, token)
}
// 准备模板数据
extraData := gin.H{
"Title": "管理员登录",
}
data := authBaseController.GetDefaultTemplateData()
data["Title"] = "管理员登录"
data["CSRFToken"] = token
// 合并额外数据
for key, value := range extraData {
data[key] = value
}
c.HTML(http.StatusOK, "login.html", data)
}
@@ -76,21 +69,30 @@ func LoginPageHandler(c *gin.Context) {
// ============================================================================
// LoginHandler 管理员登录接口
// - 接收JSON: {username, password}
// - 接收JSON: {username, password, captcha, csrf_token}
// - 验证CSRF令牌
// - 验证验证码
// - 验证用户存在与密码正确性
// - 仅允许 Role=0 的管理员登录
// - 成功后设置简单的会话Cookie后续可切换为JWT或更完善的Session
// - 仅允许管理员登录
// - 成功后设置JWT Cookie
func LoginHandler(c *gin.Context) {
var body struct {
Username string `json:"username"`
Password string `json:"password"`
Captcha string `json:"captcha"`
Username string `json:"username"`
Password string `json:"password"`
Captcha string `json:"captcha"`
CSRFToken string `json:"csrf_token"`
}
if !authBaseController.BindJSON(c, &body) {
return
}
// 1. 验证CSRF令牌 (Gin 方式)
if !validateCSRFToken(c, body.CSRFToken) {
authBaseController.HandleValidationError(c, "CSRF令牌验证失败")
return
}
if !authBaseController.ValidateRequired(c, map[string]interface{}{
"用户名": body.Username,
"密码": body.Password,
@@ -101,95 +103,133 @@ func LoginHandler(c *gin.Context) {
// 验证验证码
if !VerifyCaptcha(c, body.Captcha) {
recordLoginLog(c, body.Username, 0, "验证码错误")
authBaseController.HandleValidationError(c, "验证码错误")
return
}
// 获取数据库连接
db, ok := authBaseController.GetDB(c)
if !ok {
return
}
// 获取系统设置服务
settingsService := services.GetSettingsService()
adminUsername := settingsService.GetString("admin_username", "admin")
adminPasswordHash := settingsService.GetString("admin_password", "")
adminPasswordSalt := settingsService.GetString("admin_password_salt", "")
// 通过前缀匹配一次性获取所有管理员相关设置
var adminSettings []models.Settings
if err := db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; err != nil {
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
return
}
// 将设置转换为map便于查找
settingsMap := make(map[string]string)
for _, setting := range adminSettings {
settingsMap[setting.Name] = setting.Value
}
// 检查必要的设置是否存在
adminUsername, hasUsername := settingsMap["admin_username"]
adminPassword, hasPassword := settingsMap["admin_password"]
adminPasswordSalt, hasSalt := settingsMap["admin_password_salt"]
if !hasUsername || !hasPassword || !hasSalt {
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
// 验证密码为空的情况(首次登录需要初始化)
if adminPasswordHash == "" || adminPasswordSalt == "" {
recordLoginLog(c, body.Username, 0, "管理员账号未初始化")
authBaseController.HandleInternalError(c, "管理员账号未初始化,请联系系统管理员", nil)
return
}
// 验证用户名
if body.Username != adminUsername {
recordLoginLog(c, body.Username, 0, "用户名错误")
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
return
}
// 验证密码为空的情况(首次登录需要初始化
if adminPassword == "" || adminPasswordSalt == "" {
authBaseController.HandleInternalError(c, "管理员账号未初始化,请联系系统管理员", nil)
return
}
// 使用盐值验证密码
if !utils.VerifyPasswordWithSalt(body.Password, adminPasswordSalt, adminPassword) {
// 验证密码(使用盐值校验
if !utils.VerifyPasswordWithSalt(body.Password, adminPasswordSalt, adminPasswordHash) {
recordLoginLog(c, body.Username, 0, "密码错误")
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
return
}
// 创建虚拟用户对象用于生成JWT令牌
adminUser := models.User{
Username: adminUsername,
Password: adminPassword,
PasswordSalt: adminPasswordSalt,
}
// 生成JWT令牌
token, err := generateJWTTokenForAdmin(adminUser)
token, err := generateJWTTokenForAdmin(body.Username, adminPasswordHash)
if err != nil {
recordLoginLog(c, body.Username, 0, "生成令牌失败")
authBaseController.HandleInternalError(c, "生成令牌失败", err)
return
}
// 设置JWT Cookie使用安全配置
cookie := utils.CreateSecureCookie("admin_session", token, utils.GetDefaultCookieMaxAge())
// 设置JWT CookieHttpOnly安全
// 使用系统配置的Cookie参数
secure, sameSite, domain, maxAge := settingsService.GetCookieConfig()
cookie := utils.CreateSecureCookie("admin_session", token, maxAge, domain, secure, sameSite)
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
recordLoginLog(c, body.Username, 1, "登录成功")
authBaseController.HandleSuccess(c, "登录成功", gin.H{
"redirect": "/admin",
})
}
// recordLoginLog 记录登录日志
// status: 1-成功, 0-失败
func recordLoginLog(c *gin.Context, username string, status int, message string) {
db, err := database.GetDB()
if err != nil {
// 记录日志失败不应影响主流程,但可以记录到系统日志
fmt.Printf("Failed to connect to database for login log: %v\n", err)
return
}
log := models.LoginLog{
Type: "admin",
Username: username,
IP: c.ClientIP(),
Status: status,
Message: message,
UserAgent: c.Request.UserAgent(),
CreatedAt: time.Now(),
}
if err := db.Create(&log).Error; err != nil {
fmt.Printf("Failed to create login log: %v\n", err)
}
}
// LogoutHandler 管理员登出
// - 清理JWT Cookie
// - 清理JWT Cookie会话
// - 确保令牌完全失效
func LogoutHandler(c *gin.Context) {
// 清理JWT Cookie
clearInvalidJWTCookie(c)
// 可选将JWT令牌加入黑名单需要Redis或数据库支持
// 这里可以实现JWT黑名单机制
authBaseController.HandleSuccess(c, "已退出登录", gin.H{
"redirect": "/admin/login",
})
}
// ============================================================================
// CSRF 相关辅助函数
// ============================================================================
const (
CSRFCookieName = "csrf_token"
CSRFHeaderName = "X-CSRF-Token"
CSRFFormField = "csrf_token"
)
// setCSRFToken 设置CSRF令牌到Cookie (Gin适配)
func setCSRFToken(c *gin.Context, token string) {
c.SetCookie(CSRFCookieName, token, 3600*24, "/", "", false, false)
c.Header(CSRFHeaderName, token)
}
// validateCSRFToken 验证CSRF令牌 (Gin适配)
func validateCSRFToken(c *gin.Context, requestToken string) bool {
// 获取Cookie中的令牌
cookie, err := c.Cookie(CSRFCookieName)
if err != nil || cookie == "" {
return false
}
cookieToken := cookie
// 如果请求体中没有提供token尝试从Header获取
if requestToken == "" {
requestToken = c.GetHeader(CSRFHeaderName)
}
if requestToken == "" {
return false
}
// 使用常量时间比较
return strings.Compare(cookieToken, requestToken) == 0
}
// ============================================================================
// 辅助函数
// ============================================================================
@@ -198,14 +238,27 @@ func LogoutHandler(c *gin.Context) {
// - 统一的Cookie清理函数确保一致性
// - 在JWT校验失败时自动调用提升安全性和用户体验
func clearInvalidJWTCookie(c *gin.Context) {
cookie := utils.CreateExpiredCookie("admin_session")
_, _, domain, _ := services.GetSettingsService().GetCookieConfig()
cookie := utils.CreateExpiredCookie("admin_session", domain)
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
}
// getJWTSecret 动态获取当前的JWT密钥
// 修复安全漏洞:确保每次都从最新配置中获取密钥,而不是使用启动时的全局变量
func getJWTSecret() []byte {
return []byte(viper.GetString("security.jwt_secret"))
// 1. 尝试从数据库设置获取
settingsService := services.GetSettingsService()
if secret := settingsService.GetJWTSecret(); secret != "" {
return []byte(secret)
}
// 2. 尝试从配置文件获取(兼容旧配置)
if secret := viper.GetString("security.jwt_secret"); secret != "" {
return []byte(secret)
}
// 3. 使用默认不安全密钥(仅开发环境)
return []byte("default-insecure-jwt-secret")
}
// ============================================================================
@@ -215,27 +268,40 @@ func getJWTSecret() []byte {
// JWTClaims JWT载荷结构体
type JWTClaims struct {
Username string `json:"username"`
UUID string `json:"uuid"` // 添加虚拟角色UUID
Role int `json:"role"` // 添加虚拟角色
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改
jwt.RegisteredClaims
}
// generateJWTTokenForAdmin 生成管理员JWT令牌
// - 包含管理员UUID、用户名信息
// - 设置24小时过期时间
// - 包含管理员用户名信息和密码哈希
// - 设置过期时间
// - 使用HMAC-SHA256签名
func generateJWTTokenForAdmin(adminUser models.User) (string, error) {
func generateJWTTokenForAdmin(username, passwordHash string) (string, error) {
// 生成密码哈希摘要使用SHA256
passwordHashDigest := utils.GenerateSHA256Hash(adminUser.Password)
// 注意:传入的 passwordHash 已经是数据库存的 Hash这里我们再次 Hash 还是直接用?
// atomicLibrary 的实现是: utils.GenerateSHA256Hash(adminUser.Password)
// 这里我们直接用数据库里的 Hash 值作为 Token 的一部分即可,或者对它再 Hash 一次。
// 为了与 validateAdminPasswordHash 对应,我们需要知道验证时怎么比对。
// validateAdminPasswordHash: currentPasswordHash := utils.GenerateSHA256Hash(adminPassword.Value)
// 所以这里也应该对数据库里的值进行 Hash。
passwordHashDigest := utils.GenerateSHA256Hash(passwordHash)
// 获取虚拟管理员UUID (NetworkAuth 项目默认为 admin-uuid-001)
adminUUID := services.GetSettingsService().GetString("admin_uuid", "admin-uuid-001")
claims := JWTClaims{
Username: adminUser.Username,
Username: username,
UUID: adminUUID,
Role: 0, // 0表示超级管理员
PasswordHash: passwordHashDigest, // 包含密码哈希摘要
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(services.GetSettingsService().GetJWTExpire()) * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "凌动技术",
Subject: adminUser.Username,
Issuer: "NetworkAuth",
Subject: username,
},
}
@@ -303,7 +369,7 @@ func validateAdminPasswordHash(claims *JWTClaims, c *gin.Context) bool {
return true
}
// IsAdminAuthenticated 判断管理员是否已认证(导出
// IsAdminAuthenticated 判断管理员是否已认证(Gin版本
// - 检查admin_session Cookie中的JWT令牌
// - 验证令牌签名、过期时间和用户角色
func IsAdminAuthenticated(c *gin.Context) bool {
@@ -318,12 +384,48 @@ func IsAdminAuthenticated(c *gin.Context) bool {
return false
}
// 注释:由于这是管理员专用认证函数,不需要额外的角色验证
// 验证密码哈希
return validateAdminPasswordHash(claims, c)
}
// IsAdminAuthenticatedHttp 判断管理员是否已认证HTTP兼容版本
// 保留此方法以兼容未迁移的 Handler
func IsAdminAuthenticatedHttp(r *http.Request) bool {
cookie, err := r.Cookie("admin_session")
if err != nil || cookie.Value == "" {
return false
}
// 解析并验证JWT令牌
claims, err := parseJWTToken(cookie.Value)
if err != nil {
return false
}
// 注意HTTP 版本无法方便地获取 ClientIP 用于日志,且无法使用 Gin Context 的功能
// 这里仅做基本的 Token 验证。如果 Token 包含了 PasswordHash这里也会解析出来。
// 但验证 PasswordHash 需要 DB 访问。
// 为了完整性,我们应该也验证 PasswordHash。
// 这里的 ClientIP 只能从 r.RemoteAddr 获取。
db, err := database.GetDB()
if err != nil {
return false
}
var adminPassword models.Settings
if err := db.Where("name = ?", "admin_password").First(&adminPassword).Error; err != nil {
return false
}
currentPasswordHash := utils.GenerateSHA256Hash(adminPassword.Value)
if claims.PasswordHash != currentPasswordHash {
return false
}
return true
}
// IsAdminAuthenticatedWithCleanup 带自动清理功能的JWT校验函数
// - 当JWT校验失败时自动清理失效的Cookie
// - 适用于API接口等需要清理失效令牌的场景
@@ -341,8 +443,6 @@ func IsAdminAuthenticatedWithCleanup(c *gin.Context) bool {
return false
}
// 注释:由于这是管理员专用认证函数,不需要额外的角色验证
// 验证密码哈希
if !validateAdminPasswordHash(claims, c) {
clearInvalidJWTCookie(c)
@@ -352,23 +452,18 @@ func IsAdminAuthenticatedWithCleanup(c *gin.Context) bool {
return true
}
// GetCurrentAdminUser 获取当前登录的管理员用户信息
// - 从JWT令牌中提取用户信息
// - 自动刷新接近过期的令牌剩余时间少于6小时时刷新
// - 返回用户ID、用户名和角色
func GetCurrentAdminUser(c *gin.Context) (*JWTClaims, error) {
cookie, err := getJWTCookie(c)
// GetCurrentAdminUser 获取当前登录的管理员用户信息 (HTTP 兼容版)
func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
cookie, err := r.Cookie("admin_session")
if err != nil {
return nil, fmt.Errorf("未找到会话信息")
}
claims, err := parseJWTToken(cookie)
claims, err := parseJWTToken(cookie.Value)
if err != nil {
return nil, fmt.Errorf("无效的会话信息")
}
// 注释:由于这是管理员专用函数,不需要额外的角色验证
return claims, nil
}
@@ -394,25 +489,56 @@ func GetCurrentAdminUserWithRefresh(c *gin.Context) (*JWTClaims, bool, error) {
// 检查是否需要刷新令牌
refreshed := false
refreshThreshold := time.Duration(viper.GetInt("security.jwt_refresh")) * time.Hour
// 动态获取刷新阈值默认剩余时间少于6小时刷新
refreshThresholdHours := services.GetSettingsService().GetJWTRefresh()
if refreshThresholdHours <= 0 {
refreshThresholdHours = 6 // 默认值
}
refreshThreshold := time.Duration(refreshThresholdHours) * time.Hour
// 动态获取JWT总有效期
expireHours := services.GetSettingsService().GetJWTExpire()
if expireHours <= 0 {
expireHours = 24 // 默认值
}
// 动态获取Cookie配置用于更新Cookie过期时间
secure, sameSite, domain, maxAge := services.GetSettingsService().GetCookieConfig()
// 1. 默认情况下每次请求都更新Cookie的过期时间滑动过期
tokenToSet := cookie
shouldUpdateCookie := true
// 2. 检查是否需要刷新JWT令牌生成新的Token
if time.Until(claims.ExpiresAt.Time) < refreshThreshold {
adminUser := models.User{
Username: claims.Username,
}
newToken, err := generateJWTTokenForAdmin(adminUser)
// 获取当前的 PasswordHash
db, _ := database.GetDB()
var adminPassword models.Settings
db.Where("name = ?", "admin_password").First(&adminPassword)
// 使用新的有效期生成令牌
newToken, err := generateJWTTokenForAdmin(claims.Username, adminPassword.Value)
if err == nil {
c.SetCookie("admin_session", newToken, utils.GetDefaultCookieMaxAge(), "/", "", false, true)
tokenToSet = newToken
refreshed = true
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(24 * time.Hour))
// 更新当前claims的过期时间
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Duration(expireHours) * time.Hour))
claims.IssuedAt = jwt.NewNumericDate(time.Now())
}
}
// 3. 执行Cookie更新
if shouldUpdateCookie {
cookieObj := utils.CreateSecureCookie("admin_session", tokenToSet, maxAge, domain, secure, sameSite)
c.SetCookie(cookieObj.Name, cookieObj.Value, cookieObj.MaxAge, cookieObj.Path, cookieObj.Domain, cookieObj.Secure, cookieObj.HttpOnly)
}
return claims, refreshed, nil
}
// AdminAuthRequired 管理员认证拦截中间件
// AdminAuthRequired 管理员认证拦截中间件 (Gin Middleware)
// - 未登录:重定向到 /admin/login
// - 已登录:自动刷新接近过期的令牌,然后放行到后续处理器
func AdminAuthRequired() gin.HandlerFunc {
@@ -424,8 +550,6 @@ func AdminAuthRequired() gin.HandlerFunc {
clearInvalidJWTCookie(c)
// 中文注释区分普通页面请求与AJAX/JSON请求
// - 对 AJAX/JSON直接返回 401 JSON便于前端处理如提示重新登录
// - 对普通页面:保持原有重定向到登录页
accept := c.GetHeader("Accept")
xrw := strings.ToLower(strings.TrimSpace(c.GetHeader("X-Requested-With")))
if strings.Contains(accept, "application/json") || xrw == "xmlhttprequest" {
@@ -448,6 +572,11 @@ func AdminAuthRequired() gin.HandlerFunc {
_ = claims // 避免未使用变量警告
}
// 将解析出的用户信息存入上下文,供后续处理使用
c.Set("admin_uuid", claims.UUID)
c.Set("admin_username", claims.Username)
c.Set("admin_role", claims.Role)
c.Next()
}
}

View File

@@ -7,11 +7,9 @@ import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"networkDev/controllers"
"networkDev/middleware"
"networkDev/utils"
"NetworkAuth/middleware"
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
)
@@ -19,9 +17,6 @@ import (
// 全局变量
// ============================================================================
// 创建基础控制器实例
var captchaBaseController = controllers.NewBaseController()
// 全局验证码存储器
var store = base64Captcha.DefaultMemStore
@@ -49,7 +44,7 @@ func CaptchaHandler(c *gin.Context) {
// 使用crypto/rand生成安全的随机数
randomNum, err := secureRandomInt(3)
if err != nil {
captchaBaseController.HandleInternalError(c, "生成随机数失败", err)
c.String(http.StatusInternalServerError, "生成随机数失败")
return
}
captchaLength := 4 + randomNum // 4-6位随机长度
@@ -62,37 +57,31 @@ func CaptchaHandler(c *gin.Context) {
ShowLineOptions: 2 | 4,
Length: captchaLength,
Source: "ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789", // 混合大小写字母和数字,去除易混淆字符
Fonts: []string{"wqy-microhei.ttc"},
}
// 生成验证码
captcha := base64Captcha.NewCaptcha(&driver, store)
id, b64s, _, err := captcha.Generate()
if err != nil {
captchaBaseController.HandleInternalError(c, "生成验证码失败", err)
c.String(http.StatusInternalServerError, "生成验证码失败")
return
}
// 将验证码ID存储到session中这里简化处理实际项目中应该使用更安全的方式
// 设置cookie来存储验证码ID
cookie := utils.CreateSecureCookie("captcha_id", id, 300) // 5分钟过期
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
// 将验证码ID存储到Cookie中
c.SetCookie("captcha_id", id, 300, "/", "", false, true)
// 解码base64图片数据并返回
// 设置响应头
c.Header("Content-Type", "image/png")
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
c.Header("Pragma", "no-cache")
c.Header("Expires", "0")
// 直接返回base64编码的图片数据让浏览器解析
// 但是我们需要返回实际的图片数据所以需要解码base64
// 去掉data:image/png;base64,前缀
b64s = strings.TrimPrefix(b64s, "data:image/png;base64,")
imgData, err := base64.StdEncoding.DecodeString(b64s)
if err != nil {
captchaBaseController.HandleInternalError(c, "解码验证码图片失败", err)
c.String(http.StatusInternalServerError, "解码验证码图片失败")
return
}
@@ -110,11 +99,7 @@ func VerifyCaptcha(c *gin.Context, captchaValue string) bool {
// 从cookie中获取验证码ID
captchaId, err := c.Cookie("captcha_id")
if err != nil {
return false
}
if captchaId == "" {
if err != nil || captchaId == "" {
return false
}
@@ -125,7 +110,7 @@ func VerifyCaptcha(c *gin.Context, captchaValue string) bool {
return true
}
// 如果原始值验证失败,尝试小写验证(因为显示的是大小写混合,但允许用户输入小写)
// 如果原始值验证失败,尝试小写验证
if store.Verify(captchaId, strings.ToLower(captchaValue), false) {
// 验证成功后删除验证码
store.Verify(captchaId, strings.ToLower(captchaValue), true)
@@ -139,22 +124,3 @@ func VerifyCaptcha(c *gin.Context, captchaValue string) bool {
return false
}
// CaptchaAPIHandler 验证码API接口可选用于AJAX验证
// POST /admin/api/captcha/verify - 验证验证码
func CaptchaAPIHandler(c *gin.Context) {
var body struct {
Captcha string `json:"captcha"`
}
if !captchaBaseController.BindJSON(c, &body) {
return
}
isValid := VerifyCaptcha(c, body.Captcha)
if isValid {
captchaBaseController.HandleSuccess(c, "验证码正确", nil)
} else {
captchaBaseController.HandleValidationError(c, "验证码错误")
}
}

View File

@@ -1,9 +1,9 @@
package admin
import (
"NetworkAuth/controllers"
"NetworkAuth/models"
"net/http"
"networkDev/controllers"
"networkDev/models"
"regexp"
"strconv"
"strings"
@@ -299,7 +299,7 @@ func FunctionDeleteHandler(c *gin.Context) {
return
}
logrus.WithField("function_id", req.ID).Info("Successfully deleted function")
logrus.WithField("function_id", req.ID).Debug("Successfully deleted function")
functionBaseController.HandleSuccess(c, "删除成功", nil)
}
@@ -331,7 +331,7 @@ func FunctionsBatchDeleteHandler(c *gin.Context) {
return
}
logrus.WithField("function_ids", req.IDs).Info("Successfully batch deleted functions")
logrus.WithField("function_ids", req.IDs).Debug("Successfully batch deleted functions")
functionBaseController.HandleSuccess(c, "批量删除成功", nil)
}

View File

@@ -1,14 +1,15 @@
package admin
import (
"NetworkAuth/constants"
"NetworkAuth/controllers"
"NetworkAuth/middleware"
"NetworkAuth/models"
"NetworkAuth/services"
"NetworkAuth/utils"
"NetworkAuth/utils/timeutil"
"net/http"
"networkDev/constants"
"networkDev/controllers"
"networkDev/middleware"
"networkDev/models"
"networkDev/services"
"networkDev/utils"
"networkDev/utils/timeutil"
"strconv"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
@@ -83,11 +84,8 @@ func AdminLayoutHandler(c *gin.Context) {
data["CSRFToken"] = token
// 从数据库读取站点标题,如果失败则使用默认值
if db, ok := handlersBaseController.GetDB(c); ok {
if siteTitle, err := services.FindSettingByName("site_title", db); err == nil && siteTitle != nil {
data["Title"] = siteTitle.Value
}
}
settingsSvc := services.GetSettingsService()
data["Title"] = settingsSvc.GetString("site_title", "后台管理")
// 合并其他数据(如果有的话)
extraData := gin.H{}
@@ -192,3 +190,45 @@ func DashboardStatsHandler(c *gin.Context) {
handlersBaseController.HandleSuccess(c, "ok", data)
}
// DashboardLoginLogsHandler 获取管理员最近登录日志
func DashboardLoginLogsHandler(c *gin.Context) {
db, ok := handlersBaseController.GetDB(c)
if !ok {
return
}
// 获取分页参数
pageStr := c.DefaultQuery("page", "1")
limitStr := c.DefaultQuery("limit", "10")
page, _ := strconv.Atoi(pageStr)
limit, _ := strconv.Atoi(limitStr)
if page < 1 {
page = 1
}
if limit < 1 || limit > 100 {
limit = 10
}
offset := (page - 1) * limit
var total int64
// 当前模型的 LoginLog 本身就是专用于 admin 的登录日志模型(没有 type 字段),所以直接查询全部即可
query := db.Model(&models.LoginLog{})
if err := query.Count(&total).Error; err != nil {
handlersBaseController.HandleInternalError(c, "获取登录日志总数失败", err)
return
}
var logs []models.LoginLog
if err := query.Order("created_at desc").Offset(offset).Limit(limit).Find(&logs).Error; err != nil {
handlersBaseController.HandleInternalError(c, "获取登录日志列表失败", err)
return
}
data := gin.H{
"total": total,
"list": logs,
}
handlersBaseController.HandleSuccess(c, "获取登录日志成功", data)
}

View File

@@ -0,0 +1,174 @@
package admin
import (
"NetworkAuth/controllers"
"NetworkAuth/models"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// ============================================================================
// 全局变量
// ============================================================================
var loginLogBaseController = controllers.NewBaseController()
// ============================================================================
// 辅助函数
// ============================================================================
// RecordLoginLog 记录登录日志
func RecordLoginLog(c *gin.Context, username string, status int, message string) {
db, ok := loginLogBaseController.GetDB(c)
if !ok {
return
}
log := models.LoginLog{
Type: "admin",
Username: username,
IP: c.ClientIP(),
Status: status,
Message: message,
UserAgent: c.Request.UserAgent(),
CreatedAt: time.Now(),
}
if err := db.Create(&log).Error; err != nil {
logrus.WithError(err).Error("Failed to create login log")
}
}
// LoginLogsFragmentHandler 登录日志页面片段处理器
func LoginLogsFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "login_logs.html", gin.H{
"Title": "登录日志",
})
}
// ============================================================================
// API处理器
// ============================================================================
// LoginLogsListHandler 登录日志列表API处理器
func LoginLogsListHandler(c *gin.Context) {
// 获取分页参数
page, _ := strconv.Atoi(c.Query("page"))
if page <= 0 {
page = 1
}
limit, _ := strconv.Atoi(c.Query("limit"))
if limit <= 0 {
limit = 10
}
// 构建查询
db, ok := loginLogBaseController.GetDB(c)
if !ok {
return
}
var logs []models.LoginLog
var total int64
// 兼容旧数据Type为空和新数据Type=admin
query := db.Model(&models.LoginLog{}).Where("type = ? OR type = ? OR type IS NULL", "admin", "")
// 筛选条件:用户名
if username := strings.TrimSpace(c.Query("username")); username != "" {
query = query.Where("username = ?", username)
}
// 筛选条件IP
if ip := strings.TrimSpace(c.Query("ip")); ip != "" {
query = query.Where("ip = ?", ip)
}
// 筛选条件:状态
if statusStr := strings.TrimSpace(c.Query("status")); statusStr != "" {
if status, err := strconv.Atoi(statusStr); err == nil {
query = query.Where("status = ?", status)
}
}
// 筛选条件:时间范围
startTime := strings.TrimSpace(c.Query("start_time"))
endTime := strings.TrimSpace(c.Query("end_time"))
if startTime != "" && endTime != "" {
query = query.Where("created_at BETWEEN ? AND ?", startTime, endTime)
}
// 统计总数
if err := query.Count(&total).Error; err != nil {
loginLogBaseController.HandleInternalError(c, "获取日志总数失败", err)
return
}
// 查询数据(时间倒序,从新到旧)
offset := (page - 1) * limit
if err := query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&logs).Error; err != nil {
loginLogBaseController.HandleInternalError(c, "获取日志列表失败", err)
return
}
// 转换数据格式
var list []map[string]interface{}
for _, log := range logs {
list = append(list, map[string]interface{}{
"id": log.ID,
"username": log.Username,
"ip": log.IP,
"status": log.Status,
"message": log.Message,
"user_agent": log.UserAgent,
"created_at": log.CreatedAt,
})
}
loginLogBaseController.HandleSuccess(c, "ok", gin.H{
"list": list,
"total": total,
})
}
// LoginLogsClearHandler 清空登录日志API处理器
func LoginLogsClearHandler(c *gin.Context) {
db, ok := loginLogBaseController.GetDB(c)
if !ok {
return
}
// 物理删除所有登录日志
if err := db.Where("type = ?", "admin").Delete(&models.LoginLog{}).Error; err != nil {
logrus.WithError(err).Error("Failed to clear login logs")
loginLogBaseController.HandleInternalError(c, "清空登录日志失败", err)
return
}
// 记录操作日志
// 由于 NetworkAuth 中没有 SystemAdminUser 全局变量,这里暂时使用 "admin"
operator := "admin"
// 尝试从上下文获取用户名(如果中间件设置了的话)
// if user, exists := c.Get("username"); exists {
// operator = user.(string)
// }
log := models.OperationLog{
OperationType: "清空登录日志",
Operator: operator,
OperatorUUID: "", // NetworkAuth 中暂时无法获取 UUID
AppName: "-",
ProductName: "-",
TransactionID: "-",
Details: "管理员清空了所有登录日志",
CreatedAt: time.Now(),
}
db.Create(&log)
loginLogBaseController.HandleSuccess(c, "登录日志已清空", nil)
}

View File

@@ -0,0 +1,152 @@
package admin
import (
"NetworkAuth/controllers"
"NetworkAuth/models"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
// ============================================================================
// 全局变量
// ============================================================================
var logBaseController = controllers.NewBaseController()
// ============================================================================
// 页面处理器
// ============================================================================
// LogsFragmentHandler 日志操作页面片段处理器
func LogsFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "operation_logs.html", gin.H{
"Title": "操作日志",
})
}
// ============================================================================
// API处理器
// ============================================================================
// LogsListHandler 日志列表API处理器
func LogsListHandler(c *gin.Context) {
// 获取分页参数
page, _ := strconv.Atoi(c.Query("page"))
if page <= 0 {
page = 1
}
limit, _ := strconv.Atoi(c.Query("limit"))
if limit <= 0 {
limit = 10
}
// 获取搜索参数
startTimeStr := strings.TrimSpace(c.Query("start_time"))
endTimeStr := strings.TrimSpace(c.Query("end_time"))
operationType := strings.TrimSpace(c.Query("operation_type"))
operator := strings.TrimSpace(c.Query("operator"))
transactionID := strings.TrimSpace(c.Query("transaction_id"))
// 构建查询
db, ok := logBaseController.GetDB(c)
if !ok {
return
}
var logs []models.OperationLog
var total int64
query := db.Model(&models.OperationLog{})
// 筛选条件
if operationType != "" {
query = query.Where("operation_type = ?", operationType)
}
if operator != "" {
// 支持按 UUID 或 用户名 筛选
query = query.Where("operator_uuid = ? OR operator = ?", operator, operator)
}
if transactionID != "" {
// 优化:使用精确匹配提升查询性能
query = query.Where("transaction_id = ?", transactionID)
}
if startTimeStr != "" {
if t, err := time.ParseInLocation("2006-01-02", startTimeStr, time.Local); err == nil {
query = query.Where("created_at >= ?", t)
} else if t, err := time.ParseInLocation("2006-01-02 15:04:05", startTimeStr, time.Local); err == nil {
query = query.Where("created_at >= ?", t)
} else {
query = query.Where("created_at >= ?", startTimeStr)
}
}
if endTimeStr != "" {
if t, err := time.ParseInLocation("2006-01-02", endTimeStr, time.Local); err == nil {
t = t.Add(24*time.Hour - time.Nanosecond)
query = query.Where("created_at <= ?", t)
} else if t, err := time.ParseInLocation("2006-01-02 15:04:05", endTimeStr, time.Local); err == nil {
query = query.Where("created_at <= ?", t)
} else {
if len(endTimeStr) == 10 { // yyyy-MM-dd
endTimeStr += " 23:59:59"
}
query = query.Where("created_at <= ?", endTimeStr)
}
}
// 获取总数
if err := query.Count(&total).Error; err != nil {
logrus.WithError(err).Error("获取日志总数失败")
logBaseController.HandleInternalError(c, "获取日志总数失败", err)
return
}
// 分页查询(时间倒序,从新到旧)
offset := (page - 1) * limit
if err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&logs).Error; err != nil {
logrus.WithError(err).Error("查询日志列表失败")
logBaseController.HandleInternalError(c, "查询日志列表失败", err)
return
}
logBaseController.HandleSuccess(c, "获取日志列表成功", gin.H{
"list": logs,
"total": total,
})
}
// LogsClearHandler 清空日志API处理器
func LogsClearHandler(c *gin.Context) {
db, ok := logBaseController.GetDB(c)
if !ok {
return
}
// 开启事务进行清空
if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Delete(&models.OperationLog{}).Error; err != nil {
logrus.WithError(err).Error("清空操作日志失败")
logBaseController.HandleInternalError(c, "清空操作日志失败", err)
return
}
// 记录操作日志 (因为刚刚清空了,这条将是第一条)
operator := "admin"
log := models.OperationLog{
OperationType: "清空日志",
Operator: operator,
OperatorUUID: "",
AppName: "-",
ProductName: "-",
TransactionID: "-",
Details: "管理员清空了所有操作日志",
CreatedAt: time.Now(),
}
db.Create(&log)
logBaseController.HandleSuccess(c, "日志已清空", nil)
}

View File

@@ -0,0 +1,262 @@
package admin
import (
"NetworkAuth/database"
"NetworkAuth/models"
"NetworkAuth/services"
"NetworkAuth/utils"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// ProfileFragmentHandler 个人资料片段渲染
// - 渲染个人资料与修改密码表单
func ProfileFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "profile.html", map[string]interface{}{})
}
// ProfileInfoHandler 查询当前登录管理员的基本信息
// - 返回 username 字段
func ProfileInfoHandler(c *gin.Context) {
_, _, err := GetCurrentAdminUserWithRefresh(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 1,
"msg": "未登录或会话已过期",
"data": nil,
})
return
}
// 获取最新设置
settingsService := services.GetSettingsService()
username := settingsService.GetString("admin_username", "admin")
authBaseController.HandleSuccess(c, "ok", map[string]interface{}{
"username": username,
})
}
// ProfilePasswordUpdateHandler 修改当前登录管理员的密码
// - 接收 JSON: {old_password, new_password, confirm_password}
// - 校验旧密码正确性、新密码与确认一致性
// - 成功后更新密码哈希
func ProfilePasswordUpdateHandler(c *gin.Context) {
_, _, err := GetCurrentAdminUserWithRefresh(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 1,
"msg": "未登录或会话已过期",
"data": nil,
})
return
}
var body struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
ConfirmPassword string `json:"confirm_password"`
}
if !authBaseController.BindJSON(c, &body) {
return
}
// 基础校验
if body.OldPassword == "" || body.NewPassword == "" || body.ConfirmPassword == "" {
authBaseController.HandleValidationError(c, "旧密码/新密码/确认密码均不能为空")
return
}
if len(body.NewPassword) < 6 {
authBaseController.HandleValidationError(c, "新密码长度不能少于6位")
return
}
if body.NewPassword != body.ConfirmPassword {
authBaseController.HandleValidationError(c, "两次输入的新密码不一致")
return
}
if body.NewPassword == body.OldPassword {
authBaseController.HandleValidationError(c, "新密码不能与旧密码相同")
return
}
// 获取当前密码设置
settingsService := services.GetSettingsService()
currentHash := settingsService.GetString("admin_password", "")
currentSalt := settingsService.GetString("admin_password_salt", "")
// 校验旧密码
if !utils.VerifyPasswordWithSalt(body.OldPassword, currentSalt, currentHash) {
authBaseController.HandleValidationError(c, "旧密码不正确")
return
}
// 生成新盐值和哈希
newSalt, err := utils.GenerateRandomSalt()
if err != nil {
authBaseController.HandleInternalError(c, "生成盐值失败", err)
return
}
newHash, err := utils.HashPasswordWithSalt(body.NewPassword, newSalt)
if err != nil {
authBaseController.HandleInternalError(c, "生成密码哈希失败", err)
return
}
// 更新数据库
db, ok := authBaseController.GetDB(c)
if !ok {
return
}
// 更新 admin_password
if err := updateSetting(db, "admin_password", newHash); err != nil {
authBaseController.HandleInternalError(c, "更新密码失败", err)
return
}
// 更新 admin_password_salt
if err := updateSetting(db, "admin_password_salt", newSalt); err != nil {
authBaseController.HandleInternalError(c, "更新盐值失败", err)
return
}
// 刷新缓存
settingsService.RefreshCache()
// 清除相关缓存键
_ = utils.RedisDel(c.Request.Context(), "setting:admin_password", "setting:admin_password_salt")
// 获取当前用户名
currentUsername := settingsService.GetString("admin_username", "admin")
// 重新签发JWT并写入Cookie
token, err := generateJWTTokenForAdmin(currentUsername, newHash)
if err != nil {
authBaseController.HandleInternalError(c, "生成新令牌失败", err)
return
}
secure, sameSite, domain, maxAge := settingsService.GetCookieConfig()
cookie := utils.CreateSecureCookie("admin_session", token, maxAge, domain, secure, sameSite)
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
authBaseController.HandleSuccess(c, "密码修改成功", nil)
}
// ProfileUpdateHandler 修改当前登录管理员的用户名
// - 接收 JSON: {username}
// - 校验用户名非空、长度
// - 更新数据库后重新签发JWT并写入 Cookie保持前端展示的一致性
func ProfileUpdateHandler(c *gin.Context) {
_, _, err := GetCurrentAdminUserWithRefresh(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 1,
"msg": "未登录或会话已过期",
"data": nil,
})
return
}
var body struct {
Username string `json:"username"`
OldPassword string `json:"old_password"`
}
if !authBaseController.BindJSON(c, &body) {
return
}
username := strings.TrimSpace(body.Username)
if username == "" {
authBaseController.HandleValidationError(c, "用户名不能为空")
return
}
if len(username) > 64 {
authBaseController.HandleValidationError(c, "用户名长度不能超过64字符")
return
}
settingsService := services.GetSettingsService()
currentUsername := settingsService.GetString("admin_username", "admin")
// 如果未变化则直接返回成功
if strings.EqualFold(username, currentUsername) {
authBaseController.HandleSuccess(c, "保存成功", map[string]interface{}{
"username": username,
})
return
}
// 修改用户名需要进行当前密码校验
if strings.TrimSpace(body.OldPassword) == "" {
authBaseController.HandleValidationError(c, "修改用户名需要提供当前密码")
return
}
currentHash := settingsService.GetString("admin_password", "")
currentSalt := settingsService.GetString("admin_password_salt", "")
// 校验旧密码
if !utils.VerifyPasswordWithSalt(body.OldPassword, currentSalt, currentHash) {
authBaseController.HandleValidationError(c, "当前密码不正确")
return
}
// 更新数据库
db, ok := authBaseController.GetDB(c)
if !ok {
return
}
if err := updateSetting(db, "admin_username", username); err != nil {
authBaseController.HandleInternalError(c, "更新用户名失败", err)
return
}
// 重新签发JWT并写入Cookie
token, err := generateJWTTokenForAdmin(username, currentHash)
if err != nil {
authBaseController.HandleInternalError(c, "生成新令牌失败", err)
return
}
secure, sameSite, domain, maxAge := settingsService.GetCookieConfig()
cookie := utils.CreateSecureCookie("admin_session", token, maxAge, domain, secure, sameSite)
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
// 刷新缓存
settingsService.RefreshCache()
_ = utils.RedisDel(c.Request.Context(), "setting:admin_username")
authBaseController.HandleSuccess(c, "用户名修改成功", map[string]interface{}{
"username": username,
})
}
// 辅助函数:更新设置项
func updateSetting(db interface{}, name, value string) error {
// 类型断言
gormDB, ok := db.(*gorm.DB)
if !ok {
// 如果断言失败,尝试重新获取连接
var err error
gormDB, err = database.GetDB()
if err != nil {
return err
}
}
var setting models.Settings
if err := gormDB.Where("name = ?", name).First(&setting).Error; err != nil {
// 如果不存在则创建
setting = models.Settings{Name: name, Value: value}
return gormDB.Create(&setting).Error
}
// 存在则更新
return gormDB.Model(&setting).Update("value", value).Error
}

View File

@@ -1,55 +1,52 @@
package admin
import (
"context"
"NetworkAuth/config"
"NetworkAuth/models"
"NetworkAuth/services"
"NetworkAuth/utils"
"fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"net/http"
"networkDev/controllers"
"networkDev/models"
"networkDev/services"
"networkDev/utils"
)
// ============================================================================
// 全局变量
// ============================================================================
// 创建基础控制器实例
var settingsBaseController = controllers.NewBaseController()
// ============================================================================
// 页面处理器
// ============================================================================
// SettingsFragmentHandler 设置片段渲染
// - 渲染设置表单通过前端JS调用API加载/保存)
func SettingsFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "settings.html", gin.H{})
c.HTML(http.StatusOK, "settings.html", map[string]interface{}{})
}
// ============================================================================
// API处理器
// ============================================================================
// SubAccountSimpleListHandler 子账号简单列表API处理器 (Mock)
func SubAccountSimpleListHandler(c *gin.Context) {
// Mock implementation for NetworkAuth which has no subaccounts
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "success",
"data": []interface{}{},
})
}
// SettingsQueryHandler 设置查询API
// - 返回所有设置项的 name:value 映射
func SettingsQueryHandler(c *gin.Context) {
db, ok := settingsBaseController.GetDB(c)
db, ok := authBaseController.GetDB(c)
if !ok {
return
}
var list []models.Settings
if err := db.Find(&list).Error; err != nil {
settingsBaseController.HandleInternalError(c, "查询失败", err)
authBaseController.HandleInternalError(c, "查询失败", err)
return
}
res := map[string]string{}
for _, s := range list {
res[s.Name] = s.Value
}
settingsBaseController.HandleSuccess(c, "ok", res)
authBaseController.HandleSuccess(c, "ok", res)
}
// SettingsUpdateHandler 更新系统设置处理器
@@ -65,7 +62,8 @@ func SettingsQueryHandler(c *gin.Context) {
func SettingsUpdateHandler(c *gin.Context) {
// 先尝试解析为直接字段格式
var directBody map[string]interface{}
if !settingsBaseController.BindJSON(c, &directBody) {
if err := c.ShouldBindJSON(&directBody); err != nil {
authBaseController.HandleValidationError(c, "请求体错误")
return
}
@@ -82,7 +80,7 @@ func SettingsUpdateHandler(c *gin.Context) {
}
}
} else {
settingsBaseController.HandleValidationError(c, "settings字段格式错误")
authBaseController.HandleValidationError(c, "settings字段格式错误")
return
}
} else {
@@ -99,11 +97,19 @@ func SettingsUpdateHandler(c *gin.Context) {
}
if len(settingsData) == 0 {
settingsBaseController.HandleValidationError(c, "无设置项")
authBaseController.HandleValidationError(c, "无设置项")
return
}
db, ok := settingsBaseController.GetDB(c)
// 验证设置项值
for k, v := range settingsData {
if err := validateSettingValue(k, v); err != nil {
authBaseController.HandleValidationError(c, err.Error())
return
}
}
db, ok := authBaseController.GetDB(c)
if !ok {
return
}
@@ -113,13 +119,66 @@ func SettingsUpdateHandler(c *gin.Context) {
// 批量处理设置项
for k, v := range settingsData {
// 特殊处理 admin_password
if k == "admin_password" {
// 如果密码为空,跳过更新(保留原密码)
if v == "" {
continue
}
// 记录操作日志
// 由于 NetworkAuth 中没有 SystemAdminUser 全局变量,这里暂时使用 "admin"
// operator := "admin"
// 尝试从上下文获取用户名(如果中间件设置了的话)
// if user, exists := c.Get("username"); exists {
// operator = user.(string)
// }
// 生成随机盐值
salt, err := utils.GenerateRandomSalt()
if err != nil {
authBaseController.HandleInternalError(c, "生成盐值失败", err)
return
}
// 使用盐值哈希密码
hash, err := utils.HashPasswordWithSalt(v, salt)
if err != nil {
authBaseController.HandleInternalError(c, "密码哈希失败", err)
return
}
// 更新 salt 设置项(如果不存在则创建)
var saltSetting models.Settings
if err := db.Where("name = ?", "admin_password_salt").First(&saltSetting).Error; err != nil {
saltSetting = models.Settings{Name: "admin_password_salt", Value: salt}
if err := db.Create(&saltSetting).Error; err != nil {
logrus.WithError(err).Error("创建admin_password_salt失败")
authBaseController.HandleInternalError(c, "保存盐值失败", err)
return
}
} else {
if err := db.Model(&saltSetting).Update("value", salt).Error; err != nil {
logrus.WithError(err).Error("更新admin_password_salt失败")
authBaseController.HandleInternalError(c, "更新盐值失败", err)
return
}
}
// 将盐值相关的缓存键加入清理列表
keysToDel = append(keysToDel, "setting:admin_password_salt")
// 将当前处理的值替换为哈希后的密码
v = hash
}
var s models.Settings
if err := db.Where("name = ?", k).First(&s).Error; err != nil {
// 不存在则创建
s = models.Settings{Name: k, Value: v}
if err := db.Create(&s).Error; err != nil {
logrus.WithError(err).WithField("setting_name", k).Error("创建设置失败")
settingsBaseController.HandleInternalError(c, fmt.Sprintf("保存设置 %s 失败", k), err)
authBaseController.HandleInternalError(c, fmt.Sprintf("保存设置 %s 失败", k), err)
return
}
@@ -127,7 +186,7 @@ func SettingsUpdateHandler(c *gin.Context) {
// 存在则更新
if err := db.Model(&models.Settings{}).Where("id = ?", s.ID).Update("value", v).Error; err != nil {
logrus.WithError(err).WithField("setting_name", k).Error("更新设置失败")
settingsBaseController.HandleInternalError(c, fmt.Sprintf("更新设置 %s 失败", k), err)
authBaseController.HandleInternalError(c, fmt.Sprintf("更新设置 %s 失败", k), err)
return
}
@@ -137,10 +196,60 @@ func SettingsUpdateHandler(c *gin.Context) {
}
// 删除Redis缓存键如果Redis不可用则静默跳过
_ = utils.RedisDel(context.Background(), keysToDel...)
_ = utils.RedisDel(c.Request.Context(), keysToDel...)
// 刷新内存中的设置缓存,保证后续读取一致
services.GetSettingsService().RefreshCache()
settingsBaseController.HandleSuccess(c, "保存成功", nil)
authBaseController.HandleSuccess(c, "保存成功", nil)
}
// validateSettingValue 验证设置项值的合法性
func validateSettingValue(key, value string) error {
switch key {
case "jwt_refresh":
// 验证JWT刷新时间至少1小时
hours, err := strconv.Atoi(value)
if err != nil {
return fmt.Errorf("JWT刷新阈值必须是整数")
}
if hours < 1 {
return fmt.Errorf("JWT刷新阈值必须至少为1小时")
}
case "jwt_expire":
// 验证JWT有效期至少1小时
hours, err := strconv.Atoi(value)
if err != nil {
return fmt.Errorf("JWT有效期必须是整数")
}
if hours < 1 {
return fmt.Errorf("JWT有效期必须至少为1小时")
}
}
return nil
}
// SettingsGenerateKeyHandler 生成安全密钥API
// - type: "jwt" 或 "encryption"
func SettingsGenerateKeyHandler(c *gin.Context) {
keyType := c.Query("type")
var key string
var err error
switch keyType {
case "jwt":
key, err = config.GenerateSecureJWTSecret()
case "encryption":
key, err = config.GenerateSecureEncryptionKey()
default:
authBaseController.HandleValidationError(c, "无效的密钥类型")
return
}
if err != nil {
authBaseController.HandleInternalError(c, "生成密钥失败: "+err.Error(), err)
return
}
authBaseController.HandleSuccess(c, "生成成功", map[string]string{"key": key})
}

View File

@@ -1,282 +0,0 @@
package admin
import (
"net/http"
"networkDev/controllers"
"networkDev/models"
"networkDev/utils"
"strings"
"github.com/gin-gonic/gin"
)
// ============================================================================
// 全局变量
// ============================================================================
// 创建基础控制器实例
var baseController = controllers.NewBaseController()
// ============================================================================
// 页面处理器
// ============================================================================
// UserFragmentHandler 个人资料片段渲染
// - 渲染个人资料与修改密码表单
func UserFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "user.html", gin.H{})
}
// ============================================================================
// API处理器
// ============================================================================
// UserProfileQueryHandler 获取当前登录管理员的用户名
// - 返回 JSON: {username}
// - 直接从JWT获取用户名信息
func UserProfileQueryHandler(c *gin.Context) {
claims, _, err := GetCurrentAdminUserWithRefresh(c)
if err != nil {
baseController.HandleValidationError(c, "未登录或会话已过期")
return
}
baseController.HandleSuccess(c, "ok", gin.H{
"username": claims.Username,
})
}
// UserPasswordUpdateHandler 修改当前登录管理员的密码
// - 接收 JSON: {old_password, new_password, confirm_password}
// - 校验旧密码正确性、新密码与确认一致性
// - 成功后更新密码哈希
// - 自动刷新接近过期的JWT令牌
func UserPasswordUpdateHandler(c *gin.Context) {
claims, _, err := GetCurrentAdminUserWithRefresh(c)
if err != nil {
baseController.HandleValidationError(c, "未登录或会话已过期")
return
}
var body struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
ConfirmPassword string `json:"confirm_password"`
}
if !baseController.BindJSON(c, &body) {
return
}
// 基础校验
if !baseController.ValidateRequired(c, map[string]interface{}{
"旧密码": body.OldPassword,
"新密码": body.NewPassword,
"确认密码": body.ConfirmPassword,
}) {
return
}
if len(body.NewPassword) < 6 {
baseController.HandleValidationError(c, "新密码长度不能少于6位")
return
}
if body.NewPassword != body.ConfirmPassword {
baseController.HandleValidationError(c, "两次输入的新密码不一致")
return
}
if body.NewPassword == body.OldPassword {
baseController.HandleValidationError(c, "新密码不能与旧密码相同")
return
}
// 注释由于使用了AdminAuthRequired中间件已确保是管理员用户
// 获取数据库连接
db, ok := baseController.GetDB(c)
if !ok {
return
}
// 通过前缀匹配一次性获取所有管理员相关设置
var adminSettings []models.Settings
if err = db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; err != nil {
baseController.HandleInternalError(c, "获取管理员设置失败", err)
return
}
// 将设置转换为map便于查找
settingsMap := make(map[string]string)
for _, setting := range adminSettings {
settingsMap[setting.Name] = setting.Value
}
// 检查必要的设置是否存在
adminPassword, hasPassword := settingsMap["admin_password"]
adminPasswordSalt, hasSalt := settingsMap["admin_password_salt"]
if !hasPassword || !hasSalt {
baseController.HandleInternalError(c, "管理员密码设置不完整", nil)
return
}
// 校验旧密码
if !utils.VerifyPasswordWithSalt(body.OldPassword, adminPasswordSalt, adminPassword) {
baseController.HandleValidationError(c, "旧密码不正确")
return
}
// 生成新的密码盐值
newSalt, err := utils.GenerateRandomSalt()
if err != nil {
baseController.HandleInternalError(c, "生成密码盐失败", err)
return
}
// 生成新密码哈希
newPasswordHash, err := utils.HashPasswordWithSalt(body.NewPassword, newSalt)
if err != nil {
baseController.HandleInternalError(c, "生成密码哈希失败", err)
return
}
// 更新settings中的管理员密码和盐值
if err = db.Model(&models.Settings{}).Where("name = ?", "admin_password").Update("value", newPasswordHash).Error; err != nil {
baseController.HandleInternalError(c, "更新密码失败", err)
return
}
if err = db.Model(&models.Settings{}).Where("name = ?", "admin_password_salt").Update("value", newSalt).Error; err != nil {
baseController.HandleInternalError(c, "更新密码盐值失败", err)
return
}
// 重新生成JWT令牌包含新的密码哈希摘要
adminUser := models.User{
Username: claims.Username,
Password: newPasswordHash,
PasswordSalt: newSalt,
}
newToken, err := generateJWTTokenForAdmin(adminUser)
if err != nil {
baseController.HandleInternalError(c, "生成新令牌失败", err)
return
}
// 更新Cookie使用安全配置
c.SetCookie("admin_session", newToken, utils.GetDefaultCookieMaxAge(), "/", "", false, true)
// 密码修改成功已重新生成JWT令牌
baseController.HandleSuccess(c, "密码修改成功", nil)
}
// UserProfileUpdateHandler 修改当前登录管理员的用户名
// - 接收 JSON: {username}
// - 校验用户名非空、长度与唯一性
// - 更新数据库后重新签发JWT并写入 Cookie保持前端展示的一致性
// - 自动刷新接近过期的JWT令牌
func UserProfileUpdateHandler(c *gin.Context) {
_, _, err := GetCurrentAdminUserWithRefresh(c)
if err != nil {
baseController.HandleValidationError(c, "未登录或会话已过期")
return
}
var body struct {
Username string `json:"username"`
OldPassword string `json:"old_password"`
}
if !baseController.BindJSON(c, &body) {
return
}
username := strings.TrimSpace(body.Username)
if username == "" {
baseController.HandleValidationError(c, "用户名不能为空")
return
}
if len(username) > 64 {
baseController.HandleValidationError(c, "用户名长度不能超过64字符")
return
}
db, ok := baseController.GetDB(c)
if !ok {
return
}
// 注释由于使用了AdminAuthRequired中间件已确保是管理员用户
// 获取所有管理员相关设置
var adminSettings []models.Settings
if dbErr := db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; dbErr != nil {
baseController.HandleInternalError(c, "获取管理员设置失败", dbErr)
return
}
// 转换为map便于查找
settingsMap := make(map[string]string)
for _, setting := range adminSettings {
settingsMap[setting.Name] = setting.Value
}
adminUsername, exists := settingsMap["admin_username"]
if !exists {
baseController.HandleInternalError(c, "管理员用户名设置不存在", nil)
return
}
adminPassword, exists := settingsMap["admin_password"]
if !exists {
baseController.HandleInternalError(c, "管理员密码设置不存在", nil)
return
}
adminPasswordSalt, exists := settingsMap["admin_password_salt"]
if !exists {
baseController.HandleInternalError(c, "管理员密码盐值设置不存在", nil)
return
}
// 如果用户名未变化则直接返回成功(无需校验旧密码)
if strings.EqualFold(username, adminUsername) {
baseController.HandleSuccess(c, "保存成功", gin.H{
"username": username,
})
return
}
// 修改用户名需要进行当前密码校验
if strings.TrimSpace(body.OldPassword) == "" {
baseController.HandleValidationError(c, "修改用户名需要提供当前密码")
return
}
// 使用盐值验证当前密码
if !utils.VerifyPasswordWithSalt(body.OldPassword, adminPasswordSalt, adminPassword) {
baseController.HandleValidationError(c, "当前密码不正确")
return
}
// 更新管理员用户名设置
if dbErr := db.Model(&models.Settings{}).Where("name = ?", "admin_username").Update("value", username).Error; dbErr != nil {
baseController.HandleInternalError(c, "更新管理员用户名失败", dbErr)
return
}
// 重新签发JWT并写入Cookie
// 创建虚拟用户对象用于生成JWT令牌
adminUser := models.User{
Username: username, // 使用新的用户名
Password: adminPassword,
PasswordSalt: adminPasswordSalt,
}
token, err := generateJWTTokenForAdmin(adminUser)
if err != nil {
baseController.HandleInternalError(c, "生成新令牌失败", err)
return
}
c.SetCookie("admin_session", token, utils.GetDefaultCookieMaxAge(), "/", "", false, true)
baseController.HandleSuccess(c, "保存成功", gin.H{
"username": username,
})
}

View File

@@ -0,0 +1,7 @@
package admin
import (
"NetworkAuth/controllers"
)
var base = controllers.NewBaseController()

View File

@@ -1,9 +1,9 @@
package admin
import (
"NetworkAuth/controllers"
"NetworkAuth/models"
"net/http"
"networkDev/controllers"
"networkDev/models"
"regexp"
"strconv"
"strings"
@@ -319,7 +319,7 @@ func VariableDeleteHandler(c *gin.Context) {
return
}
logrus.WithField("variable_id", req.ID).Info("Successfully deleted variable")
logrus.WithField("variable_id", req.ID).Debug("Successfully deleted variable")
variableBaseController.HandleSuccess(c, "删除成功", nil)
}
@@ -351,7 +351,7 @@ func VariablesBatchDeleteHandler(c *gin.Context) {
return
}
logrus.WithField("variable_ids", req.IDs).Info("Successfully batch deleted variables")
logrus.WithField("variable_ids", req.IDs).Debug("Successfully batch deleted variables")
variableBaseController.HandleSuccess(c, "批量删除成功", nil)
}