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 集成 + 容器内真实系统账号端到端验证
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user