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:
1 parent
4df2506a74
commit
1f8af649ca
7 files changed
+107
-21
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user