package service import ( "fmt" "strings" "sync" "time" "unicode" "gorm.io/gorm" "filestoragesystem/internal/config" "filestoragesystem/internal/model" "filestoragesystem/internal/repository" "filestoragesystem/internal/utils" "filestoragesystem/pkg/apperr" ) const ( maxLoginFails = 5 // 最大登录失败次数 lockDuration = 15 * time.Minute // 锁定时长 signTimeWindow = 5 * time.Minute // 签名时间戳有效窗口 ) // AuthService 认证服务 type AuthService struct { cfg *config.Config userRepo *repository.UserRepo apiKeyRepo *repository.APIKeyRepo roleRepo *repository.RoleRepo settingRepo *repository.SettingRepo opLogRepo *repository.OpLogRepo mu sync.Mutex loginFails map[string]*loginState // 登录失败状态 nonces map[string]time.Time // 防重放nonce } type loginState struct { fails int lockedUntil time.Time } // ensureState 惰性初始化登录失败与nonce缓存 func (s *AuthService) ensureState() { s.mu.Lock() defer s.mu.Unlock() if s.loginFails == nil { s.loginFails = make(map[string]*loginState) } if s.nonces == nil { s.nonces = make(map[string]time.Time) } } // Register 用户注册 func (s *AuthService) Register(username, email, password string) (*model.User, error) { // 注册开关 if v, err := s.settingRepo.GetValue("register_enabled"); err == nil && v != "true" { return nil, apperr.ErrRegisterClosed } username = strings.TrimSpace(username) email = strings.TrimSpace(strings.ToLower(email)) if len(username) < 3 || len(username) > 50 { return nil, fmt.Errorf("用户名长度需在3-50之间") } for _, ch := range username { if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' && ch != '-' && ch < 0x80 { return nil, fmt.Errorf("用户名仅支持字母、数字、下划线和横线") } } if !utils.IsEmail(email) { return nil, fmt.Errorf("邮箱格式不正确") } if !utils.IsValidPassword(password) { return nil, fmt.Errorf("密码至少8位且需包含字母和数字") } if _, err := s.userRepo.FindByUsername(username); err == nil { return nil, apperr.ErrUserExists } if _, err := s.userRepo.FindByEmail(email); err == nil { return nil, apperr.ErrUserExists } // 默认角色与配额 role, err := s.roleRepo.FindByCode("user") if err != nil { return nil, fmt.Errorf("默认角色不存在: %w", err) } limit := int64(10737418240) if v, err := s.settingRepo.GetValue("default_storage_limit"); err == nil { var n int64 if _, err := fmt.Sscanf(v, "%d", &n); err == nil && n > 0 { limit = n } } hash, err := utils.HashPassword(password) if err != nil { return nil, err } u := &model.User{ Username: username, Email: email, PasswordHash: hash, RoleID: role.ID, Status: 1, StorageLimit: limit, } if err := s.userRepo.Create(u); err != nil { return nil, err } utils.Info("auth", "新用户注册: %s", username) return u, nil } // Login 用户登录,成功返回用户与JWT token func (s *AuthService) Login(account, password, ip string) (*model.User, string, error) { s.ensureState() account = strings.TrimSpace(account) // 锁定检查 s.mu.Lock() if st, ok := s.loginFails[account]; ok { if time.Now().Before(st.lockedUntil) { s.mu.Unlock() return nil, "", apperr.ErrAccountLocked } } s.mu.Unlock() u, err := s.userRepo.FindByUsernameOrEmail(account) if err != nil { s.recordLoginFail(account) return nil, "", apperr.ErrPasswordWrong } if u.Status != 1 { return nil, "", apperr.ErrUserDisabled } if !utils.CheckPassword(u.PasswordHash, password) { s.recordLoginFail(account) return nil, "", apperr.ErrPasswordWrong } // 登录成功:清除失败计数 s.mu.Lock() delete(s.loginFails, account) s.mu.Unlock() now := time.Now() _ = s.userRepo.UpdateFields(u.ID, map[string]interface{}{ "last_login_at": now, "last_login_ip": ip, }) token, err := utils.GenerateToken(u.ID, u.Username, roleCodeOf(u)) if err != nil { return nil, "", err } return u, token, nil } func (s *AuthService) recordLoginFail(account string) { s.mu.Lock() defer s.mu.Unlock() if s.loginFails == nil { s.loginFails = make(map[string]*loginState) } st, ok := s.loginFails[account] if !ok { st = &loginState{} s.loginFails[account] = st } st.fails++ if st.fails >= maxLoginFails { st.lockedUntil = time.Now().Add(lockDuration) st.fails = 0 utils.Warn("auth", "账户 %s 因连续登录失败被锁定%d分钟", account, int(lockDuration.Minutes())) } } // roleCodeOf 用户角色编码(内存缓存于User.Role) func roleCodeOf(u *model.User) string { if u.Role != nil { return u.Role.Code } return "" } // CreateAPIKey 为用户生成API密钥,secret仅此一次返回 func (s *AuthService) CreateAPIKey(userID uint, name string) (*model.APIKey, string, error) { u, err := s.userRepo.FindByID(userID) if err != nil { return nil, "", apperr.ErrNotFound } key := &model.APIKey{ UserID: userID, AccessKey: utils.RandomKey(32), SecretKey: utils.RandomKey(64), Name: name, Status: 1, } if key.Name == "" { key.Name = u.Username + "的密钥" } if err := s.apiKeyRepo.Create(key); err != nil { return nil, "", err } utils.Info("auth", "用户 %s 创建API密钥: %s", u.Username, key.AccessKey) return key, key.SecretKey, nil } // ListAPIKeys 用户密钥列表 func (s *AuthService) ListAPIKeys(userID uint) ([]model.APIKey, error) { return s.apiKeyRepo.ListByUser(userID) } // DeleteAPIKey 删除密钥 func (s *AuthService) DeleteAPIKey(userID, id uint) error { if err := s.apiKeyRepo.Delete(id, userID); err != nil { return apperr.ErrNotFound } return nil } // VerifyAPIKeySignature 校验API Key HMAC-SHA256签名,返回所属用户 // 签名串: accessKey + "\n" + timestamp + "\n" + nonce + "\n" + method + "\n" + path func (s *AuthService) VerifyAPIKeySignature(accessKey, timestamp, nonce, signature, method, path string) (*model.User, error) { s.ensureState() // 时间窗口 var ts int64 if _, err := fmt.Sscanf(timestamp, "%d", &ts); err != nil { return nil, apperr.ErrSignInvalid } t := time.Unix(ts, 0) if diff := time.Since(t); diff > signTimeWindow || diff < -signTimeWindow { return nil, apperr.ErrSignExpired } // 防重放 s.mu.Lock() if s.nonces == nil { s.nonces = make(map[string]time.Time) } if _, used := s.nonces[nonce]; used { s.mu.Unlock() return nil, apperr.ErrSignInvalid } s.nonces[nonce] = time.Now() // 顺带清理过期nonce if len(s.nonces) > 10000 { cutoff := time.Now().Add(-signTimeWindow * 2) for k, v := range s.nonces { if v.Before(cutoff) { delete(s.nonces, k) } } } s.mu.Unlock() key, err := s.apiKeyRepo.FindByAccessKey(accessKey) if err != nil { return nil, apperr.ErrSignInvalid } if key.Status != 1 { return nil, apperr.ErrForbidden } message := strings.Join([]string{accessKey, timestamp, nonce, method, path}, "\n") expect := utils.HMACSHA256(key.SecretKey, message) if !strings.EqualFold(expect, signature) { return nil, apperr.ErrSignInvalid } _ = s.apiKeyRepo.Touch(key.ID) u, err := s.userRepo.FindByID(key.UserID) if err != nil { return nil, apperr.ErrNotFound } if u.Status != 1 { return nil, apperr.ErrUserDisabled } return u, nil } // LoadUserByID 加载用户(含角色权限) func (s *AuthService) LoadUserByID(id uint) (*model.User, error) { u, err := s.userRepo.FindByID(id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, apperr.ErrNotFound } return nil, err } if u.Status != 1 { return nil, apperr.ErrUserDisabled } return u, nil } // PermissionCodesOf 用户拥有的权限编码集合 func PermissionCodesOf(u *model.User) map[string]bool { codes := make(map[string]bool) if u == nil || u.Role == nil { return codes } if u.Role.Code == "super_admin" { codes["*"] = true return codes } for _, p := range u.Role.Permissions { codes[p.Code] = true } return codes } // BootstrapAdmin 按配置确保管理员账户存在 func (s *AuthService) BootstrapAdmin() error { ac := s.cfg.Admin if ac.Username == "" { return nil } u, err := s.userRepo.FindByUsername(ac.Username) if err == nil { // 已存在:确保其为配置的角色 if role, rerr := s.roleRepo.FindByCode(ac.Role); rerr == nil && u.RoleID != role.ID { _ = s.userRepo.UpdateFields(u.ID, map[string]interface{}{"role_id": role.ID, "status": 1}) } return nil } role, err := s.roleRepo.FindByCode(ac.Role) if err != nil { return fmt.Errorf("配置的管理员角色 %s 不存在: %w", ac.Role, err) } hash, err := utils.HashPassword(ac.Password) if err != nil { return err } admin := &model.User{ Username: ac.Username, Email: ac.Email, PasswordHash: hash, RoleID: role.ID, Status: 1, StorageLimit: 1 << 40, } if err := s.userRepo.Create(admin); err != nil { return err } utils.Info("auth", "已创建管理员账户: %s", ac.Username) return nil }