Files
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

71 lines
1.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package auth
import (
"errors"
"sync"
"time"
"ws_usernode/internal/pkg"
)
// ErrCaptchaInvalid 表示图形验证码校验失败。
var ErrCaptchaInvalid = errors.New("auth: 图形验证码错误")
// Captcha 图形验证码(防机器人,登录前置)。
type Captcha struct {
ID string
Text string
}
// CaptchaStore 为图形验证码存储。单实例内存实现为默认;
// 多实例部署需改 DB/RedisPLAN §6 注明)。
type CaptchaStore interface {
// New 生成一个验证码并返回其 ID。
New() (*Captcha, error)
// Verify 校验并一次性消费。失败或过期返回 false。
Verify(id, answer string) bool
}
// MemoryCaptchaStore 单实例内存实现。
type MemoryCaptchaStore struct {
ttl time.Duration
mu sync.Mutex
entries map[string]*captchaEntry
}
type captchaEntry struct {
text string
expiresAt time.Time
}
// NewMemoryCaptchaStore 创建内存图形验证码存储。
func NewMemoryCaptchaStore(ttl time.Duration) *MemoryCaptchaStore {
return &MemoryCaptchaStore{ttl: ttl, entries: make(map[string]*captchaEntry)}
}
func (s *MemoryCaptchaStore) New() (*Captcha, error) {
text, err := pkg.RandomDigits(4)
if err != nil {
return nil, err
}
id, err := pkg.RandomHex(16)
if err != nil {
return nil, err
}
s.mu.Lock()
defer s.mu.Unlock()
s.entries[id] = &captchaEntry{text: text, expiresAt: time.Now().Add(s.ttl)}
return &Captcha{ID: id, Text: text}, nil
}
func (s *MemoryCaptchaStore) Verify(id, answer string) bool {
s.mu.Lock()
defer s.mu.Unlock()
e, ok := s.entries[id]
if !ok {
return false
}
delete(s.entries, id) // 一次性
return e.text == answer && time.Now().Before(e.expiresAt)
}