// Package auth 提供认证相关能力:OTP(双通道)、会话、bcrypt、图形验证码。 // // OTP 双通道对齐:邮件发送与 CLI 获取共用同一 OTPStore(同一验证码、同一 // 10 分钟有效期、同一 60s 冷却与失败限速),邮件失败不阻断 CLI 通道。 // 单实例用内存存储;多实例需改为 DB/Redis(PLAN §6 注明)。 package auth import ( "errors" "sync" "time" "ws_usernode/internal/pkg" ) // OTP 错误。 var ( ErrCooldown = errors.New("auth: otp 发送冷却中") ErrInvalidCode = errors.New("auth: 验证码错误") ErrTooManyFails = errors.New("auth: 失败次数过多,请稍后再试") ) const ( otpCodeLen = 6 maxFailures = 5 // 单账号连续失败限速阈值 failureWin = 10 * time.Minute // 失败计数窗口 ) // OTPStore 为 OTP 验证码存储。内存实现为单实例默认实现。 type OTPStore interface { // Send 为 username 生成新验证码(覆盖旧码)。冷却期内调用返回 ErrCooldown。 // 邮件与 CLI 双通道都走该方法,保证对齐。 Send(username string, ttl, cooldown time.Duration) (string, error) // Verify 校验验证码并一次性消费。失败累计计数(达到阈值返回 ErrTooManyFails)。 Verify(username, code string) (bool, error) // Failures 返回 username 当前失败计数。 Failures(username string) (int, error) } type otpEntry struct { code string expiresAt time.Time cooldownAt time.Time failures int } // MemoryOTPStore 为单实例内存实现。 type MemoryOTPStore struct { mu sync.Mutex entries map[string]*otpEntry } // NewMemoryOTPStore 创建内存 OTP 存储。 func NewMemoryOTPStore() *MemoryOTPStore { return &MemoryOTPStore{entries: make(map[string]*otpEntry)} } func (s *MemoryOTPStore) Send(username string, ttl, cooldown time.Duration) (string, error) { s.mu.Lock() defer s.mu.Unlock() now := time.Now() if e, ok := s.entries[username]; ok && now.Before(e.cooldownAt) { return "", ErrCooldown } code, err := pkg.RandomDigits(otpCodeLen) if err != nil { return "", err } s.entries[username] = &otpEntry{ code: code, expiresAt: now.Add(ttl), cooldownAt: now.Add(cooldown), failures: 0, } return code, nil } func (s *MemoryOTPStore) Verify(username, code string) (bool, error) { s.mu.Lock() defer s.mu.Unlock() e, ok := s.entries[username] if !ok { return false, ErrInvalidCode } now := time.Now() if now.After(e.expiresAt) { delete(s.entries, username) return false, ErrInvalidCode } if e.failures >= maxFailures { return false, ErrTooManyFails } if e.code != code { e.failures++ if e.failures >= maxFailures { return false, ErrTooManyFails } return false, ErrInvalidCode } delete(s.entries, username) // 一次性 return true, nil } func (s *MemoryOTPStore) Failures(username string) (int, error) { s.mu.Lock() defer s.mu.Unlock() if e, ok := s.entries[username]; ok { return e.failures, nil } return 0, nil }