mirror of
https://github.com/skyle1995/NetworkAuth.git
synced 2026-05-25 02:24:05 +08:00
New administrator authentication method
New configuration generation scheme
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,8 +1,8 @@
|
||||
/config.json
|
||||
/database.db
|
||||
/recharge.db
|
||||
logs
|
||||
模板
|
||||
.DS_Store
|
||||
networkDev
|
||||
node.txt
|
||||
config.json
|
||||
node.txt
|
||||
networkDev
|
||||
@@ -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("默认系统设置初始化失败")
|
||||
}
|
||||
|
||||
212
config/config.go
212
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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
49
config/security.go
Normal file
49
config/security.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
// 验证服务器配置
|
||||
|
||||
@@ -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,便于前端处理(如提示重新登录)
|
||||
// - 对普通页面:保持原有重定向到登录页
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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:更新时间"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: '状态',
|
||||
|
||||
@@ -40,10 +40,10 @@
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form" id="systemForm">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="maintenance-mode">关闭系统</label>
|
||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="maintenance-mode">维护模式</label>
|
||||
<div class="layui-input-block">
|
||||
<div style="display: flex; align-items: center; justify-content: flex-start; gap: 10px;">
|
||||
<input type="checkbox" name="maintenance_mode" lay-skin="switch" lay-text="关闭系统|开启系统" title="关闭系统|开启系统">
|
||||
<input type="checkbox" name="maintenance_mode" lay-skin="switch" lay-text="开启|关闭" title="开启|关闭">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,51 +1,14 @@
|
||||
{{ define "user.html" }}
|
||||
<section>
|
||||
<h2>个人资料</h2>
|
||||
<h2>账户管理</h2>
|
||||
<div class="layui-tab layui-tab-brief" lay-filter="userTabs" style="margin-top: 16px;">
|
||||
<ul class="layui-tab-title">
|
||||
<li class="layui-this">个人资料</li>
|
||||
<li>修改密码</li>
|
||||
<li>修改用户名</li>d
|
||||
<li class="layui-this">修改密码</li>
|
||||
<li>修改用户名</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
<!-- 个人资料模块 -->
|
||||
<div class="layui-tab-item layui-show">
|
||||
<div class="layui-card" style="margin-top: 16px;">
|
||||
<div class="layui-card-header">个人资料</div>
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form" id="profileForm" lay-filter="profileForm">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">UUID</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="uuid" disabled readonly class="layui-input readonly-field"
|
||||
style="font-family: monospace; font-size: 12px;" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">用户组</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="role" disabled readonly class="layui-input readonly-field" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">用户名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="username" disabled readonly class="layui-input readonly-field" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">创建时间</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="created_at" disabled readonly class="layui-input readonly-field" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改密码模块 -->
|
||||
<div class="layui-tab-item">
|
||||
<div class="layui-tab-item layui-show">
|
||||
<div class="layui-card" style="margin-top: 16px;">
|
||||
<div class="layui-card-header">修改密码</div>
|
||||
<div class="layui-card-body">
|
||||
@@ -58,7 +21,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">新密码</label>
|
||||
<label class="layui-form-label">新的密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="password" name="new_password" placeholder="请输入新密码(至少6位)" autocomplete="off"
|
||||
class="layui-input" lay-verify="required" />
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})()
|
||||
|
||||
Reference in New Issue
Block a user