Files
usernode/internal/auth/ratelimit.go
T
cao.wangrenbo 630d240dc0 feat(M1): 认证与用户管理 — 双通道登录、cookie 会话、用户 CRUD 与真实系统账号对接
认证:
- 图形验证码 GET /auth/captcha(内置 PNG 渲染,零第三方依赖)
- 外部用户 OTP 双通道:DB 存储(otp_codes)使邮件与 CLI 共用同一验证码/冷却/失败限速
- 管理员 bcrypt 登录 + 连续失败限速锁定;admin/forgot + admin/reset 邮件重置(SMTP 或日志)
- cookie 会话(HttpOnly/SameSite)、me/logout、admin/user 鉴权中间件

用户管理(admin):
- CRUD + disable/enable/extend/delete,对接 system 层真实 useradd/usermod/userdel/passwd
- system 层三执行模式:dry-run(默认,安全)/ direct(容器/测试用户)/ sudo(生产 sudoers 白名单)
- Exists 系统账号一致性检查;deploy/sudoers.example 白名单模板
- 关键操作接入 append-only 审计

其他:
- CLI user otp 改 DB store,与邮件通道真正对齐
- 容器镜像补 shadow(alpine 无 useradd);Makefile VERSION 0.2.0-m1
- 测试:auth/service 单测 + api httptest 集成 + 容器内真实系统账号端到端验证
2026-08-29 23:40:20 +08:00

69 lines
1.6 KiB
Go

package auth
import (
"sync"
"time"
)
// RateLimiter 内存登录限速器:按 key(如用户名)统计连续失败次数,
// 达到阈值后锁定 lockFor 时长,锁定期内 Allow 返回 false。
// 单实例内存实现即可满足(登录失败限速无跨进程一致性要求)。
type RateLimiter struct {
mu sync.Mutex
max int
lockFor time.Duration
entries map[string]*rlEntry
now func() time.Time // 可注入时钟(测试)
}
type rlEntry struct {
failures int
lockedUntil time.Time
}
// NewRateLimiter 创建限速器。
func NewRateLimiter(max int, lockFor time.Duration) *RateLimiter {
return &RateLimiter{max: max, lockFor: lockFor, entries: make(map[string]*rlEntry), now: time.Now}
}
// Allow 返回 key 当前是否允许继续尝试。
func (l *RateLimiter) Allow(key string) bool {
l.mu.Lock()
defer l.mu.Unlock()
e, ok := l.entries[key]
if !ok {
return true
}
if now := l.now(); now.Before(e.lockedUntil) {
return false
} else if e.failures >= l.max {
// 锁定已过期:重置计数,允许重试
delete(l.entries, key)
return true
}
return true
}
// RecordFailure 记录一次失败;达到阈值后进入锁定。
func (l *RateLimiter) RecordFailure(key string) {
l.mu.Lock()
defer l.mu.Unlock()
now := l.now()
e, ok := l.entries[key]
if !ok || now.After(e.lockedUntil) && e.failures >= l.max {
e = &rlEntry{}
l.entries[key] = e
}
e.failures++
if e.failures >= l.max {
e.lockedUntil = now.Add(l.lockFor)
}
}
// Reset 清除 key 的失败记录(登录成功后调用)。
func (l *RateLimiter) Reset(key string) {
l.mu.Lock()
defer l.mu.Unlock()
delete(l.entries, key)
}