修复验证码服务在 PostgreSQL 下的事务行锁查询
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-10-06 03:12:46 +08:00
parent b1f976fd7a
commit 2697ba7ac0
4 changed files with 59 additions and 15 deletions

View File

@ -186,6 +186,7 @@ func (s *EventStore) TryAcquireCleanupLease(ctx context.Context, now time.Time)
now = s.now()
}
session := s.engine.NewSession().Context(ctx)
defer session.Close()
if err := session.Begin(); err != nil {
return cleanupLease{}, false, fmt.Errorf("begin cleanup lease transaction: %w", err)
}
@ -196,12 +197,9 @@ func (s *EventStore) TryAcquireCleanupLease(ctx context.Context, now time.Time)
if err := insertCleanupState(session, now); err != nil {
return rollback(fmt.Errorf("initialize cleanup state: %w", err))
}
query := session.Where("state_key = ?", cleanupStateKey)
if s.engine.DriverName() != "sqlite3" && s.engine.DriverName() != "sqlite" {
query = query.ForUpdate()
}
var state models.VerificationCleanupState
has, err := query.Get(&state)
has, err := getVerificationRowForUpdate(session, &state,
"SELECT * FROM verification_cleanup_state WHERE state_key = ?", cleanupStateKey)
if err != nil {
return rollback(fmt.Errorf("load cleanup state: %w", err))
}

View File

@ -102,11 +102,8 @@ func (r *PersistentRateLimiter) allowWithRelease(ctx context.Context, request Ra
return rollback(fmt.Errorf("initialize verification rate limit: %w", err))
}
var row models.VerificationRateLimit
query := session.Where("dimension_key = ?", key)
if r.engine.DriverName() != "sqlite3" && r.engine.DriverName() != "sqlite" {
query = query.ForUpdate()
}
has, err := query.Get(&row)
has, err := getVerificationRowForUpdate(session, &row,
"SELECT * FROM verification_rate_limit WHERE dimension_key = ?", key)
if err != nil {
return rollback(fmt.Errorf("load verification rate limit: %w", err))
}
@ -159,11 +156,8 @@ func (r *PersistentRateLimiter) releaseCooldown(rows []models.VerificationRateLi
for _, row := range rows {
// 同一维度已有新发送时,不清理新请求的冷却,也不重置小时和每日计数。
var current models.VerificationRateLimit
query := session.ID(row.ID)
if r.engine.DriverName() != "sqlite3" && r.engine.DriverName() != "sqlite" {
query = query.ForUpdate()
}
has, err := query.Get(&current)
has, err := getVerificationRowForUpdate(session, &current,
"SELECT * FROM verification_rate_limit WHERE id = ?", row.ID)
if err != nil {
return err
}

View File

@ -0,0 +1,27 @@
package verification
import (
"fmt"
"xorm.io/xorm"
)
// 使用参数化查询保留事务行锁,避免当前 Xorm 仅支持 MySQL 的行锁构造器。
func getVerificationRowForUpdate(session *xorm.Session, bean any, query string, args ...any) (bool, error) {
statement, err := verificationLockQuery(session.Engine().DriverName(), query)
if err != nil {
return false, err
}
return session.SQL(statement, args...).Get(bean)
}
func verificationLockQuery(driver, query string) (string, error) {
switch driver {
case "postgres", "mysql":
return query + " FOR UPDATE", nil
case "sqlite", "sqlite3":
return query, nil
default:
return "", fmt.Errorf("unsupported verification lock driver: %s", driver)
}
}

View File

@ -0,0 +1,25 @@
package verification
import "testing"
func TestVerificationLockQuery(t *testing.T) {
const query = "SELECT * FROM verification_rate_limit WHERE dimension_key = ?"
for _, driver := range []string{"postgres", "mysql", "sqlite", "sqlite3"} {
t.Run(driver, func(t *testing.T) {
got, err := verificationLockQuery(driver, query)
if err != nil {
t.Fatal(err)
}
want := query
if driver == "postgres" || driver == "mysql" {
want += " FOR UPDATE"
}
if got != want {
t.Fatalf("query = %q, want %q", got, want)
}
})
}
if got, err := verificationLockQuery("unknown", query); err == nil || got != "" {
t.Fatalf("unsupported driver did not fail closed: %q, %v", got, err)
}
}