refactor: 清理旧版兼容性,升级为 bcrypt 密码算法
- 密码哈希从 MD5+SHA1 升级为 bcrypt - 删除 super_admins/users 表中的 salt 字段 - 删除旧版升级文件(upgrade.php, check_upgrade, execute_upgrade, sql/upgrades/) - 删除 PASSWORD_SALT 配置项 - 清理所有'兼容 Python 版'注释 - 新项目独立,无历史包袱
This commit is contained in:
@@ -51,9 +51,6 @@ type Config struct {
|
||||
JWTExpireMinutes int
|
||||
JWTIdleTimeoutMinutes int
|
||||
|
||||
// 密码加密(兼容 Python 版)
|
||||
PasswordSalt string
|
||||
|
||||
// 系统管理员配置
|
||||
SuperAdminLoginPath string
|
||||
SuperAdminDefaultUser string
|
||||
@@ -98,8 +95,6 @@ func Load() (*Config, error) {
|
||||
JWTExpireMinutes: getEnvInt("JWT_EXPIRE_MINUTES", 60),
|
||||
JWTIdleTimeoutMinutes: getEnvInt("JWT_IDLE_TIMEOUT_MINUTES", 10),
|
||||
|
||||
PasswordSalt: getEnv("PASSWORD_SALT", ""),
|
||||
|
||||
SuperAdminLoginPath: getEnv("SUPER_ADMIN_LOGIN_PATH", "/super-admin"),
|
||||
SuperAdminDefaultUser: getEnv("SUPER_ADMIN_DEFAULT_USERNAME", "admin"),
|
||||
// 安全警告:默认密码仅用于首次部署初始化,上线前必须在 .env 中修改 SUPER_ADMIN_DEFAULT_PASSWORD。
|
||||
@@ -110,14 +105,11 @@ func Load() (*Config, error) {
|
||||
LogFile: getEnv("LOG_FILE", "logs/app.log"),
|
||||
}
|
||||
|
||||
// 校验必填项
|
||||
// 校验必填项
|
||||
if cfg.JWTSecretKey == "" {
|
||||
return nil, fmt.Errorf("配置 JWT_SECRET_KEY 不能为空")
|
||||
}
|
||||
if cfg.PasswordSalt == "" {
|
||||
return nil, fmt.Errorf("配置 PASSWORD_SALT 不能为空")
|
||||
}
|
||||
|
||||
AppConfig = cfg
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -15,15 +15,14 @@ import "time"
|
||||
|
||||
// SuperAdmin 超级管理员模型,对应 super_admins 表
|
||||
type SuperAdmin struct {
|
||||
ID int `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Username string `gorm:"column:username;type:varchar(50);uniqueIndex;not null" json:"username"`
|
||||
PasswordHash string `gorm:"column:password_hash;type:varchar(255);not null" json:"-"`
|
||||
Salt string `gorm:"column:salt;type:varchar(64);not null" json:"-"`
|
||||
RealName string `gorm:"column:real_name;type:varchar(50);not null" json:"real_name"`
|
||||
Status int8 `gorm:"column:status;default:1" json:"status"`
|
||||
NeedChangePassword int8 `gorm:"column:need_change_password;default:1" json:"need_change_password"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
|
||||
ID int `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Username string `gorm:"column:username;type:varchar(50);uniqueIndex;not null" json:"username"`
|
||||
PasswordHash string `gorm:"column:password_hash;type:varchar(60);not null" json:"-"`
|
||||
RealName string `gorm:"column:real_name;type:varchar(50);not null" json:"real_name"`
|
||||
Status int8 `gorm:"column:status;default:1" json:"status"`
|
||||
NeedChangePassword int8 `gorm:"column:need_change_password;default:1" json:"need_change_password"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
@@ -17,7 +17,7 @@ import "time"
|
||||
type User struct {
|
||||
UserID int `gorm:"column:user_id;primaryKey;autoIncrement" json:"user_id"`
|
||||
Username string `gorm:"column:username;type:varchar(50);uniqueIndex;not null" json:"username"`
|
||||
PasswordHash string `gorm:"column:password_hash;type:varchar(255);not null" json:"-"`
|
||||
PasswordHash string `gorm:"column:password_hash;type:varchar(60);not null" json:"-"`
|
||||
RealName string `gorm:"column:real_name;type:varchar(50);not null" json:"real_name"`
|
||||
UserType string `gorm:"column:user_type;type:enum('student','parent','admin','super_admin');not null" json:"user_type"`
|
||||
StudentID *int `gorm:"column:student_id" json:"student_id"`
|
||||
|
||||
@@ -54,20 +54,12 @@ func (r *SuperAdminRepo) Create(admin *model.SuperAdmin) (int, error) {
|
||||
return admin.ID, nil
|
||||
}
|
||||
|
||||
// UpdatePassword 更新超级管理员密码
|
||||
// UpdatePassword 更新超级管理员密码并清除强制改密标记
|
||||
func (r *SuperAdminRepo) UpdatePassword(id int, passwordHash string) error {
|
||||
return r.db.Model(&model.SuperAdmin{}).
|
||||
Where("id = ?", id).
|
||||
Update("password_hash", passwordHash).Error
|
||||
}
|
||||
|
||||
// UpdatePasswordWithSalt 更新超级管理员密码和盐值,并清除强制改密标记
|
||||
func (r *SuperAdminRepo) UpdatePasswordWithSalt(id int, passwordHash, salt string) error {
|
||||
return r.db.Model(&model.SuperAdmin{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"password_hash": passwordHash,
|
||||
"salt": salt,
|
||||
"password_hash": passwordHash,
|
||||
"need_change_password": 0,
|
||||
}).Error
|
||||
}
|
||||
@@ -98,11 +90,10 @@ func (r *SuperAdminRepo) UpdateStatus(id int, status int8) error {
|
||||
}
|
||||
|
||||
// EnsureDefaultAdmin 确保默认超级管理员存在(使用 INSERT IGNORE 避免并发竞态)
|
||||
func (r *SuperAdminRepo) EnsureDefaultAdmin(username, passwordHash, salt, realName string) error {
|
||||
func (r *SuperAdminRepo) EnsureDefaultAdmin(username, passwordHash, realName string) error {
|
||||
admin := model.SuperAdmin{
|
||||
Username: username,
|
||||
PasswordHash: passwordHash,
|
||||
Salt: salt,
|
||||
RealName: realName,
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/config"
|
||||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model"
|
||||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/repository"
|
||||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/crypto"
|
||||
@@ -100,10 +99,17 @@ func (s *AdminService) getInitialPassword(classID int) (string, error) {
|
||||
return pwd, nil
|
||||
}
|
||||
|
||||
// hashPassword 对密码进行 bcrypt 哈希,失败时 panic(不应发生)
|
||||
func hashPasswordOrPanic(password string) string {
|
||||
hash, err := crypto.HashPassword(password)
|
||||
if err != nil {
|
||||
logger.Sugared.Fatalf("密码哈希失败: %v", err)
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
// AddStudent 新增学生
|
||||
func (s *AdminService) AddStudent(studentNo, name string, parentAccount *string, classID int, dormitoryNumber *string) (map[string]interface{}, error) {
|
||||
cfg := config.AppConfig
|
||||
|
||||
// 校验宿舍号格式
|
||||
if !validateDormitoryNumber(dormitoryNumber) {
|
||||
return map[string]interface{}{"success": false, "message": "宿舍号格式不正确,应为如 东1-101 的格式"}, nil
|
||||
@@ -134,7 +140,7 @@ func (s *AdminService) AddStudent(studentNo, name string, parentAccount *string,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
passwordHash := crypto.HashPassword(defaultPassword, cfg.PasswordSalt)
|
||||
passwordHash := hashPasswordOrPanic(defaultPassword)
|
||||
_, err = s.userRepo.CreateStudent(studentNo, passwordHash, name, studentID)
|
||||
if err != nil {
|
||||
logger.Sugared.Errorf("创建学生登录账号失败: student_no=%s, student_id=%d, err=%v", studentNo, studentID, err)
|
||||
@@ -147,7 +153,7 @@ func (s *AdminService) AddStudent(studentNo, name string, parentAccount *string,
|
||||
if parentAccount != nil && *parentAccount != "" {
|
||||
exists, _ := s.userRepo.CheckUsernameExists(*parentAccount)
|
||||
if !exists {
|
||||
parentHash := crypto.HashPassword(defaultPassword, cfg.PasswordSalt)
|
||||
parentHash := hashPasswordOrPanic(defaultPassword)
|
||||
parentRealName := fmt.Sprintf("%s家长", name)
|
||||
if _, err := s.userRepo.CreateParent(*parentAccount, parentHash, parentRealName, studentID); err != nil {
|
||||
logger.Sugared.Warnf("创建家长账号失败(学生记录已保留): parent_account=%s, student_id=%d, err=%v", *parentAccount, studentID, err)
|
||||
@@ -167,7 +173,6 @@ func (s *AdminService) AddStudent(studentNo, name string, parentAccount *string,
|
||||
// 未使用数据库事务的原因:Repository 层未暴露事务接口,全量事务包裹需要较大重构;
|
||||
// 且批量导入场景下允许部分成功是合理的业务权衡(用户可修正失败记录后重新导入)。
|
||||
func (s *AdminService) ImportStudents(students []map[string]interface{}, classID int) (map[string]interface{}, error) {
|
||||
cfg := config.AppConfig
|
||||
successCount := 0
|
||||
failedCount := 0
|
||||
var details []map[string]interface{}
|
||||
@@ -262,7 +267,7 @@ func (s *AdminService) ImportStudents(students []map[string]interface{}, classID
|
||||
existingSet[studentNo] = true
|
||||
|
||||
// 创建学生登录账号
|
||||
passwordHash := crypto.HashPassword(password, cfg.PasswordSalt)
|
||||
passwordHash := hashPasswordOrPanic(password)
|
||||
if _, err := s.userRepo.CreateStudent(studentNo, passwordHash, name, studentID); err != nil {
|
||||
logger.Sugared.Errorf("批量导入-创建学生登录账号失败: student_no=%s, student_id=%d, err=%v", studentNo, studentID, err)
|
||||
// 回滚学生记录
|
||||
@@ -277,7 +282,7 @@ func (s *AdminService) ImportStudents(students []map[string]interface{}, classID
|
||||
|
||||
// 创建家长账号
|
||||
if parentAccount != nil && *parentAccount != "" && !usernameSet[*parentAccount] {
|
||||
parentHash := crypto.HashPassword(password, cfg.PasswordSalt)
|
||||
parentHash := hashPasswordOrPanic(password)
|
||||
parentRealName := fmt.Sprintf("%s家长", name)
|
||||
if _, err := s.userRepo.CreateParent(*parentAccount, parentHash, parentRealName, studentID); err != nil {
|
||||
logger.Sugared.Errorf("批量导入-创建家长账号失败: parent_account=%s, student_id=%d, err=%v", *parentAccount, studentID, err)
|
||||
@@ -346,7 +351,6 @@ func (s *AdminService) ResetStudentPassword(studentID int, newPassword string) e
|
||||
if valid, msg := crypto.ValidatePasswordStrength(newPassword); !valid {
|
||||
return fmt.Errorf("%s", msg)
|
||||
}
|
||||
cfg := config.AppConfig
|
||||
student, err := s.studentRepo.GetByID(studentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("学生不存在")
|
||||
@@ -356,14 +360,15 @@ func (s *AdminService) ResetStudentPassword(studentID int, newPassword string) e
|
||||
if err != nil {
|
||||
return fmt.Errorf("学生登录账号不存在")
|
||||
}
|
||||
passwordHash := crypto.HashPassword(newPassword, cfg.PasswordSalt)
|
||||
passwordHash, err := crypto.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("密码加密失败")
|
||||
}
|
||||
return s.userRepo.UpdatePassword(user.UserID, passwordHash)
|
||||
}
|
||||
|
||||
// AddAdmin 添加管理员
|
||||
func (s *AdminService) AddAdmin(username, realName, password, roleType string, classID int, subjectID *int) (map[string]interface{}, error) {
|
||||
cfg := config.AppConfig
|
||||
|
||||
exists, _ := s.userRepo.CheckUsernameExists(username)
|
||||
if exists {
|
||||
return map[string]interface{}{"success": false, "message": "用户名已存在"}, nil
|
||||
@@ -377,7 +382,10 @@ func (s *AdminService) AddAdmin(username, realName, password, roleType string, c
|
||||
password = pwd
|
||||
}
|
||||
|
||||
passwordHash := crypto.HashPassword(password, cfg.PasswordSalt)
|
||||
passwordHash, err := crypto.HashPassword(password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("密码加密失败: %w", err)
|
||||
}
|
||||
userID, err := s.userRepo.CreateAdmin(username, passwordHash, realName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -436,8 +444,10 @@ func (s *AdminService) ResetAdminPassword(userID int, newPassword string) error
|
||||
if valid, msg := crypto.ValidatePasswordStrength(newPassword); !valid {
|
||||
return fmt.Errorf("%s", msg)
|
||||
}
|
||||
cfg := config.AppConfig
|
||||
passwordHash := crypto.HashPassword(newPassword, cfg.PasswordSalt)
|
||||
passwordHash, err := crypto.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("密码加密失败")
|
||||
}
|
||||
return s.userRepo.UpdatePassword(userID, passwordHash)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,13 @@ import (
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/config"
|
||||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model"
|
||||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/repository"
|
||||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/crypto"
|
||||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/database"
|
||||
appJwt "hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/jwt"
|
||||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/logger"
|
||||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/config"
|
||||
)
|
||||
|
||||
// AuthService 认证服务
|
||||
@@ -124,10 +124,8 @@ func (s *AuthService) Login(username, password, ip, userAgent string) *LoginResu
|
||||
return s.tryParentLogin(username, password, ip, userAgent, cfg, attemptsKey, ipAttemptsKey)
|
||||
}
|
||||
|
||||
// 验证密码(使用全局 PASSWORD_SALT,与 Python 版兼容。
|
||||
// 已知设计局限:全局共享盐值,若泄露则所有普通用户密码面临风险。
|
||||
// 后续迁移计划:为每个用户生成独立盐值,存储在 users 表中。)
|
||||
if !crypto.VerifyPassword(password, user.PasswordHash, cfg.PasswordSalt) {
|
||||
// 验证密码(bcrypt)
|
||||
if !crypto.VerifyPassword(password, user.PasswordHash) {
|
||||
s.logService.WriteLoginLog(username, 0, ip, userAgent, "用户名或密码错误")
|
||||
return &LoginResult{Success: false, Message: "用户名或密码错误"}
|
||||
}
|
||||
@@ -222,7 +220,7 @@ func (s *AuthService) loginAsStudent(student *model.Student, password, ip, userA
|
||||
return &LoginResult{Success: false, Message: "用户名或密码错误"}
|
||||
}
|
||||
|
||||
if !crypto.VerifyPassword(password, user.PasswordHash, cfg.PasswordSalt) {
|
||||
if !crypto.VerifyPassword(password, user.PasswordHash) {
|
||||
return &LoginResult{Success: false, Message: "用户名或密码错误"}
|
||||
}
|
||||
|
||||
@@ -288,7 +286,7 @@ func (s *AuthService) tryParentLogin(username, password, ip, userAgent string, c
|
||||
return &LoginResult{Success: false, Message: "用户名或密码错误"}
|
||||
}
|
||||
|
||||
if !crypto.VerifyPassword(password, user.PasswordHash, cfg.PasswordSalt) {
|
||||
if !crypto.VerifyPassword(password, user.PasswordHash) {
|
||||
return &LoginResult{Success: false, Message: "用户名或密码错误"}
|
||||
}
|
||||
|
||||
@@ -340,8 +338,6 @@ func (s *AuthService) Logout(userID int) error {
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (s *AuthService) ChangePassword(userID int, oldPassword, newPassword string, force bool) error {
|
||||
cfg := config.AppConfig
|
||||
|
||||
user, err := s.userRepo.GetByUserID(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("用户不存在")
|
||||
@@ -349,7 +345,7 @@ func (s *AuthService) ChangePassword(userID int, oldPassword, newPassword string
|
||||
|
||||
// 验证原密码(强制改密时跳过)
|
||||
if !force {
|
||||
if !crypto.VerifyPassword(oldPassword, user.PasswordHash, cfg.PasswordSalt) {
|
||||
if !crypto.VerifyPassword(oldPassword, user.PasswordHash) {
|
||||
return fmt.Errorf("原密码错误")
|
||||
}
|
||||
}
|
||||
@@ -360,7 +356,10 @@ func (s *AuthService) ChangePassword(userID int, oldPassword, newPassword string
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
newHash := crypto.HashPassword(newPassword, cfg.PasswordSalt)
|
||||
newHash, err := crypto.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("密码加密失败")
|
||||
}
|
||||
if err := s.userRepo.UpdatePassword(userID, newHash); err != nil {
|
||||
return fmt.Errorf("密码修改失败")
|
||||
}
|
||||
@@ -458,4 +457,3 @@ func getPasswordChangePath(userType string) string {
|
||||
return "/"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,16 +40,13 @@ func (s *SuperAdminService) EnsureDefaultAdmin() error {
|
||||
|
||||
logger.Sugared.Warnf("⚠️ 当前使用默认超级管理员密码,部署环境请务必修改 SUPER_ADMIN_DEFAULT_PASSWORD 并重启服务")
|
||||
|
||||
// 为超级管理员生成独立的随机 Salt
|
||||
salt, err := crypto.GenerateRandomPassword(16)
|
||||
passwordHash, err := crypto.HashPassword(cfg.SuperAdminDefaultPass)
|
||||
if err != nil {
|
||||
return fmt.Errorf("生成随机盐值失败: %w", err)
|
||||
return fmt.Errorf("密码哈希失败: %w", err)
|
||||
}
|
||||
passwordHash := crypto.HashPassword(cfg.SuperAdminDefaultPass, salt)
|
||||
if err := s.superAdminRepo.EnsureDefaultAdmin(
|
||||
cfg.SuperAdminDefaultUser,
|
||||
passwordHash,
|
||||
salt,
|
||||
"系统管理员",
|
||||
); err != nil {
|
||||
return fmt.Errorf("创建默认超级管理员失败: %w", err)
|
||||
@@ -82,7 +79,7 @@ func (s *SuperAdminService) Login(username, password, ip, userAgent string) (map
|
||||
return map[string]interface{}{"success": false, "message": "用户名或密码错误"}, nil
|
||||
}
|
||||
|
||||
if !crypto.VerifyPassword(password, admin.PasswordHash, admin.Salt) {
|
||||
if !crypto.VerifyPassword(password, admin.PasswordHash) {
|
||||
s.logService.WriteLoginLog(username, 0, ip, userAgent, "用户名或密码错误")
|
||||
return map[string]interface{}{"success": false, "message": "用户名或密码错误"}, nil
|
||||
}
|
||||
@@ -120,7 +117,7 @@ func (s *SuperAdminService) Login(username, password, ip, userAgent string) (map
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ChangePassword 超级管理员修改密码(操作 super_admins 表,使用独立 salt)
|
||||
// ChangePassword 超级管理员修改密码
|
||||
func (s *SuperAdminService) ChangePassword(adminID int, oldPassword, newPassword string, force bool) error {
|
||||
admin, err := s.superAdminRepo.GetByID(adminID)
|
||||
if err != nil {
|
||||
@@ -129,7 +126,7 @@ func (s *SuperAdminService) ChangePassword(adminID int, oldPassword, newPassword
|
||||
|
||||
// 验证原密码(强制改密时跳过)
|
||||
if !force {
|
||||
if !crypto.VerifyPassword(oldPassword, admin.PasswordHash, admin.Salt) {
|
||||
if !crypto.VerifyPassword(oldPassword, admin.PasswordHash) {
|
||||
return fmt.Errorf("原密码错误")
|
||||
}
|
||||
}
|
||||
@@ -139,14 +136,12 @@ func (s *SuperAdminService) ChangePassword(adminID int, oldPassword, newPassword
|
||||
return fmt.Errorf("%s", msg)
|
||||
}
|
||||
|
||||
// 生成新的独立 salt
|
||||
newSalt, err := crypto.GenerateRandomPassword(16)
|
||||
newHash, err := crypto.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("生成随机盐值失败: %w", err)
|
||||
return fmt.Errorf("密码加密失败: %w", err)
|
||||
}
|
||||
newHash := crypto.HashPassword(newPassword, newSalt)
|
||||
|
||||
if err := s.superAdminRepo.UpdatePasswordWithSalt(adminID, newHash, newSalt); err != nil {
|
||||
if err := s.superAdminRepo.UpdatePassword(adminID, newHash); err != nil {
|
||||
return fmt.Errorf("密码修改失败")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user