认证: - 图形验证码 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 集成 + 容器内真实系统账号端到端验证
69 lines
1.6 KiB
Go
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)
|
|
}
|