fix: 收尾 P2——登录恒定耗时、AdminUsage N+1、用量索引重塑与时区缝

- 登录失败路径补 argon2 恒定耗时校验(用户不存在/被禁用与密码错误的时序不可区分,抹掉用户名枚举面)
- AdminUsage 每行 2 次查询改批量预取(100 行分页省 200 次查询)
- usage_logs 补 key_id 索引与 (user,created)/(channel,created) 复合索引(schema v6);AutoMigrate 按名跳过已存在索引,旧库的复合索引需一次性删掉重建,用 system_configs 打标记防 PG 每次启动重演
- 用量统计默认窗口改 UTC(usage_daily 的 date 键是 UTC,非 UTC 服务器上图表窗口偏移一天)
- 注册 email 大小写归一;并发撞唯一索引回查后返回 409 而非 500
- PatchKey 的 name 校验与 CreateKey 对齐(1-64 字符),PATCH 不再能绕过
- TrendChart 条宽不超过槽位(点数多时不再互相重叠),超过 32 个点隐藏标签行
This commit is contained in:
Sakurasan committed 2026-10-29 03:05:56 +08:00
1 parent 4df2506a74
commit 1f8af649ca
7 files changed
+107 -21

No files matched your search

+26 -3
View File
@@ -25,10 +25,24 @@ type Handler struct {
a *app.App
gw *proxy.Gateway
passkeys *passkey.Service
// dummyHash 与配置同参数的 argon2id 哈希:用户不存在/被禁用时也跑一次完整校验,
// 抹平「存在→校验几十 ms / 不存在→立即 401」的时序差(用户名枚举面)。
dummyHash string
}
func NewHandler(a *app.App, gw *proxy.Gateway, pk *passkey.Service) *Handler {
return &Handler{a: a, gw: gw, passkeys: pk}
h := &Handler{a: a, gw: gw, passkeys: pk}
if dummy, err := a.Hasher.HashPassword("openteam-timing-equalizer"); err == nil {
h.dummyHash = dummy
}
return h
}
// equalizeTiming 恒定耗时:登录失败路径同样消耗一次 argon2,时序上与命中用户不可区分。
func (h *Handler) equalizeTiming(password string) {
if h.dummyHash != "" {
_, _ = h.a.Hasher.VerifyPassword(h.dummyHash, password)
}
}
// authLimited 认证端点滑动窗口限速:超限返回 429 并带 Retry-After。
@@ -102,8 +116,9 @@ func (h *Handler) Register(c *gin.Context) {
}
}()
}
email := strings.ToLower(req.Email)
var count int64
h.a.DB.Model(&store.User{}).Where("username = ? OR email = ?", req.Username, req.Email).Count(&count)
h.a.DB.Model(&store.User{}).Where("username = ? OR email = ?", req.Username, email).Count(&count)
if count > 0 {
resp.Fail(c, http.StatusConflict, "username or email already exists")
return
@@ -115,7 +130,7 @@ func (h *Handler) Register(c *gin.Context) {
}
u := store.User{
Username: req.Username,
Email: strings.ToLower(req.Email),
Email: email,
PasswordHash: hash,
Role: store.RoleUser,
Balance: 5.0, // 新用户赠送体验余额(可通过 admin 调整)
@@ -126,6 +141,13 @@ func (h *Handler) Register(c *gin.Context) {
u.InviteCode = &ic
}
if err := h.a.DB.Create(&u).Error; err != nil {
// 并发注册撞唯一索引(前置 count 检查是 TOCTOU 的):回查区分冲突与真故障
var n int64
h.a.DB.Model(&store.User{}).Where("username = ? OR email = ?", req.Username, email).Count(&n)
if n > 0 {
resp.Fail(c, http.StatusConflict, "username or email already exists")
return
}
resp.Fail(c, http.StatusInternalServerError, "failed to create user")
return
}
@@ -156,6 +178,7 @@ func (h *Handler) Login(c *gin.Context) {
var u store.User
err := h.a.DB.Where("username = ? OR email = ?", req.Username, strings.ToLower(req.Username)).First(&u).Error
if err != nil || u.Status != store.UserStatusActive {
h.equalizeTiming(req.Password)
resp.Fail(c, http.StatusUnauthorized, "invalid credentials")
return
}