Files
mce/internal/service/auth_password.go
Victor_Jay 97adf54d6d fix: golangci-lint 零告警通过 (57→0) + gofumpt/goimports 全量格式化
## CI 修复 (P0/P1)

P0 — 编译阻塞:
  - interfaces.go: postUseCase ISP 接口移除不用的 Create/Update/Delete

P1 — 必须修复:
  - ST1000: 为 13 个包添加包注释 (common/config/model/service/...)
  - ST1005: redis_store.go 全部错误消息改为小写开头
  - errcheck (19处): defer Close()→闭包忽略, notifier.Create→_=, r.Run→检查error
  - errorlint (9处): switch-on-error→errors.Is 链, ==→errors.Is
  - ST1020/ST1022: 导出符号注释以符号名开头

P2 — 安全评审:
  - gosec (12处): G203/G301/G304/G306 添加 nolint 注释并附理由

P3 — 清理:
  - unused: 移除 hasUnicode/energyStore/current/energyAdminUseCase
  - gofumpt + goimports 全量格式化 (35+ 文件)
2026-06-22 02:27:35 +08:00

278 lines
6.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"regexp"
"strings"
"time"
"metazone.cc/mce/internal/common"
"metazone.cc/mce/internal/model"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
var (
pwLower = regexp.MustCompile(`[a-z]`)
pwUpper = regexp.MustCompile(`[A-Z]`)
pwDigit = regexp.MustCompile(`\d`)
pwSymbol = regexp.MustCompile(`[^a-zA-Z0-9]`)
)
// 键盘水平序列QWERTY 布局)
var keyboardHoriz = []string{
"qwertyuiop", "asdfghjkl", "zxcvbnm",
"poiuytrewq", "lkjhgfdsa", "mnbvcxz",
}
// 键盘垂直序列
var keyboardVert = []string{
"qaz", "wsx", "edc", "rfv", "tgb", "yhn", "ujm", "ik", "ol", "p",
"zaq", "xsw", "cde", "vfr", "bgt", "nhy", "mju", "ki", "lo",
}
// dummyHash 防时序攻击:当用户不存在时,仍对虚拟哈希执行完整的 bcrypt 比对
var dummyHash []byte
func init() {
hash, err := bcrypt.GenerateFromPassword([]byte("metazone-dummy-hash-2026"), 12)
if err != nil {
panic("failed to generate bcrypt dummy hash: " + err.Error())
}
dummyHash = hash
}
// validatePassword 多级密码强度校验
func validatePassword(pw string, level string) error {
if level == "" {
level = "medium"
}
if len(pw) < 8 {
return common.ErrWeakPassword
}
switch level {
case "low":
return nil
case "medium":
if !hasAtLeastN(pw, 2) {
return common.ErrWeakPassword
}
case "high":
if !hasAtLeastN(pw, 3) {
return common.ErrWeakPassword
}
if isWeakSequence(pw) {
return common.ErrWeakPassword
}
case "very_high":
if !hasAtLeastN(pw, 4) {
return common.ErrWeakPassword
}
if isWeakSequence(pw) {
return common.ErrWeakPassword
}
}
return nil
}
// hasAtLeastN 检查密码包含至少 n 类字符(小写/大写/数字/符号)
func hasAtLeastN(pw string, n int) bool {
count := 0
if pwLower.MatchString(pw) {
count++
}
if pwUpper.MatchString(pw) {
count++
}
if pwDigit.MatchString(pw) {
count++
}
if pwSymbol.MatchString(pw) {
count++
}
return count >= n
}
// isWeakSequence 检查连续字符≥3 重复/排序/键盘序列)
func isWeakSequence(pw string) bool {
runes := []rune(strings.ToLower(pw))
if len(runes) < 3 {
return false
}
// 重复字符 aaa 111
repeatCount := 1
// 升序/降序计数
ascCount, descCount := 1, 1
for i := 1; i < len(runes); i++ {
// 重复
if runes[i] == runes[i-1] {
repeatCount++
if repeatCount >= 3 {
return true
}
} else {
repeatCount = 1
}
// 升序 abc / 123
if runes[i] == runes[i-1]+1 {
ascCount++
if ascCount >= 3 {
return true
}
} else {
ascCount = 1
}
// 降序 cba / 321
if runes[i] == runes[i-1]-1 {
descCount++
if descCount >= 3 {
return true
}
} else {
descCount = 1
}
}
// 键盘序列
lower := strings.ToLower(pw)
for _, seq := range keyboardHoriz {
if len(seq) >= 3 {
for i := 0; i <= len(seq)-3; i++ {
if strings.Contains(lower, seq[i:i+3]) {
return true
}
}
}
}
for _, seq := range keyboardVert {
if len(seq) >= 3 && strings.Contains(lower, seq) {
return true
}
}
return false
}
// PasswordStrengthLevel 获取当前密码强度级别
func PasswordStrengthLevel(strengthStr string) string {
switch strengthStr {
case "low", "medium", "high", "very_high":
return strengthStr
}
return "medium"
}
// PasswordStrengthHint 前端校验提示文案
func PasswordStrengthHint(level string) string {
switch level {
case "low":
return "密码至少 8 位"
case "medium":
return "密码至少 8 位,需包含字母、数字或符号中的两类"
case "high":
return "密码至少 8 位,需包含大小写字母、数字和符号中的至少三类,且不含连续字符"
case "very_high":
return "密码至少 8 位,需包含大小写字母、数字和符号全部四类,且不含连续字符"
}
return "密码至少 8 位"
}
// ChangePassword 修改密码
// 流程:验证当前密码 → 校验新密码强度 → 哈希 → 更新 → 删除所有 session强制重新登录
func (s *AuthService) ChangePassword(userID uint, currentPassword, newPassword string) error {
user, err := s.userRepo.FindByID(userID)
if err != nil {
return common.ErrUserNotFound
}
if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(currentPassword)) != nil {
return common.ErrIncorrectPassword
}
if err := validatePassword(newPassword, s.siteSettings.PasswordStrength()); err != nil {
return err
}
hash, err := common.HashPassword(newPassword, s.cfg.Bcrypt.Cost)
if err != nil {
return err
}
user.PasswordHash = hash
if err := s.userRepo.Update(user); err != nil {
return err
}
// 删除所有 session强制所有设备重新登录
return s.invalidateSessions(userID)
}
// DeleteAccount 用户自主注销
func (s *AuthService) DeleteAccount(userID uint, password, reason string) error {
user, err := s.userRepo.FindByID(userID)
if err != nil {
return common.ErrUserNotFound
}
if user.Role == model.RoleOwner {
return common.ErrOwnerCannotDelete
}
if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) != nil {
return common.ErrIncorrectPassword
}
user.Status = model.StatusDeleted
user.DeleteReason = reason
if err := s.userRepo.Update(user); err != nil {
return err
}
return s.invalidateSessions(userID)
}
// invalidateSessions 销毁某用户的所有 session内部方法
func (s *AuthService) invalidateSessions(userID uint) error {
return s.sessionManager.DestroyByUID(userID)
}
// InvalidateSessions 公开方法:销毁用户所有 session供 admin 等外部调用)
func (s *AuthService) InvalidateSessions(userID uint) error {
return s.sessionManager.DestroyByUID(userID)
}
// ConfirmRestore 二次确认恢复已注销账号
func (s *AuthService) ConfirmRestore(req model.LoginRequest, loginIP string) (*model.User, error) {
user, err := s.userRepo.FindByEmail(req.Email)
if err != nil {
_ = bcrypt.CompareHashAndPassword(dummyHash, []byte(req.Password))
return nil, common.ErrInvalidCred
}
if user.Status != model.StatusDeleted {
return nil, common.ErrUserNotFound
}
if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)) != nil {
return nil, common.ErrInvalidCred
}
// 恢复账号
user.Status = model.StatusActive
user.DeletedAt = gorm.DeletedAt{}
now := time.Now()
user.LastLoginIP = loginIP
user.LastLoginAt = &now
if err := s.userRepo.Update(user); err != nil {
return nil, err
}
return user, nil
}