From 270c5a8ffdbd9cdcbe9b8dbcd3f8aeac70ef7e0c Mon Sep 17 00:00:00 2001 From: skyle1995 Date: Sun, 26 Oct 2025 09:35:07 +0800 Subject: [PATCH] New administrator authentication method New configuration generation scheme --- .gitignore | 6 +- cmd/server.go | 6 +- config/config.go | 212 +++++++++++++++++++- config/config.json | 48 ----- config/security.go | 49 +++++ config/validator.go | 147 +------------- controllers/admin/auth.go | 223 +++++++-------------- controllers/admin/user.go | 160 ++++++++------- cookies.txt | 5 - database/seed_user.go | 54 ----- database/{seed_settings.go => settings.go} | 69 ++++++- models/user.go | 2 +- services/settings.go | 3 +- utils/common.go | 12 +- web/static/js/admin.js | 2 +- web/template/admin/apis.html | 2 +- web/template/admin/settings.html | 4 +- web/template/admin/user.html | 82 ++------ 18 files changed, 520 insertions(+), 566 deletions(-) delete mode 100644 config/config.json create mode 100644 config/security.go delete mode 100644 cookies.txt delete mode 100644 database/seed_user.go rename database/{seed_settings.go => settings.go} (58%) diff --git a/.gitignore b/.gitignore index 49e1259..1b7fcfb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ -/config.json /database.db /recharge.db logs 模板 .DS_Store -networkDev -node.txt \ No newline at end of file +config.json +node.txt +networkDev \ No newline at end of file diff --git a/cmd/server.go b/cmd/server.go index f9b8e84..b52d106 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -60,11 +60,7 @@ func runServer(cmd *cobra.Command, args []string) { if err := database.AutoMigrate(); err != nil { logrus.WithError(err).Fatal("数据库自动迁移失败") } - // 初始化默认管理员账号(admin/admin123) - if err := database.SeedDefaultAdmin(); err != nil { - logrus.WithError(err).Fatal("默认管理员初始化失败") - } - // 初始化默认系统设置 + // 初始化默认系统设置(包含管理员账号) if err := database.SeedDefaultSettings(); err != nil { logrus.WithError(err).Fatal("默认系统设置初始化失败") } diff --git a/config/config.go b/config/config.go index 92b52f4..fca869d 100644 --- a/config/config.go +++ b/config/config.go @@ -2,7 +2,7 @@ package config import ( "bytes" - _ "embed" + "encoding/json" "errors" "io/fs" "os" @@ -11,8 +11,156 @@ import ( "github.com/spf13/viper" ) -//go:embed config.json -var DefaultConfig string +// 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"` // 静态文件目录 +} + +// DatabaseConfig 数据库配置结构体 +// 包含数据库连接相关的配置信息 +type DatabaseConfig struct { + Type string `json:"type" mapstructure:"type"` // 数据库类型(mysql/sqlite) + MySQL MySQLConfig `json:"mysql" mapstructure:"mysql"` // MySQL配置 + SQLite SQLiteConfig `json:"sqlite" mapstructure:"sqlite"` // SQLite配置 +} + +// MySQLConfig MySQL数据库配置结构体 +// 包含MySQL数据库连接的详细配置信息 +type MySQLConfig struct { + Host string `json:"host" mapstructure:"host"` // 数据库主机地址 + Port int `json:"port" mapstructure:"port"` // 数据库端口 + Username string `json:"username" mapstructure:"username"` // 数据库用户名 + Password string `json:"password" mapstructure:"password"` // 数据库密码 + Database string `json:"database" mapstructure:"database"` // 数据库名称 + Charset string `json:"charset" mapstructure:"charset"` // 字符集 + MaxIdleConns int `json:"max_idle_conns" mapstructure:"max_idle_conns"` // 最大空闲连接数 + MaxOpenConns int `json:"max_open_conns" mapstructure:"max_open_conns"` // 最大打开连接数 +} + +// SQLiteConfig SQLite数据库配置结构体 +// 包含SQLite数据库文件路径配置 +type SQLiteConfig struct { + Path string `json:"path" mapstructure:"path"` // 数据库文件路径 +} + +// RedisConfig Redis配置结构体 +// 包含Redis连接相关的配置信息 +type RedisConfig struct { + Host string `json:"host" mapstructure:"host"` // Redis服务器地址 + Port int `json:"port" mapstructure:"port"` // Redis服务器端口 + Password string `json:"password" mapstructure:"password"` // Redis密码 + DB int `json:"db" mapstructure:"db"` // Redis数据库编号 +} + +// LogConfig 日志配置结构体 +// 包含日志记录相关的配置信息 +type LogConfig struct { + Level string `json:"level" mapstructure:"level"` // 日志级别 + File string `json:"file" mapstructure:"file"` // 日志文件路径 + MaxSize int `json:"max_size" mapstructure:"max_size"` // 单个日志文件最大大小(MB) + MaxBackups int `json:"max_backups" mapstructure:"max_backups"` // 保留的旧日志文件数量 + MaxAge int `json:"max_age" mapstructure:"max_age"` // 日志文件保留天数 +} + +// CookieConfig Cookie配置结构体 +// 包含Cookie相关的安全配置信息 +type CookieConfig struct { + Secure bool `json:"secure" mapstructure:"secure"` // 是否只在HTTPS下发送Cookie + SameSite string `json:"same_site" mapstructure:"same_site"` // SameSite属性(Strict/Lax/None) + Domain string `json:"domain" mapstructure:"domain"` // Cookie域名 + MaxAge int `json:"max_age" mapstructure:"max_age"` // Cookie最大存活时间(秒) +} + +// 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配置 +} + +// AppConfig 应用配置结构体 +type AppConfig struct { + Server ServerConfig `json:"server" mapstructure:"server"` + Database DatabaseConfig `json:"database" mapstructure:"database"` + Redis RedisConfig `json:"redis" mapstructure:"redis"` + Log LogConfig `json:"log" mapstructure:"log"` + Security SecurityConfig `json:"security" mapstructure:"security"` +} + +// GetDefaultAppConfig 获取默认应用配置 +func GetDefaultAppConfig() *AppConfig { + return &AppConfig{ + Server: ServerConfig{ + Host: "0.0.0.0", + Port: 8080, + Mode: "debug", + Dist: "", + }, + Database: DatabaseConfig{ + Type: "sqlite", + MySQL: MySQLConfig{ + Host: "localhost", + Port: 3306, + Username: "root", + Password: "password", + Database: "networkdev", + Charset: "utf8mb4", + MaxIdleConns: 10, + MaxOpenConns: 100, + }, + SQLite: SQLiteConfig{ + Path: "./database.db", + }, + }, + Redis: RedisConfig{ + Host: "localhost", + Port: 6379, + Password: "", + DB: 0, + }, + Log: LogConfig{ + Level: "info", + File: "./logs/app.log", + MaxSize: 100, + MaxBackups: 5, + MaxAge: 30, + }, + Security: SecurityConfig{ + JWTSecret: "", + EncryptionKey: "", + JWTRefreshThresholdHours: 6, + Cookie: CookieConfig{ + Secure: true, + SameSite: "Lax", + Domain: "", + MaxAge: 86400, + }, + }, + } +} + +// GetSecureDefaultAppConfig 获取带有安全密钥的默认应用配置 +func GetSecureDefaultAppConfig() (*AppConfig, error) { + config := GetDefaultAppConfig() + + // 生成安全密钥 + jwtSecret, encryptionKey, err := GenerateSecureKeys() + if err != nil { + return nil, err + } + + // 设置安全密钥 + config.Security.JWTSecret = jwtSecret + config.Security.EncryptionKey = encryptionKey + + return config, nil +} // Init 初始化配置文件 func Init(cfgFilePath string) { @@ -24,7 +172,31 @@ func Init(cfgFilePath string) { var pathError *fs.PathError if errors.As(err, &pathError) { log.Warn("未找到配置文件,使用默认配置") - err = os.WriteFile(cfgFilePath, []byte(DefaultConfig), 0o644) + + // 生成带有安全密钥的默认配置 + defaultConfig, configErr := GetSecureDefaultAppConfig() + if configErr != nil { + log.WithFields( + log.Fields{ + "err": configErr, + }, + ).Error("生成安全配置失败,使用基础默认配置") + defaultConfig = GetDefaultAppConfig() + } + + // 将配置结构体转换为JSON + configBytes, marshalErr := json.MarshalIndent(defaultConfig, "", " ") + if marshalErr != nil { + log.WithFields( + log.Fields{ + "err": marshalErr, + }, + ).Fatal("序列化默认配置失败") + return + } + + // 写入配置文件 + err = os.WriteFile(cfgFilePath, configBytes, 0o644) if err != nil { log.WithFields( log.Fields{ @@ -36,16 +208,17 @@ func Init(cfgFilePath string) { log.Fields{ "file": cfgFilePath, }, - ).Info("写入默认配置文件成功") + ).Info("写入默认配置文件成功(已生成安全密钥)") } - // 写完默认配置后再读一次 - err = viper.ReadConfig(bytes.NewBuffer([]byte(DefaultConfig))) + + // 将配置加载到viper中 + err = viper.ReadConfig(bytes.NewBuffer(configBytes)) if err != nil { log.WithFields( log.Fields{ "err": err, }, - ).Error("读取默认配置文件失败") + ).Error("读取默认配置失败") } else { log.Info("已成功读取默认配置") } @@ -63,8 +236,8 @@ func Init(cfgFilePath string) { }, ).Info("使用配置文件") - // 验证配置并设置默认值 - if _, err := ValidateAndSetDefaults(); err != nil { + // 验证配置 + if _, err := ValidateConfig(); err != nil { log.WithFields( log.Fields{ "err": err, @@ -75,5 +248,22 @@ func Init(cfgFilePath string) { // CreateDefaultConfig 创建默认配置文件 func CreateDefaultConfig(filePath string) error { - return os.WriteFile(filePath, []byte(DefaultConfig), 0o644) + // 生成带有安全密钥的默认配置 + defaultConfig, err := GetSecureDefaultAppConfig() + if err != nil { + log.WithFields( + log.Fields{ + "err": err, + }, + ).Error("生成安全配置失败,使用基础默认配置") + defaultConfig = GetDefaultAppConfig() + } + + // 将配置结构体转换为JSON + configBytes, err := json.MarshalIndent(defaultConfig, "", " ") + if err != nil { + return err + } + + return os.WriteFile(filePath, configBytes, 0o644) } diff --git a/config/config.json b/config/config.json deleted file mode 100644 index b87c00f..0000000 --- a/config/config.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "server": { - "host": "0.0.0.0", - "port": 8080, - "mode": "debug", - "dist": "" - }, - "database": { - "type": "sqlite", - "mysql": { - "host": "localhost", - "port": 3306, - "username": "root", - "password": "password", - "database": "networkdev", - "charset": "utf8mb4", - "max_idle_conns": 10, - "max_open_conns": 100 - }, - "sqlite": { - "path": "./database.db" - } - }, - "redis": { - "host": "localhost", - "port": 6379, - "password": "", - "db": 0 - }, - "log": { - "level": "info", - "file": "./logs/app.log", - "max_size": 100, - "max_backups": 5, - "max_age": 30 - }, - "security": { - "jwt_secret": "your-jwt-secret-key", - "encryption_key": "your-encryption-key", - "jwt_refresh_threshold_hours": 6, - "cookie": { - "secure": false, - "same_site": "Lax", - "domain": "", - "max_age": 86400 - } - } -} \ No newline at end of file diff --git a/config/security.go b/config/security.go new file mode 100644 index 0000000..1cf55bd --- /dev/null +++ b/config/security.go @@ -0,0 +1,49 @@ +package config + +import ( + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" +) + +// GenerateSecureJWTSecret 生成安全的JWT密钥 +// 生成64字节(512位)的随机密钥,使用base64编码 +func GenerateSecureJWTSecret() (string, error) { + // 生成64字节的随机数据 + bytes := make([]byte, 64) + if _, err := rand.Read(bytes); err != nil { + return "", fmt.Errorf("生成JWT密钥失败: %w", err) + } + + // 使用base64编码,便于配置文件存储 + return base64.StdEncoding.EncodeToString(bytes), nil +} + +// GenerateSecureEncryptionKey 生成安全的加密密钥 +// 生成32字节(256位)的随机密钥,使用十六进制编码 +func GenerateSecureEncryptionKey() (string, error) { + // 生成32字节的随机数据(AES-256) + bytes := make([]byte, 32) + if _, err := rand.Read(bytes); err != nil { + return "", fmt.Errorf("生成加密密钥失败: %w", err) + } + + // 使用十六进制编码 + return hex.EncodeToString(bytes), nil +} + +// GenerateSecureKeys 生成所有安全密钥 +func GenerateSecureKeys() (jwtSecret, encryptionKey string, err error) { + jwtSecret, err = GenerateSecureJWTSecret() + if err != nil { + return "", "", err + } + + encryptionKey, err = GenerateSecureEncryptionKey() + if err != nil { + return "", "", err + } + + return jwtSecret, encryptionKey, nil +} \ No newline at end of file diff --git a/config/validator.go b/config/validator.go index 8a54809..69c507e 100644 --- a/config/validator.go +++ b/config/validator.go @@ -13,80 +13,8 @@ import ( "github.com/spf13/viper" ) -// ServerConfig 服务器配置结构体 -// 包含HTTP服务器的基本配置信息 -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"` // 静态文件目录 -} - -// DatabaseConfig 数据库配置结构体 -// 支持MySQL和SQLite两种数据库类型 -type DatabaseConfig struct { - Type string `json:"type" mapstructure:"type"` // 数据库类型(mysql/sqlite) - MySQL MySQLConfig `json:"mysql" mapstructure:"mysql"` // MySQL配置 - SQLite SQLiteConfig `json:"sqlite" mapstructure:"sqlite"` // SQLite配置 -} - -// MySQLConfig MySQL数据库配置结构体 -// 包含MySQL数据库连接和连接池的配置信息 -type MySQLConfig struct { - Host string `json:"host" mapstructure:"host"` // 数据库主机地址 - Port int `json:"port" mapstructure:"port"` // 数据库端口 - Username string `json:"username" mapstructure:"username"` // 数据库用户名 - Password string `json:"password" mapstructure:"password"` // 数据库密码 - Database string `json:"database" mapstructure:"database"` // 数据库名称 - Charset string `json:"charset" mapstructure:"charset"` // 字符集 - MaxIdleConns int `json:"max_idle_conns" mapstructure:"max_idle_conns"` // 最大空闲连接数 - MaxOpenConns int `json:"max_open_conns" mapstructure:"max_open_conns"` // 最大打开连接数 -} - -// SQLiteConfig SQLite数据库配置结构体 -// 包含SQLite数据库文件路径配置 -type SQLiteConfig struct { - Path string `json:"path" mapstructure:"path"` // 数据库文件路径 -} - -// RedisConfig Redis配置结构体 -// 包含Redis缓存服务器的连接配置 -type RedisConfig struct { - Host string `json:"host" mapstructure:"host"` // Redis服务器地址 - Port int `json:"port" mapstructure:"port"` // Redis服务器端口 - Password string `json:"password" mapstructure:"password"` // Redis密码 - DB int `json:"db" mapstructure:"db"` // Redis数据库编号 -} - -// LogConfig 日志配置结构体 -// 包含日志记录的相关配置信息 -type LogConfig struct { - Level string `json:"level" mapstructure:"level"` // 日志级别 - File string `json:"file" mapstructure:"file"` // 日志文件路径 - MaxSize int `json:"max_size" mapstructure:"max_size"` // 单个日志文件最大大小(MB) - MaxBackups int `json:"max_backups" mapstructure:"max_backups"` // 保留的旧日志文件数量 - MaxAge int `json:"max_age" mapstructure:"max_age"` // 日志文件保留天数 -} - -// 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令牌刷新阈值(小时) -} - -// AppConfig 应用配置结构体 -type AppConfig struct { - Server ServerConfig `json:"server" mapstructure:"server"` - Database DatabaseConfig `json:"database" mapstructure:"database"` - Redis RedisConfig `json:"redis" mapstructure:"redis"` - Log LogConfig `json:"log" mapstructure:"log"` - Security SecurityConfig `json:"security" mapstructure:"security"` -} - -// ValidateAndSetDefaults 验证配置并设置默认值 -func ValidateAndSetDefaults() (*AppConfig, error) { +// ValidateConfig 验证配置 +func ValidateConfig() (*AppConfig, error) { var config AppConfig // 解析配置到结构体 @@ -94,9 +22,6 @@ func ValidateAndSetDefaults() (*AppConfig, error) { return nil, fmt.Errorf("解析配置失败: %w", err) } - // 设置默认值 - setDefaults(&config) - // 验证配置 if err := validateConfig(&config); err != nil { return nil, fmt.Errorf("配置验证失败: %w", err) @@ -106,74 +31,6 @@ func ValidateAndSetDefaults() (*AppConfig, error) { return &config, nil } -// setDefaults 设置默认值 -func setDefaults(config *AppConfig) { - // 服务器默认值 - if config.Server.Host == "" { - config.Server.Host = "0.0.0.0" - } - if config.Server.Port == 0 { - config.Server.Port = 8080 - } - if config.Server.Mode == "" { - config.Server.Mode = "debug" - } - - // 数据库默认值 - if config.Database.Type == "" { - config.Database.Type = "sqlite" - } - if config.Database.MySQL.Port == 0 { - config.Database.MySQL.Port = 3306 - } - if config.Database.MySQL.Charset == "" { - config.Database.MySQL.Charset = "utf8mb4" - } - if config.Database.MySQL.MaxIdleConns == 0 { - config.Database.MySQL.MaxIdleConns = 10 - } - if config.Database.MySQL.MaxOpenConns == 0 { - config.Database.MySQL.MaxOpenConns = 100 - } - if config.Database.SQLite.Path == "" { - config.Database.SQLite.Path = "./database.db" - } - - // Redis默认值 - if config.Redis.Host == "" { - config.Redis.Host = "localhost" - } - if config.Redis.Port == 0 { - config.Redis.Port = 6379 - } - - // 日志默认值 - if config.Log.Level == "" { - config.Log.Level = "info" - } - // 不为空的日志文件路径设置默认值,保持为空表示只输出到控制台 - if config.Log.MaxSize == 0 { - config.Log.MaxSize = 100 - } - if config.Log.MaxBackups == 0 { - config.Log.MaxBackups = 5 - } - if config.Log.MaxAge == 0 { - config.Log.MaxAge = 30 - } - - // 安全配置默认值 - if config.Security.JWTSecret == "" || config.Security.JWTSecret == "your-jwt-secret-key" { - config.Security.JWTSecret = "default-jwt-secret-change-in-production" - } - if config.Security.EncryptionKey == "" || config.Security.EncryptionKey == "your-encryption-key" { - config.Security.EncryptionKey = "default-encryption-key-change-in-production" - } - if config.Security.JWTRefreshThresholdHours == 0 { - config.Security.JWTRefreshThresholdHours = 6 - } -} - // validateConfig 验证配置 func validateConfig(config *AppConfig) error { // 验证服务器配置 diff --git a/controllers/admin/auth.go b/controllers/admin/auth.go index fa3a055..d86a4f6 100644 --- a/controllers/admin/auth.go +++ b/controllers/admin/auth.go @@ -48,7 +48,7 @@ func LoginPageHandler(w http.ResponseWriter, r *http.Request) { } data := utils.GetDefaultTemplateData() data["CSRFToken"] = token - + // 合并额外数据 for key, value := range extraData { data[key] = value @@ -98,25 +98,56 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { return } - var user models.User - dbErr := db.Where("username = ?", body.Username).First(&user).Error - if dbErr != nil { + // 通过前缀匹配一次性获取所有管理员相关设置 + var adminSettings []models.Settings + if err = db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; err != nil { utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil) return } - if user.Role != 0 { - utils.JsonResponse(w, http.StatusForbidden, false, "非管理员账号不可登录后台", nil) + + // 将设置转换为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 { + utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil) + return + } + + // 验证用户名 + if body.Username != adminUsername { + utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil) + return + } + + // 验证密码为空的情况(首次登录需要初始化) + if adminPassword == "" || adminPasswordSalt == "" { + utils.JsonResponse(w, http.StatusInternalServerError, false, "管理员账号未初始化,请联系系统管理员", nil) return } // 使用盐值验证密码 - if !utils.VerifyPasswordWithSalt(body.Password, user.PasswordSalt, user.Password) { + if !utils.VerifyPasswordWithSalt(body.Password, adminPasswordSalt, adminPassword) { utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil) return } + // 创建虚拟用户对象用于生成JWT令牌 + adminUser := models.User{ + Username: adminUsername, + Password: adminPassword, + PasswordSalt: adminPasswordSalt, + } + // 生成JWT令牌 - token, err := generateJWTToken(user) + token, err := generateJWTTokenForAdmin(adminUser) if err != nil { utils.JsonResponse(w, http.StatusInternalServerError, false, "生成令牌失败", nil) return @@ -159,32 +190,30 @@ var jwtSecret = []byte(viper.GetString("security.jwt_secret")) // JWTClaims JWT载荷结构 type JWTClaims struct { - UserUUID string `json:"user_uuid"` Username string `json:"username"` - Role int `json:"role"` + IsAdmin bool `json:"is_admin"` // 是否为管理员 PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改 jwt.RegisteredClaims } -// generateJWTToken 生成JWT令牌 -// - 包含用户ID、用户名、角色信息 +// generateJWTTokenForAdmin 生成管理员JWT令牌 +// - 包含管理员UUID、用户名信息 // - 设置24小时过期时间 // - 使用HMAC-SHA256签名 -func generateJWTToken(user models.User) (string, error) { +func generateJWTTokenForAdmin(adminUser models.User) (string, error) { // 生成密码哈希摘要(使用SHA256) - passwordHashDigest := utils.GenerateSHA256Hash(user.Password) - + passwordHashDigest := utils.GenerateSHA256Hash(adminUser.Password) + claims := JWTClaims{ - UserUUID: user.UUID, - Username: user.Username, - Role: user.Role, + Username: adminUser.Username, + IsAdmin: true, // 管理员 PasswordHash: passwordHashDigest, // 包含密码哈希摘要 RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), Issuer: "凌动技术", - Subject: user.UUID, + Subject: adminUser.Username, }, } @@ -230,39 +259,16 @@ func IsAdminAuthenticated(r *http.Request) bool { return false } - // 验证用户角色(只允许管理员角色=0) - if claims.Role != 0 { + // 验证用户角色(只允许管理员) + if !claims.IsAdmin { return false } - // 验证用户是否仍然存在于数据库中 - db, err := database.GetDB() - if err != nil { - return false - } - - var user models.User - if dbErr := db.Where("uuid = ? AND role = 0", claims.UserUUID).First(&user).Error; dbErr != nil { - // 记录安全事件:用户不存在但持有有效JWT令牌 - fmt.Printf("[SECURITY WARNING] Invalid JWT token detected - User not found: UUID=%s, Username=%s, IP=%s\n", - claims.UserUUID, claims.Username, r.RemoteAddr) - return false - } - - // 验证用户名是否匹配(防止用户名被修改后仍使用旧令牌) - if user.Username != claims.Username { - // 记录安全事件:用户名不匹配 - fmt.Printf("[SECURITY WARNING] Username mismatch detected - Token username=%s, DB username=%s, UUID=%s, IP=%s\n", - claims.Username, user.Username, claims.UserUUID, r.RemoteAddr) - return false - } - - // 验证密码哈希是否匹配(防止密码被修改后仍使用旧令牌) - currentPasswordHash := utils.GenerateSHA256Hash(user.Password) - if claims.PasswordHash != currentPasswordHash { - // 记录安全事件:密码哈希不匹配,可能密码已被修改 - fmt.Printf("[SECURITY WARNING] Password hash mismatch detected - Token may be invalid due to password change: UUID=%s, Username=%s, IP=%s\n", - claims.UserUUID, claims.Username, r.RemoteAddr) + // 对于管理员,不需要验证数据库中的用户记录,因为管理员信息存储在settings中 + // 只需要验证JWT中的信息即可 + if !claims.IsAdmin { + fmt.Printf("[SECURITY WARNING] Invalid admin token detected - Username=%s, IP=%s\n", + claims.Username, r.RemoteAddr) return false } @@ -286,42 +292,17 @@ func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) boo return false } - // 验证用户角色(只允许管理员角色=0) - if claims.Role != 0 { + // 验证用户角色(只允许管理员) + if !claims.IsAdmin { clearInvalidJWTCookie(w) return false } - // 验证用户是否仍然存在于数据库中 - db, err := database.GetDB() - if err != nil { - return false - } - - var user models.User - if dbErr := db.Where("uuid = ? AND role = 0", claims.UserUUID).First(&user).Error; dbErr != nil { - // 记录安全事件并清理失效Cookie - fmt.Printf("[SECURITY WARNING] Invalid JWT token detected - User not found: UUID=%s, Username=%s, IP=%s\n", - claims.UserUUID, claims.Username, r.RemoteAddr) - clearInvalidJWTCookie(w) - return false - } - - // 验证用户名是否匹配(防止用户名被修改后仍使用旧令牌) - if user.Username != claims.Username { - // 记录安全事件并清理失效Cookie - fmt.Printf("[SECURITY WARNING] Username mismatch detected - Token username=%s, DB username=%s, UUID=%s, IP=%s\n", - claims.Username, user.Username, claims.UserUUID, r.RemoteAddr) - clearInvalidJWTCookie(w) - return false - } - - // 验证密码哈希是否匹配(防止密码被修改后仍使用旧令牌) - currentPasswordHash := utils.GenerateSHA256Hash(user.Password) - if claims.PasswordHash != currentPasswordHash { - // 记录安全事件并清理失效Cookie - fmt.Printf("[SECURITY WARNING] Password hash mismatch detected - Token may be invalid due to password change: UUID=%s, Username=%s, IP=%s\n", - claims.UserUUID, claims.Username, r.RemoteAddr) + // 对于管理员,不需要验证数据库中的用户记录,因为管理员信息存储在settings中 + // 只需要验证JWT中的信息即可 + if !claims.IsAdmin { + fmt.Printf("[SECURITY WARNING] Invalid admin token detected - Username=%s, IP=%s\n", + claims.Username, r.RemoteAddr) clearInvalidJWTCookie(w) return false } @@ -344,39 +325,14 @@ func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) { return nil, fmt.Errorf("无效的会话信息") } - if claims.Role != 0 { + if !claims.IsAdmin { return nil, fmt.Errorf("权限不足") } - // 验证用户是否仍然存在于数据库中 - db, err := database.GetDB() - if err != nil { - return nil, fmt.Errorf("数据库连接失败") - } - - var user models.User - if dbErr := db.Where("uuid = ? AND role = 0", claims.UserUUID).First(&user).Error; dbErr != nil { - // 记录安全事件:用户不存在但持有有效JWT令牌 - fmt.Printf("[SECURITY WARNING] Invalid JWT token detected in GetCurrentAdminUser - User not found: UUID=%s, Username=%s, IP=%s\n", - claims.UserUUID, claims.Username, r.RemoteAddr) - return nil, fmt.Errorf("用户不存在或权限已变更") - } - - // 验证用户名是否匹配(防止用户名被修改后仍使用旧令牌) - if user.Username != claims.Username { - // 记录安全事件:用户名不匹配 - fmt.Printf("[SECURITY WARNING] Username mismatch detected in GetCurrentAdminUser - Token username=%s, DB username=%s, UUID=%s, IP=%s\n", - claims.Username, user.Username, claims.UserUUID, r.RemoteAddr) - return nil, fmt.Errorf("用户信息已变更,请重新登录") - } - - // 验证密码哈希是否匹配(防止密码被修改后仍使用旧令牌) - currentPasswordHash := utils.GenerateSHA256Hash(user.Password) - if claims.PasswordHash != currentPasswordHash { - // 记录安全事件:密码哈希不匹配,可能密码已被修改 - fmt.Printf("[SECURITY WARNING] Password hash mismatch detected in GetCurrentAdminUser - Token may be invalid due to password change: UUID=%s, Username=%s, IP=%s\n", - claims.UserUUID, claims.Username, r.RemoteAddr) - return nil, fmt.Errorf("密码已变更,请重新登录") + // 对于管理员,不需要验证数据库中的用户记录,因为管理员信息存储在settings中 + // 只需要验证JWT中的信息即可 + if !claims.IsAdmin { + return nil, fmt.Errorf("无效的管理员令牌") } return claims, nil @@ -397,52 +353,25 @@ func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JW return nil, false, fmt.Errorf("无效的会话信息") } - if claims.Role != 0 { + if !claims.IsAdmin { return nil, false, fmt.Errorf("权限不足") } - // 验证用户是否仍然存在于数据库中 - db, err := database.GetDB() - if err != nil { - return nil, false, fmt.Errorf("数据库连接失败") - } - - var user models.User - if dbErr := db.Where("uuid = ? AND role = 0", claims.UserUUID).First(&user).Error; dbErr != nil { - // 记录安全事件:用户不存在但持有有效JWT令牌 - fmt.Printf("[SECURITY WARNING] Invalid JWT token detected in GetCurrentAdminUserWithRefresh - User not found: UUID=%s, Username=%s, IP=%s\n", - claims.UserUUID, claims.Username, r.RemoteAddr) - return nil, false, fmt.Errorf("用户不存在或权限已变更") - } - - // 验证用户名是否匹配(防止用户名被修改后仍使用旧令牌) - if user.Username != claims.Username { - // 记录安全事件:用户名不匹配 - fmt.Printf("[SECURITY WARNING] Username mismatch detected in GetCurrentAdminUserWithRefresh - Token username=%s, DB username=%s, UUID=%s, IP=%s\n", - claims.Username, user.Username, claims.UserUUID, r.RemoteAddr) - return nil, false, fmt.Errorf("用户信息已变更,请重新登录") - } - - // 验证密码哈希是否匹配(防止密码被修改后仍使用旧令牌) - currentPasswordHash := utils.GenerateSHA256Hash(user.Password) - if claims.PasswordHash != currentPasswordHash { - // 记录安全事件:密码哈希不匹配,可能密码已被修改 - fmt.Printf("[SECURITY WARNING] Password hash mismatch detected in GetCurrentAdminUserWithRefresh - Token may be invalid due to password change: UUID=%s, Username=%s, IP=%s\n", - claims.UserUUID, claims.Username, r.RemoteAddr) - return nil, false, fmt.Errorf("密码已变更,请重新登录") + // 对于管理员,不需要验证数据库中的用户记录,因为管理员信息存储在settings中 + // 只需要验证JWT中的信息即可 + if !claims.IsAdmin { + return nil, false, fmt.Errorf("无效的管理员令牌") } // 检查是否需要刷新令牌(根据配置的阈值) refreshed := false refreshThreshold := time.Duration(viper.GetInt("security.jwt_refresh_threshold_hours")) * time.Hour if time.Until(claims.ExpiresAt.Time) < refreshThreshold { - // 生成新的JWT令牌 - user := models.User{ - UUID: claims.UserUUID, + // 为管理员生成新的JWT令牌 + adminUser := models.User{ Username: claims.Username, - Role: claims.Role, } - newToken, err := generateJWTToken(user) + newToken, err := generateJWTTokenForAdmin(adminUser) if err == nil { // 更新Cookie(使用安全配置) newCookie := utils.CreateSecureCookie("admin_session", newToken, utils.GetDefaultCookieMaxAge()) @@ -468,7 +397,7 @@ func AdminAuthRequired(next http.HandlerFunc) http.HandlerFunc { if err != nil { // 自动清理失效的JWT Cookie,提升安全性和用户体验 clearInvalidJWTCookie(w) - + // 中文注释:区分普通页面请求与AJAX/JSON请求 // - 对 AJAX/JSON:直接返回 401 JSON,便于前端处理(如提示重新登录) // - 对普通页面:保持原有重定向到登录页 diff --git a/controllers/admin/user.go b/controllers/admin/user.go index 56ba7ef..3c44059 100644 --- a/controllers/admin/user.go +++ b/controllers/admin/user.go @@ -2,7 +2,6 @@ package admin import ( "encoding/json" - "fmt" "net/http" "networkDev/database" "networkDev/models" @@ -16,9 +15,9 @@ func UserFragmentHandler(w http.ResponseWriter, r *http.Request) { utils.RenderTemplate(w, "user.html", map[string]interface{}{}) } -// UserProfileQueryHandler 查询当前登录管理员的基本信息 -// - 返回 uuid/username/role/created_at 四个字段 -// - 自动刷新接近过期的JWT令牌 +// UserProfileQueryHandler 获取当前登录管理员的用户名 +// - 返回 JSON: {username} +// - 直接从JWT获取用户名信息 func UserProfileQueryHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) @@ -31,24 +30,8 @@ func UserProfileQueryHandler(w http.ResponseWriter, r *http.Request) { return } - // 查询用户完整信息以获取创建时间 - db, err := database.GetDB() - if err != nil { - utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil) - return - } - - var user models.User - if dbErr := db.Where("uuid = ?", claims.UserUUID).First(&user).Error; dbErr != nil { - utils.JsonResponse(w, http.StatusNotFound, false, "用户不存在", nil) - return - } - utils.JsonResponse(w, http.StatusOK, true, "ok", map[string]interface{}{ - "uuid": user.UUID, - "username": user.Username, - "role": user.Role, - "created_at": user.CreatedAt, + "username": claims.Username, }) } @@ -98,21 +81,42 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) { return } + // 确认是管理员 + if !claims.IsAdmin { + utils.JsonResponse(w, http.StatusForbidden, false, "权限不足", nil) + return + } + + // 获取数据库连接 db, err := database.GetDB() if err != nil { utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil) return } - // 查询当前用户 - var user models.User - if dbErr := db.Where("uuid = ?", claims.UserUUID).First(&user).Error; dbErr != nil { - utils.JsonResponse(w, http.StatusNotFound, false, "用户不存在", nil) + // 通过前缀匹配一次性获取所有管理员相关设置 + var adminSettings []models.Settings + if err = db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; err != nil { + utils.JsonResponse(w, http.StatusInternalServerError, false, "获取管理员设置失败", nil) return } - // 校验旧密码(使用盐值验证) - if !utils.VerifyPasswordWithSalt(body.OldPassword, user.PasswordSalt, user.Password) { + // 将设置转换为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 { + utils.JsonResponse(w, http.StatusInternalServerError, false, "管理员密码设置不完整", nil) + return + } + + // 校验旧密码 + if !utils.VerifyPasswordWithSalt(body.OldPassword, adminPasswordSalt, adminPassword) { utils.JsonResponse(w, http.StatusUnauthorized, false, "旧密码不正确", nil) return } @@ -120,41 +124,34 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) { // 生成新的密码盐值 newSalt, err := utils.GenerateRandomSalt() if err != nil { - // 添加详细错误日志 - fmt.Printf("生成密码盐失败: %v\n", err) utils.JsonResponse(w, http.StatusInternalServerError, false, "生成密码盐失败", nil) return } - fmt.Printf("成功生成新盐值,长度: %d\n", len(newSalt)) - // 使用新盐值生成密码哈希 - hash, err := utils.HashPasswordWithSalt(body.NewPassword, newSalt) + // 生成新密码哈希 + newPasswordHash, err := utils.HashPasswordWithSalt(body.NewPassword, newSalt) if err != nil { - // 添加详细错误日志 - fmt.Printf("生成密码哈希失败: %v, 密码长度: %d, 盐值长度: %d\n", err, len(body.NewPassword), len(newSalt)) utils.JsonResponse(w, http.StatusInternalServerError, false, "生成密码哈希失败", nil) return } - fmt.Printf("成功生成密码哈希,长度: %d\n", len(hash)) - // 更新密码和盐值 - if dbErr := db.Model(&models.User{}).Where("uuid = ?", claims.UserUUID).Updates(map[string]interface{}{ - "password": hash, - "password_salt": newSalt, - }).Error; dbErr != nil { + // 更新settings中的管理员密码和盐值 + if err = db.Model(&models.Settings{}).Where("name = ?", "admin_password").Update("value", newPasswordHash).Error; err != nil { utils.JsonResponse(w, http.StatusInternalServerError, false, "更新密码失败", nil) return } - - // 重新查询用户信息(包含新密码) - var updatedUser models.User - if dbErr := db.Where("uuid = ?", claims.UserUUID).First(&updatedUser).Error; dbErr != nil { - utils.JsonResponse(w, http.StatusInternalServerError, false, "查询用户信息失败", nil) + if err = db.Model(&models.Settings{}).Where("name = ?", "admin_password_salt").Update("value", newSalt).Error; err != nil { + utils.JsonResponse(w, http.StatusInternalServerError, false, "更新密码盐值失败", nil) return } // 重新生成JWT令牌(包含新的密码哈希摘要) - newToken, err := generateJWTToken(updatedUser) + adminUser := models.User{ + Username: claims.Username, + Password: newPasswordHash, + PasswordSalt: newSalt, + } + newToken, err := generateJWTTokenForAdmin(adminUser) if err != nil { utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil) return @@ -210,19 +207,45 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) { return } - // 检查唯一性:排除当前用户UUID - var cnt int64 - if dbErr := db.Model(&models.User{}).Where("username = ? AND uuid <> ?", username, claims.UserUUID).Count(&cnt).Error; dbErr != nil { - utils.JsonResponse(w, http.StatusInternalServerError, false, "检查用户名唯一性失败", nil) - return - } - if cnt > 0 { - utils.JsonResponse(w, http.StatusBadRequest, false, "用户名已存在,请更换", nil) + // 确认当前用户是管理员 + if !claims.IsAdmin { + utils.JsonResponse(w, http.StatusForbidden, false, "权限不足", nil) return } - // 如果未变化则直接返回成功(无需校验旧密码) - if strings.EqualFold(username, claims.Username) { + // 获取所有管理员相关设置 + var adminSettings []models.Settings + if dbErr := db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; dbErr != nil { + utils.JsonResponse(w, http.StatusInternalServerError, false, "获取管理员设置失败", nil) + return + } + + // 转换为map便于查找 + settingsMap := make(map[string]string) + for _, setting := range adminSettings { + settingsMap[setting.Name] = setting.Value + } + + adminUsername, exists := settingsMap["admin_username"] + if !exists { + utils.JsonResponse(w, http.StatusInternalServerError, false, "管理员用户名设置不存在", nil) + return + } + + adminPassword, exists := settingsMap["admin_password"] + if !exists { + utils.JsonResponse(w, http.StatusInternalServerError, false, "管理员密码设置不存在", nil) + return + } + + adminPasswordSalt, exists := settingsMap["admin_password_salt"] + if !exists { + utils.JsonResponse(w, http.StatusInternalServerError, false, "管理员密码盐值设置不存在", nil) + return + } + + // 如果用户名未变化则直接返回成功(无需校验旧密码) + if strings.EqualFold(username, adminUsername) { utils.JsonResponse(w, http.StatusOK, true, "保存成功", map[string]interface{}{ "username": username, }) @@ -234,28 +257,27 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) { utils.JsonResponse(w, http.StatusBadRequest, false, "修改用户名需要提供当前密码", nil) return } - // 查询当前用户并校验旧密码 - var user models.User - if dbErr := db.Where("uuid = ?", claims.UserUUID).First(&user).Error; dbErr != nil { - utils.JsonResponse(w, http.StatusNotFound, false, "用户不存在", nil) - return - } + // 使用盐值验证当前密码 - if !utils.VerifyPasswordWithSalt(body.OldPassword, user.PasswordSalt, user.Password) { + if !utils.VerifyPasswordWithSalt(body.OldPassword, adminPasswordSalt, adminPassword) { utils.JsonResponse(w, http.StatusUnauthorized, false, "当前密码不正确", nil) return } - // 执行更新 - if dbErr := db.Model(&models.User{}).Where("uuid = ?", claims.UserUUID).Update("username", username).Error; dbErr != nil { - utils.JsonResponse(w, http.StatusInternalServerError, false, "更新用户名失败", nil) + // 更新管理员用户名设置 + if dbErr := db.Model(&models.Settings{}).Where("name = ?", "admin_username").Update("value", username).Error; dbErr != nil { + utils.JsonResponse(w, http.StatusInternalServerError, false, "更新管理员用户名失败", nil) return } // 重新签发JWT并写入Cookie - // 使用完整的用户信息(包含密码)来生成JWT令牌 - user.Username = username // 更新用户名 - token, err := generateJWTToken(user) + // 创建虚拟用户对象用于生成JWT令牌 + adminUser := models.User{ + Username: username, // 使用新的用户名 + Password: adminPassword, + PasswordSalt: adminPasswordSalt, + } + token, err := generateJWTTokenForAdmin(adminUser) if err != nil { utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil) return diff --git a/cookies.txt b/cookies.txt deleted file mode 100644 index 73a80a5..0000000 --- a/cookies.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Netscape HTTP Cookie File -# https://curl.se/docs/http-cookies.html -# This file was generated by libcurl! Edit at your own risk. - -#HttpOnly_localhost FALSE / TRUE 1761422606 csrf_token QLYaH1VddKCyAFgijZ80OYxzDht7zVLPbXH-rprEXvM= diff --git a/database/seed_user.go b/database/seed_user.go deleted file mode 100644 index d991e35..0000000 --- a/database/seed_user.go +++ /dev/null @@ -1,54 +0,0 @@ -package database - -import ( - "networkDev/models" - "networkDev/utils" - - "github.com/sirupsen/logrus" -) - -// SeedDefaultAdmin 初始化默认管理员账号 -// - 如果已存在任何管理员用户(role=0),则跳过 -// - 如不存在,则创建用户名为 admin、密码为 admin123(以 bcrypt 哈希存储)、角色 Role=0 的管理员 -// - 根据需求:默认 admin 用户的 ID 固定为 10000 -func SeedDefaultAdmin() error { - db, err := GetDB() - if err != nil { - return err - } - - // 检查是否存在任何管理员用户(role=0) - var count int64 - if dbErr := db.Model(&models.User{}).Where("role = ?", 0).Count(&count).Error; dbErr != nil { - return dbErr - } - if count > 0 { - logrus.Info("已存在管理员用户,跳过默认管理员创建") - return nil - } - - // 生成密码盐值 - salt, err := utils.GenerateRandomSalt() - if err != nil { - return err - } - - // 使用盐值生成密码哈希(不存明文) - hash, err := utils.HashPasswordWithSalt("admin123", salt) - if err != nil { - return err - } - - // 创建默认管理员(ID和UUID将自动生成) - admin := models.User{ - Username: "admin", - Password: hash, - PasswordSalt: salt, - Role: 0, // 0=管理员 - } - if err := db.Create(&admin).Error; err != nil { - return err - } - logrus.WithField("username", "admin").WithField("uuid", admin.UUID).Info("默认管理员创建成功") - return nil -} diff --git a/database/seed_settings.go b/database/settings.go similarity index 58% rename from database/seed_settings.go rename to database/settings.go index 15834b7..d43bd89 100644 --- a/database/seed_settings.go +++ b/database/settings.go @@ -2,8 +2,10 @@ package database import ( "networkDev/models" + "networkDev/utils" "github.com/sirupsen/logrus" + "gorm.io/gorm" ) // SeedDefaultSettings 初始化默认系统设置 @@ -60,7 +62,23 @@ func SeedDefaultSettings() error { { Name: "maintenance_mode", Value: "0", - Description: "系统开关,0=开启系统,1=关闭系统", + Description: "维护模式,0=关闭维护模式,1=开启维护模式", + }, + // ===== 管理员账号相关默认项 ===== + { + Name: "admin_username", + Value: "admin", + Description: "管理员用户名", + }, + { + Name: "admin_password", + Value: "", + Description: "管理员密码哈希值", + }, + { + Name: "admin_password_salt", + Value: "", + Description: "管理员密码加密盐值", }, // ===== 页脚与备案相关默认项 ===== { @@ -106,6 +124,55 @@ func SeedDefaultSettings() error { } } + // 初始化默认管理员账号(如果密码为空) + if err := initDefaultAdmin(db); err != nil { + return err + } + logrus.Info("默认系统设置初始化完成") return nil } + +// initDefaultAdmin 初始化默认管理员账号 +// 如果admin_password为空,则生成默认密码admin123的哈希值 +func initDefaultAdmin(db *gorm.DB) error { + var passwordSetting models.Settings + if err := db.Where("name = ?", "admin_password").First(&passwordSetting).Error; err != nil { + logrus.WithError(err).Error("获取管理员密码设置失败") + return err + } + + // 如果密码已设置,跳过初始化 + if passwordSetting.Value != "" { + logrus.Info("管理员密码已设置,跳过默认密码初始化") + return nil + } + + // 生成密码盐值 + salt, err := utils.GenerateRandomSalt() + if err != nil { + logrus.WithError(err).Error("生成密码盐值失败") + return err + } + + // 使用盐值生成密码哈希(默认密码:admin123) + hash, err := utils.HashPasswordWithSalt("admin123", salt) + if err != nil { + logrus.WithError(err).Error("生成密码哈希失败") + return err + } + + // 更新密码和盐值 + if err := db.Model(&models.Settings{}).Where("name = ?", "admin_password").Update("value", hash).Error; err != nil { + logrus.WithError(err).Error("更新管理员密码失败") + return err + } + + if err := db.Model(&models.Settings{}).Where("name = ?", "admin_password_salt").Update("value", salt).Error; err != nil { + logrus.WithError(err).Error("更新管理员密码盐值失败") + return err + } + + logrus.Info("默认管理员账号初始化完成,用户名: admin, 密码: admin123") + return nil +} diff --git a/models/user.go b/models/user.go index 016061b..fbeb5c6 100644 --- a/models/user.go +++ b/models/user.go @@ -10,13 +10,13 @@ import ( // User 用户表模型 // 说明:PasswordSalt 使用 32 字节随机盐(以 16 进制存储为 64 个字符),因此列长度设置为 64 +// 注意:此表只存储普通用户,管理员账号存储在settings表中 type User struct { ID uint `gorm:"primaryKey;comment:用户ID,自增主键"` UUID string `gorm:"uniqueIndex;size:36;not null;comment:用户的唯一标识符" json:"uuid"` Username string `gorm:"uniqueIndex;size:64;not null;comment:用户名,唯一索引"` Password string `gorm:"size:255;not null;comment:密码哈希值"` PasswordSalt string `gorm:"size:64;not null;comment:密码加密盐值"` - Role int `gorm:"not null;comment:用户角色,0=管理员,1=普通用户"` CreatedAt time.Time `gorm:"comment:创建时间"` UpdatedAt time.Time `gorm:"comment:更新时间"` } diff --git a/services/settings.go b/services/settings.go index fae5b57..1be1d05 100644 --- a/services/settings.go +++ b/services/settings.go @@ -93,13 +93,12 @@ func (s *SettingsService) RefreshCache() { s.loadAllSettings() } - // GetSessionTimeout 获取会话超时时间(秒) func (s *SettingsService) GetSessionTimeout() int { return s.GetInt("session_timeout", 3600) // 默认1小时 } -// IsMaintenanceMode 检查系统是否关闭 +// IsMaintenanceMode 检查是否开启维护模式 func (s *SettingsService) IsMaintenanceMode() bool { return s.GetBool("maintenance_mode", false) } diff --git a/utils/common.go b/utils/common.go index 5f93771..22b50b0 100644 --- a/utils/common.go +++ b/utils/common.go @@ -60,17 +60,15 @@ func GetDefaultTemplateData() map[string]interface{} { func GetTemplateDataWithCSRF(r *http.Request, additionalData map[string]interface{}) map[string]interface{} { // 获取默认模板数据 data := GetDefaultTemplateData() - + // 添加CSRF令牌 data["CSRFToken"] = GetCSRFTokenForTemplate(r) - + // 合并额外数据 - if additionalData != nil { - for key, value := range additionalData { - data[key] = value - } + for key, value := range additionalData { + data[key] = value } - + return data } diff --git a/web/static/js/admin.js b/web/static/js/admin.js index 4809871..76886b7 100755 --- a/web/static/js/admin.js +++ b/web/static/js/admin.js @@ -297,7 +297,7 @@ loadScript(layuijs, function () { 'site-description': '站点描述:网站的简要描述,用于SEO和搜索引擎结果展示', 'site-logo': '站点Logo:网站的标志图片路径,建议使用SVG格式', // 系统配置 (settings.html) - 'maintenance-mode': '系统关闭:开启后网站将进入维护模式,普通用户无法访问', + 'maintenance-mode': '维护模式:开启后网站将进入维护模式,普通用户无法访问', 'default-user-role': '默认角色:新注册用户的默认权限级别,0为管理员,1为普通成员', 'session-timeout': '会话超时:用户登录会话的有效时间,单位为秒,超时后需要重新登录', // 页脚与备案信息 (settings.html) diff --git a/web/template/admin/apis.html b/web/template/admin/apis.html index 5bcb8a3..4430ba2 100644 --- a/web/template/admin/apis.html +++ b/web/template/admin/apis.html @@ -221,8 +221,8 @@ cols: [[ { field: 'id', title: 'ID', width: 80, sort: true }, { field: 'app_name', title: '应用名称', minWidth: 150 }, - { field: 'uuid', title: 'UUID', minWidth: 335 }, { field: 'api_type_name', title: '接口类型', minWidth: 120 }, + { field: 'uuid', title: 'UUID', minWidth: 335 }, { field: 'status_name', title: '状态', diff --git a/web/template/admin/settings.html b/web/template/admin/settings.html index 92d5b0e..44d718a 100644 --- a/web/template/admin/settings.html +++ b/web/template/admin/settings.html @@ -40,10 +40,10 @@
- +
- +
diff --git a/web/template/admin/user.html b/web/template/admin/user.html index c469981..785e60f 100644 --- a/web/template/admin/user.html +++ b/web/template/admin/user.html @@ -1,51 +1,14 @@ {{ define "user.html" }}
-

个人资料

+

账户管理

    -
  • 个人资料
  • -
  • 修改密码
  • -
  • 修改用户名
  • d +
  • 修改密码
  • +
  • 修改用户名
- -
-
-
个人资料
-
- -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
- -
-
-
- -
+
修改密码
@@ -58,7 +21,7 @@
- +
@@ -166,31 +129,22 @@ const element = layui.element // 全局变量 - let userProfile = null + let currentUsername = null - // 加载个人资料 - const loadProfile = async () => { + // 获取当前用户名 + const getCurrentUsername = async () => { try { const res = await fetch('/admin/api/user/profile') const data = await res.json() const ok = (data.success === true) || (data.code === 0) - if (!ok) throw new Error(data.message || data.msg || '加载失败') - - userProfile = data.data || {} - - // 填充个人资料表单 - const profileData = { - ...userProfile, - role: roleToText(userProfile.role), - created_at: formatTime(userProfile.created_at) - } - form.val('profileForm', profileData) + if (!ok) throw new Error(data.message || data.msg || '获取用户信息失败') + currentUsername = data.data.username // 填充用户名修改表单的当前用户名 - form.val('usernameForm', { current_username: userProfile.username }) + form.val('usernameForm', { current_username: currentUsername }) } catch (e) { - layer.msg(e.message || '加载个人资料失败', { icon: 2 }) + layer.msg(e.message || '获取用户信息失败', { icon: 2 }) } } @@ -273,7 +227,7 @@ return { ok: false, msg: '请填写新用户名和当前密码' } } - if (new_username === userProfile?.username) { + if (new_username === currentUsername) { return { ok: false, msg: '新用户名不能与当前用户名相同' } } @@ -307,14 +261,14 @@ layer.msg('用户名修改成功', { icon: 1 }) - // 重新加载个人资料 - await loadProfile() + // 重新获取当前用户名 + await getCurrentUsername() // 清空表单(不显示重置提示) form.val('usernameForm', { new_username: '', password: '', - current_username: userProfile?.username || '' + current_username: currentUsername || '' }) } catch (e) { @@ -328,7 +282,7 @@ form.val('usernameForm', { new_username: '', password: '', - current_username: userProfile?.username || '' + current_username: currentUsername || '' }) layer.msg('表单已重置', { icon: 1 }) } @@ -348,7 +302,7 @@ document.getElementById('resetUsernameBtn')?.addEventListener('click', UsernameModule.reset) // 初始化加载 - loadProfile() + getCurrentUsername() }) }) })()