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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,8 +1,8 @@
|
|||||||
/config.json
|
|
||||||
/database.db
|
/database.db
|
||||||
/recharge.db
|
/recharge.db
|
||||||
logs
|
logs
|
||||||
模板
|
模板
|
||||||
.DS_Store
|
.DS_Store
|
||||||
networkDev
|
config.json
|
||||||
node.txt
|
node.txt
|
||||||
|
networkDev
|
||||||
@@ -60,11 +60,7 @@ func runServer(cmd *cobra.Command, args []string) {
|
|||||||
if err := database.AutoMigrate(); err != nil {
|
if err := database.AutoMigrate(); err != nil {
|
||||||
logrus.WithError(err).Fatal("数据库自动迁移失败")
|
logrus.WithError(err).Fatal("数据库自动迁移失败")
|
||||||
}
|
}
|
||||||
// 初始化默认管理员账号(admin/admin123)
|
// 初始化默认系统设置(包含管理员账号)
|
||||||
if err := database.SeedDefaultAdmin(); err != nil {
|
|
||||||
logrus.WithError(err).Fatal("默认管理员初始化失败")
|
|
||||||
}
|
|
||||||
// 初始化默认系统设置
|
|
||||||
if err := database.SeedDefaultSettings(); err != nil {
|
if err := database.SeedDefaultSettings(); err != nil {
|
||||||
logrus.WithError(err).Fatal("默认系统设置初始化失败")
|
logrus.WithError(err).Fatal("默认系统设置初始化失败")
|
||||||
}
|
}
|
||||||
|
|||||||
212
config/config.go
212
config/config.go
@@ -2,7 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
_ "embed"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
@@ -11,8 +11,156 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed config.json
|
// ServerConfig 服务器配置结构体
|
||||||
var DefaultConfig string
|
// 包含服务器运行相关的配置信息
|
||||||
|
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 初始化配置文件
|
// Init 初始化配置文件
|
||||||
func Init(cfgFilePath string) {
|
func Init(cfgFilePath string) {
|
||||||
@@ -24,7 +172,31 @@ func Init(cfgFilePath string) {
|
|||||||
var pathError *fs.PathError
|
var pathError *fs.PathError
|
||||||
if errors.As(err, &pathError) {
|
if errors.As(err, &pathError) {
|
||||||
log.Warn("未找到配置文件,使用默认配置")
|
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 {
|
if err != nil {
|
||||||
log.WithFields(
|
log.WithFields(
|
||||||
log.Fields{
|
log.Fields{
|
||||||
@@ -36,16 +208,17 @@ func Init(cfgFilePath string) {
|
|||||||
log.Fields{
|
log.Fields{
|
||||||
"file": cfgFilePath,
|
"file": cfgFilePath,
|
||||||
},
|
},
|
||||||
).Info("写入默认配置文件成功")
|
).Info("写入默认配置文件成功(已生成安全密钥)")
|
||||||
}
|
}
|
||||||
// 写完默认配置后再读一次
|
|
||||||
err = viper.ReadConfig(bytes.NewBuffer([]byte(DefaultConfig)))
|
// 将配置加载到viper中
|
||||||
|
err = viper.ReadConfig(bytes.NewBuffer(configBytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(
|
log.WithFields(
|
||||||
log.Fields{
|
log.Fields{
|
||||||
"err": err,
|
"err": err,
|
||||||
},
|
},
|
||||||
).Error("读取默认配置文件失败")
|
).Error("读取默认配置失败")
|
||||||
} else {
|
} else {
|
||||||
log.Info("已成功读取默认配置")
|
log.Info("已成功读取默认配置")
|
||||||
}
|
}
|
||||||
@@ -63,8 +236,8 @@ func Init(cfgFilePath string) {
|
|||||||
},
|
},
|
||||||
).Info("使用配置文件")
|
).Info("使用配置文件")
|
||||||
|
|
||||||
// 验证配置并设置默认值
|
// 验证配置
|
||||||
if _, err := ValidateAndSetDefaults(); err != nil {
|
if _, err := ValidateConfig(); err != nil {
|
||||||
log.WithFields(
|
log.WithFields(
|
||||||
log.Fields{
|
log.Fields{
|
||||||
"err": err,
|
"err": err,
|
||||||
@@ -75,5 +248,22 @@ func Init(cfgFilePath string) {
|
|||||||
|
|
||||||
// CreateDefaultConfig 创建默认配置文件
|
// CreateDefaultConfig 创建默认配置文件
|
||||||
func CreateDefaultConfig(filePath string) error {
|
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"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServerConfig 服务器配置结构体
|
// ValidateConfig 验证配置
|
||||||
// 包含HTTP服务器的基本配置信息
|
func ValidateConfig() (*AppConfig, error) {
|
||||||
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) {
|
|
||||||
var config AppConfig
|
var config AppConfig
|
||||||
|
|
||||||
// 解析配置到结构体
|
// 解析配置到结构体
|
||||||
@@ -94,9 +22,6 @@ func ValidateAndSetDefaults() (*AppConfig, error) {
|
|||||||
return nil, fmt.Errorf("解析配置失败: %w", err)
|
return nil, fmt.Errorf("解析配置失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置默认值
|
|
||||||
setDefaults(&config)
|
|
||||||
|
|
||||||
// 验证配置
|
// 验证配置
|
||||||
if err := validateConfig(&config); err != nil {
|
if err := validateConfig(&config); err != nil {
|
||||||
return nil, fmt.Errorf("配置验证失败: %w", err)
|
return nil, fmt.Errorf("配置验证失败: %w", err)
|
||||||
@@ -106,74 +31,6 @@ func ValidateAndSetDefaults() (*AppConfig, error) {
|
|||||||
return &config, nil
|
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 验证配置
|
// validateConfig 验证配置
|
||||||
func validateConfig(config *AppConfig) error {
|
func validateConfig(config *AppConfig) error {
|
||||||
// 验证服务器配置
|
// 验证服务器配置
|
||||||
|
|||||||
@@ -98,25 +98,56 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var user models.User
|
// 通过前缀匹配一次性获取所有管理员相关设置
|
||||||
dbErr := db.Where("username = ?", body.Username).First(&user).Error
|
var adminSettings []models.Settings
|
||||||
if dbErr != nil {
|
if err = db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; err != nil {
|
||||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil)
|
utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil)
|
||||||
return
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用盐值验证密码
|
// 使用盐值验证密码
|
||||||
if !utils.VerifyPasswordWithSalt(body.Password, user.PasswordSalt, user.Password) {
|
if !utils.VerifyPasswordWithSalt(body.Password, adminPasswordSalt, adminPassword) {
|
||||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil)
|
utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建虚拟用户对象用于生成JWT令牌
|
||||||
|
adminUser := models.User{
|
||||||
|
Username: adminUsername,
|
||||||
|
Password: adminPassword,
|
||||||
|
PasswordSalt: adminPasswordSalt,
|
||||||
|
}
|
||||||
|
|
||||||
// 生成JWT令牌
|
// 生成JWT令牌
|
||||||
token, err := generateJWTToken(user)
|
token, err := generateJWTTokenForAdmin(adminUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成令牌失败", nil)
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成令牌失败", nil)
|
||||||
return
|
return
|
||||||
@@ -159,32 +190,30 @@ var jwtSecret = []byte(viper.GetString("security.jwt_secret"))
|
|||||||
|
|
||||||
// JWTClaims JWT载荷结构
|
// JWTClaims JWT载荷结构
|
||||||
type JWTClaims struct {
|
type JWTClaims struct {
|
||||||
UserUUID string `json:"user_uuid"`
|
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Role int `json:"role"`
|
IsAdmin bool `json:"is_admin"` // 是否为管理员
|
||||||
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改
|
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateJWTToken 生成JWT令牌
|
// generateJWTTokenForAdmin 生成管理员JWT令牌
|
||||||
// - 包含用户ID、用户名、角色信息
|
// - 包含管理员UUID、用户名信息
|
||||||
// - 设置24小时过期时间
|
// - 设置24小时过期时间
|
||||||
// - 使用HMAC-SHA256签名
|
// - 使用HMAC-SHA256签名
|
||||||
func generateJWTToken(user models.User) (string, error) {
|
func generateJWTTokenForAdmin(adminUser models.User) (string, error) {
|
||||||
// 生成密码哈希摘要(使用SHA256)
|
// 生成密码哈希摘要(使用SHA256)
|
||||||
passwordHashDigest := utils.GenerateSHA256Hash(user.Password)
|
passwordHashDigest := utils.GenerateSHA256Hash(adminUser.Password)
|
||||||
|
|
||||||
claims := JWTClaims{
|
claims := JWTClaims{
|
||||||
UserUUID: user.UUID,
|
Username: adminUser.Username,
|
||||||
Username: user.Username,
|
IsAdmin: true, // 管理员
|
||||||
Role: user.Role,
|
|
||||||
PasswordHash: passwordHashDigest, // 包含密码哈希摘要
|
PasswordHash: passwordHashDigest, // 包含密码哈希摘要
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||||
Issuer: "凌动技术",
|
Issuer: "凌动技术",
|
||||||
Subject: user.UUID,
|
Subject: adminUser.Username,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,39 +259,16 @@ func IsAdminAuthenticated(r *http.Request) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证用户角色(只允许管理员角色=0)
|
// 验证用户角色(只允许管理员)
|
||||||
if claims.Role != 0 {
|
if !claims.IsAdmin {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证用户是否仍然存在于数据库中
|
// 对于管理员,不需要验证数据库中的用户记录,因为管理员信息存储在settings中
|
||||||
db, err := database.GetDB()
|
// 只需要验证JWT中的信息即可
|
||||||
if err != nil {
|
if !claims.IsAdmin {
|
||||||
return false
|
fmt.Printf("[SECURITY WARNING] Invalid admin token detected - Username=%s, IP=%s\n",
|
||||||
}
|
claims.Username, r.RemoteAddr)
|
||||||
|
|
||||||
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)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,42 +292,17 @@ func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) boo
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证用户角色(只允许管理员角色=0)
|
// 验证用户角色(只允许管理员)
|
||||||
if claims.Role != 0 {
|
if !claims.IsAdmin {
|
||||||
clearInvalidJWTCookie(w)
|
clearInvalidJWTCookie(w)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证用户是否仍然存在于数据库中
|
// 对于管理员,不需要验证数据库中的用户记录,因为管理员信息存储在settings中
|
||||||
db, err := database.GetDB()
|
// 只需要验证JWT中的信息即可
|
||||||
if err != nil {
|
if !claims.IsAdmin {
|
||||||
return false
|
fmt.Printf("[SECURITY WARNING] Invalid admin token detected - Username=%s, IP=%s\n",
|
||||||
}
|
claims.Username, r.RemoteAddr)
|
||||||
|
|
||||||
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)
|
|
||||||
clearInvalidJWTCookie(w)
|
clearInvalidJWTCookie(w)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -344,39 +325,14 @@ func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
|
|||||||
return nil, fmt.Errorf("无效的会话信息")
|
return nil, fmt.Errorf("无效的会话信息")
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims.Role != 0 {
|
if !claims.IsAdmin {
|
||||||
return nil, fmt.Errorf("权限不足")
|
return nil, fmt.Errorf("权限不足")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证用户是否仍然存在于数据库中
|
// 对于管理员,不需要验证数据库中的用户记录,因为管理员信息存储在settings中
|
||||||
db, err := database.GetDB()
|
// 只需要验证JWT中的信息即可
|
||||||
if err != nil {
|
if !claims.IsAdmin {
|
||||||
return nil, fmt.Errorf("数据库连接失败")
|
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("密码已变更,请重新登录")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return claims, nil
|
return claims, nil
|
||||||
@@ -397,52 +353,25 @@ func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JW
|
|||||||
return nil, false, fmt.Errorf("无效的会话信息")
|
return nil, false, fmt.Errorf("无效的会话信息")
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims.Role != 0 {
|
if !claims.IsAdmin {
|
||||||
return nil, false, fmt.Errorf("权限不足")
|
return nil, false, fmt.Errorf("权限不足")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证用户是否仍然存在于数据库中
|
// 对于管理员,不需要验证数据库中的用户记录,因为管理员信息存储在settings中
|
||||||
db, err := database.GetDB()
|
// 只需要验证JWT中的信息即可
|
||||||
if err != nil {
|
if !claims.IsAdmin {
|
||||||
return nil, false, fmt.Errorf("数据库连接失败")
|
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("密码已变更,请重新登录")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否需要刷新令牌(根据配置的阈值)
|
// 检查是否需要刷新令牌(根据配置的阈值)
|
||||||
refreshed := false
|
refreshed := false
|
||||||
refreshThreshold := time.Duration(viper.GetInt("security.jwt_refresh_threshold_hours")) * time.Hour
|
refreshThreshold := time.Duration(viper.GetInt("security.jwt_refresh_threshold_hours")) * time.Hour
|
||||||
if time.Until(claims.ExpiresAt.Time) < refreshThreshold {
|
if time.Until(claims.ExpiresAt.Time) < refreshThreshold {
|
||||||
// 生成新的JWT令牌
|
// 为管理员生成新的JWT令牌
|
||||||
user := models.User{
|
adminUser := models.User{
|
||||||
UUID: claims.UserUUID,
|
|
||||||
Username: claims.Username,
|
Username: claims.Username,
|
||||||
Role: claims.Role,
|
|
||||||
}
|
}
|
||||||
newToken, err := generateJWTToken(user)
|
newToken, err := generateJWTTokenForAdmin(adminUser)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// 更新Cookie(使用安全配置)
|
// 更新Cookie(使用安全配置)
|
||||||
newCookie := utils.CreateSecureCookie("admin_session", newToken, utils.GetDefaultCookieMaxAge())
|
newCookie := utils.CreateSecureCookie("admin_session", newToken, utils.GetDefaultCookieMaxAge())
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package admin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"networkDev/database"
|
"networkDev/database"
|
||||||
"networkDev/models"
|
"networkDev/models"
|
||||||
@@ -16,9 +15,9 @@ func UserFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.RenderTemplate(w, "user.html", map[string]interface{}{})
|
utils.RenderTemplate(w, "user.html", map[string]interface{}{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserProfileQueryHandler 查询当前登录管理员的基本信息
|
// UserProfileQueryHandler 获取当前登录管理员的用户名
|
||||||
// - 返回 uuid/username/role/created_at 四个字段
|
// - 返回 JSON: {username}
|
||||||
// - 自动刷新接近过期的JWT令牌
|
// - 直接从JWT获取用户名信息
|
||||||
func UserProfileQueryHandler(w http.ResponseWriter, r *http.Request) {
|
func UserProfileQueryHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
@@ -31,24 +30,8 @@ func UserProfileQueryHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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{}{
|
utils.JsonResponse(w, http.StatusOK, true, "ok", map[string]interface{}{
|
||||||
"uuid": user.UUID,
|
"username": claims.Username,
|
||||||
"username": user.Username,
|
|
||||||
"role": user.Role,
|
|
||||||
"created_at": user.CreatedAt,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,21 +81,42 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确认是管理员
|
||||||
|
if !claims.IsAdmin {
|
||||||
|
utils.JsonResponse(w, http.StatusForbidden, false, "权限不足", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据库连接
|
||||||
db, err := database.GetDB()
|
db, err := database.GetDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询当前用户
|
// 通过前缀匹配一次性获取所有管理员相关设置
|
||||||
var user models.User
|
var adminSettings []models.Settings
|
||||||
if dbErr := db.Where("uuid = ?", claims.UserUUID).First(&user).Error; dbErr != nil {
|
if err = db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; err != nil {
|
||||||
utils.JsonResponse(w, http.StatusNotFound, false, "用户不存在", nil)
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "获取管理员设置失败", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验旧密码(使用盐值验证)
|
// 将设置转换为map便于查找
|
||||||
if !utils.VerifyPasswordWithSalt(body.OldPassword, user.PasswordSalt, user.Password) {
|
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)
|
utils.JsonResponse(w, http.StatusUnauthorized, false, "旧密码不正确", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -120,41 +124,34 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// 生成新的密码盐值
|
// 生成新的密码盐值
|
||||||
newSalt, err := utils.GenerateRandomSalt()
|
newSalt, err := utils.GenerateRandomSalt()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 添加详细错误日志
|
|
||||||
fmt.Printf("生成密码盐失败: %v\n", err)
|
|
||||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成密码盐失败", nil)
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成密码盐失败", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("成功生成新盐值,长度: %d\n", len(newSalt))
|
|
||||||
|
|
||||||
// 使用新盐值生成密码哈希
|
// 生成新密码哈希
|
||||||
hash, err := utils.HashPasswordWithSalt(body.NewPassword, newSalt)
|
newPasswordHash, err := utils.HashPasswordWithSalt(body.NewPassword, newSalt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 添加详细错误日志
|
|
||||||
fmt.Printf("生成密码哈希失败: %v, 密码长度: %d, 盐值长度: %d\n", err, len(body.NewPassword), len(newSalt))
|
|
||||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成密码哈希失败", nil)
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成密码哈希失败", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("成功生成密码哈希,长度: %d\n", len(hash))
|
|
||||||
|
|
||||||
// 更新密码和盐值
|
// 更新settings中的管理员密码和盐值
|
||||||
if dbErr := db.Model(&models.User{}).Where("uuid = ?", claims.UserUUID).Updates(map[string]interface{}{
|
if err = db.Model(&models.Settings{}).Where("name = ?", "admin_password").Update("value", newPasswordHash).Error; err != nil {
|
||||||
"password": hash,
|
|
||||||
"password_salt": newSalt,
|
|
||||||
}).Error; dbErr != nil {
|
|
||||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新密码失败", nil)
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新密码失败", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err = db.Model(&models.Settings{}).Where("name = ?", "admin_password_salt").Update("value", newSalt).Error; err != nil {
|
||||||
// 重新查询用户信息(包含新密码)
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新密码盐值失败", nil)
|
||||||
var updatedUser models.User
|
|
||||||
if dbErr := db.Where("uuid = ?", claims.UserUUID).First(&updatedUser).Error; dbErr != nil {
|
|
||||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "查询用户信息失败", nil)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新生成JWT令牌(包含新的密码哈希摘要)
|
// 重新生成JWT令牌(包含新的密码哈希摘要)
|
||||||
newToken, err := generateJWTToken(updatedUser)
|
adminUser := models.User{
|
||||||
|
Username: claims.Username,
|
||||||
|
Password: newPasswordHash,
|
||||||
|
PasswordSalt: newSalt,
|
||||||
|
}
|
||||||
|
newToken, err := generateJWTTokenForAdmin(adminUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil)
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil)
|
||||||
return
|
return
|
||||||
@@ -210,19 +207,45 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查唯一性:排除当前用户UUID
|
// 确认当前用户是管理员
|
||||||
var cnt int64
|
if !claims.IsAdmin {
|
||||||
if dbErr := db.Model(&models.User{}).Where("username = ? AND uuid <> ?", username, claims.UserUUID).Count(&cnt).Error; dbErr != nil {
|
utils.JsonResponse(w, http.StatusForbidden, false, "权限不足", nil)
|
||||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查用户名唯一性失败", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if cnt > 0 {
|
|
||||||
utils.JsonResponse(w, http.StatusBadRequest, false, "用户名已存在,请更换", nil)
|
|
||||||
return
|
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{}{
|
utils.JsonResponse(w, http.StatusOK, true, "保存成功", map[string]interface{}{
|
||||||
"username": username,
|
"username": username,
|
||||||
})
|
})
|
||||||
@@ -234,28 +257,27 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.JsonResponse(w, http.StatusBadRequest, false, "修改用户名需要提供当前密码", nil)
|
utils.JsonResponse(w, http.StatusBadRequest, false, "修改用户名需要提供当前密码", nil)
|
||||||
return
|
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)
|
utils.JsonResponse(w, http.StatusUnauthorized, false, "当前密码不正确", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行更新
|
// 更新管理员用户名设置
|
||||||
if dbErr := db.Model(&models.User{}).Where("uuid = ?", claims.UserUUID).Update("username", username).Error; dbErr != nil {
|
if dbErr := db.Model(&models.Settings{}).Where("name = ?", "admin_username").Update("value", username).Error; dbErr != nil {
|
||||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新用户名失败", nil)
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新管理员用户名失败", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新签发JWT并写入Cookie
|
// 重新签发JWT并写入Cookie
|
||||||
// 使用完整的用户信息(包含密码)来生成JWT令牌
|
// 创建虚拟用户对象用于生成JWT令牌
|
||||||
user.Username = username // 更新用户名
|
adminUser := models.User{
|
||||||
token, err := generateJWTToken(user)
|
Username: username, // 使用新的用户名
|
||||||
|
Password: adminPassword,
|
||||||
|
PasswordSalt: adminPasswordSalt,
|
||||||
|
}
|
||||||
|
token, err := generateJWTTokenForAdmin(adminUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil)
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil)
|
||||||
return
|
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 (
|
import (
|
||||||
"networkDev/models"
|
"networkDev/models"
|
||||||
|
"networkDev/utils"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SeedDefaultSettings 初始化默认系统设置
|
// SeedDefaultSettings 初始化默认系统设置
|
||||||
@@ -60,7 +62,23 @@ func SeedDefaultSettings() error {
|
|||||||
{
|
{
|
||||||
Name: "maintenance_mode",
|
Name: "maintenance_mode",
|
||||||
Value: "0",
|
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("默认系统设置初始化完成")
|
logrus.Info("默认系统设置初始化完成")
|
||||||
return nil
|
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 用户表模型
|
// User 用户表模型
|
||||||
// 说明:PasswordSalt 使用 32 字节随机盐(以 16 进制存储为 64 个字符),因此列长度设置为 64
|
// 说明:PasswordSalt 使用 32 字节随机盐(以 16 进制存储为 64 个字符),因此列长度设置为 64
|
||||||
|
// 注意:此表只存储普通用户,管理员账号存储在settings表中
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uint `gorm:"primaryKey;comment:用户ID,自增主键"`
|
ID uint `gorm:"primaryKey;comment:用户ID,自增主键"`
|
||||||
UUID string `gorm:"uniqueIndex;size:36;not null;comment:用户的唯一标识符" json:"uuid"`
|
UUID string `gorm:"uniqueIndex;size:36;not null;comment:用户的唯一标识符" json:"uuid"`
|
||||||
Username string `gorm:"uniqueIndex;size:64;not null;comment:用户名,唯一索引"`
|
Username string `gorm:"uniqueIndex;size:64;not null;comment:用户名,唯一索引"`
|
||||||
Password string `gorm:"size:255;not null;comment:密码哈希值"`
|
Password string `gorm:"size:255;not null;comment:密码哈希值"`
|
||||||
PasswordSalt string `gorm:"size:64;not null;comment:密码加密盐值"`
|
PasswordSalt string `gorm:"size:64;not null;comment:密码加密盐值"`
|
||||||
Role int `gorm:"not null;comment:用户角色,0=管理员,1=普通用户"`
|
|
||||||
CreatedAt time.Time `gorm:"comment:创建时间"`
|
CreatedAt time.Time `gorm:"comment:创建时间"`
|
||||||
UpdatedAt time.Time `gorm:"comment:更新时间"`
|
UpdatedAt time.Time `gorm:"comment:更新时间"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,13 +93,12 @@ func (s *SettingsService) RefreshCache() {
|
|||||||
s.loadAllSettings()
|
s.loadAllSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// GetSessionTimeout 获取会话超时时间(秒)
|
// GetSessionTimeout 获取会话超时时间(秒)
|
||||||
func (s *SettingsService) GetSessionTimeout() int {
|
func (s *SettingsService) GetSessionTimeout() int {
|
||||||
return s.GetInt("session_timeout", 3600) // 默认1小时
|
return s.GetInt("session_timeout", 3600) // 默认1小时
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsMaintenanceMode 检查系统是否关闭
|
// IsMaintenanceMode 检查是否开启维护模式
|
||||||
func (s *SettingsService) IsMaintenanceMode() bool {
|
func (s *SettingsService) IsMaintenanceMode() bool {
|
||||||
return s.GetBool("maintenance_mode", false)
|
return s.GetBool("maintenance_mode", false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,11 +65,9 @@ func GetTemplateDataWithCSRF(r *http.Request, additionalData map[string]interfac
|
|||||||
data["CSRFToken"] = GetCSRFTokenForTemplate(r)
|
data["CSRFToken"] = GetCSRFTokenForTemplate(r)
|
||||||
|
|
||||||
// 合并额外数据
|
// 合并额外数据
|
||||||
if additionalData != nil {
|
|
||||||
for key, value := range additionalData {
|
for key, value := range additionalData {
|
||||||
data[key] = value
|
data[key] = value
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ loadScript(layuijs, function () {
|
|||||||
'site-description': '站点描述:网站的简要描述,用于SEO和搜索引擎结果展示',
|
'site-description': '站点描述:网站的简要描述,用于SEO和搜索引擎结果展示',
|
||||||
'site-logo': '站点Logo:网站的标志图片路径,建议使用SVG格式',
|
'site-logo': '站点Logo:网站的标志图片路径,建议使用SVG格式',
|
||||||
// 系统配置 (settings.html)
|
// 系统配置 (settings.html)
|
||||||
'maintenance-mode': '系统关闭:开启后网站将进入维护模式,普通用户无法访问',
|
'maintenance-mode': '维护模式:开启后网站将进入维护模式,普通用户无法访问',
|
||||||
'default-user-role': '默认角色:新注册用户的默认权限级别,0为管理员,1为普通成员',
|
'default-user-role': '默认角色:新注册用户的默认权限级别,0为管理员,1为普通成员',
|
||||||
'session-timeout': '会话超时:用户登录会话的有效时间,单位为秒,超时后需要重新登录',
|
'session-timeout': '会话超时:用户登录会话的有效时间,单位为秒,超时后需要重新登录',
|
||||||
// 页脚与备案信息 (settings.html)
|
// 页脚与备案信息 (settings.html)
|
||||||
|
|||||||
@@ -221,8 +221,8 @@
|
|||||||
cols: [[
|
cols: [[
|
||||||
{ field: 'id', title: 'ID', width: 80, sort: true },
|
{ field: 'id', title: 'ID', width: 80, sort: true },
|
||||||
{ field: 'app_name', title: '应用名称', minWidth: 150 },
|
{ field: 'app_name', title: '应用名称', minWidth: 150 },
|
||||||
{ field: 'uuid', title: 'UUID', minWidth: 335 },
|
|
||||||
{ field: 'api_type_name', title: '接口类型', minWidth: 120 },
|
{ field: 'api_type_name', title: '接口类型', minWidth: 120 },
|
||||||
|
{ field: 'uuid', title: 'UUID', minWidth: 335 },
|
||||||
{
|
{
|
||||||
field: 'status_name',
|
field: 'status_name',
|
||||||
title: '状态',
|
title: '状态',
|
||||||
|
|||||||
@@ -40,10 +40,10 @@
|
|||||||
<div class="layui-card-body">
|
<div class="layui-card-body">
|
||||||
<form class="layui-form" id="systemForm">
|
<form class="layui-form" id="systemForm">
|
||||||
<div class="layui-form-item">
|
<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 class="layui-input-block">
|
||||||
<div style="display: flex; align-items: center; justify-content: flex-start; gap: 10px;">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,51 +1,14 @@
|
|||||||
{{ define "user.html" }}
|
{{ define "user.html" }}
|
||||||
<section>
|
<section>
|
||||||
<h2>个人资料</h2>
|
<h2>账户管理</h2>
|
||||||
<div class="layui-tab layui-tab-brief" lay-filter="userTabs" style="margin-top: 16px;">
|
<div class="layui-tab layui-tab-brief" lay-filter="userTabs" style="margin-top: 16px;">
|
||||||
<ul class="layui-tab-title">
|
<ul class="layui-tab-title">
|
||||||
<li class="layui-this">个人资料</li>
|
<li class="layui-this">修改密码</li>
|
||||||
<li>修改密码</li>
|
<li>修改用户名</li>
|
||||||
<li>修改用户名</li>d
|
|
||||||
</ul>
|
</ul>
|
||||||
<div class="layui-tab-content">
|
<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" style="margin-top: 16px;">
|
||||||
<div class="layui-card-header">修改密码</div>
|
<div class="layui-card-header">修改密码</div>
|
||||||
<div class="layui-card-body">
|
<div class="layui-card-body">
|
||||||
@@ -58,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layui-form-item">
|
<div class="layui-form-item">
|
||||||
<label class="layui-form-label">新密码</label>
|
<label class="layui-form-label">新的密码</label>
|
||||||
<div class="layui-input-block">
|
<div class="layui-input-block">
|
||||||
<input type="password" name="new_password" placeholder="请输入新密码(至少6位)" autocomplete="off"
|
<input type="password" name="new_password" placeholder="请输入新密码(至少6位)" autocomplete="off"
|
||||||
class="layui-input" lay-verify="required" />
|
class="layui-input" lay-verify="required" />
|
||||||
@@ -166,31 +129,22 @@
|
|||||||
const element = layui.element
|
const element = layui.element
|
||||||
|
|
||||||
// 全局变量
|
// 全局变量
|
||||||
let userProfile = null
|
let currentUsername = null
|
||||||
|
|
||||||
// 加载个人资料
|
// 获取当前用户名
|
||||||
const loadProfile = async () => {
|
const getCurrentUsername = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/admin/api/user/profile')
|
const res = await fetch('/admin/api/user/profile')
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
const ok = (data.success === true) || (data.code === 0)
|
const ok = (data.success === true) || (data.code === 0)
|
||||||
if (!ok) throw new Error(data.message || data.msg || '加载失败')
|
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)
|
|
||||||
|
|
||||||
|
currentUsername = data.data.username
|
||||||
// 填充用户名修改表单的当前用户名
|
// 填充用户名修改表单的当前用户名
|
||||||
form.val('usernameForm', { current_username: userProfile.username })
|
form.val('usernameForm', { current_username: currentUsername })
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
layer.msg(e.message || '加载个人资料失败', { icon: 2 })
|
layer.msg(e.message || '获取用户信息失败', { icon: 2 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +227,7 @@
|
|||||||
return { ok: false, msg: '请填写新用户名和当前密码' }
|
return { ok: false, msg: '请填写新用户名和当前密码' }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (new_username === userProfile?.username) {
|
if (new_username === currentUsername) {
|
||||||
return { ok: false, msg: '新用户名不能与当前用户名相同' }
|
return { ok: false, msg: '新用户名不能与当前用户名相同' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,14 +261,14 @@
|
|||||||
|
|
||||||
layer.msg('用户名修改成功', { icon: 1 })
|
layer.msg('用户名修改成功', { icon: 1 })
|
||||||
|
|
||||||
// 重新加载个人资料
|
// 重新获取当前用户名
|
||||||
await loadProfile()
|
await getCurrentUsername()
|
||||||
|
|
||||||
// 清空表单(不显示重置提示)
|
// 清空表单(不显示重置提示)
|
||||||
form.val('usernameForm', {
|
form.val('usernameForm', {
|
||||||
new_username: '',
|
new_username: '',
|
||||||
password: '',
|
password: '',
|
||||||
current_username: userProfile?.username || ''
|
current_username: currentUsername || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -328,7 +282,7 @@
|
|||||||
form.val('usernameForm', {
|
form.val('usernameForm', {
|
||||||
new_username: '',
|
new_username: '',
|
||||||
password: '',
|
password: '',
|
||||||
current_username: userProfile?.username || ''
|
current_username: currentUsername || ''
|
||||||
})
|
})
|
||||||
layer.msg('表单已重置', { icon: 1 })
|
layer.msg('表单已重置', { icon: 1 })
|
||||||
}
|
}
|
||||||
@@ -348,7 +302,7 @@
|
|||||||
document.getElementById('resetUsernameBtn')?.addEventListener('click', UsernameModule.reset)
|
document.getElementById('resetUsernameBtn')?.addEventListener('click', UsernameModule.reset)
|
||||||
|
|
||||||
// 初始化加载
|
// 初始化加载
|
||||||
loadProfile()
|
getCurrentUsername()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})()
|
})()
|
||||||
|
|||||||
Reference in New Issue
Block a user