From 05c01ccebe16413fd786ab0c7d292e0baead89b4 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 18 Jun 2026 13:27:49 +0800 Subject: [PATCH] fix(session): serialize session-limit enforcement with row lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit login 的 COUNT→DELETE→INSERT 改为事务内执行,并对 users 行加 FOR UPDATE, 串行化同一用户的并发登录,消除超出 MAX_SESSIONS_PER_USER 的竞态(M1)。 锁定 users 行而非 sessions 表,粒度最小;commit 后无残留 DB 操作。 --- src/api/auth.rs | 49 ++++++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/api/auth.rs b/src/api/auth.rs index 6fe913a..956b040 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -190,7 +190,7 @@ pub async fn login(username: String, password: String) -> Result Result NOW()", &[&user_id], @@ -253,27 +260,27 @@ pub async fn login(username: String, password: String) -> Result= max_sessions { - client - .execute( - "DELETE FROM sessions WHERE id IN ( - SELECT id FROM sessions - WHERE user_id = $1 AND expires_at > NOW() - ORDER BY created_at ASC - LIMIT 1 - )", - &[&user_id], - ) - .await - .map_err(AppError::query)?; - } - - client - .execute( - "INSERT INTO sessions (user_id, token_hash, user_agent, expires_at) VALUES ($1, $2, $3, $4)", - &[&user_id, &token_hash, &None::, &expires_at], + tx.execute( + "DELETE FROM sessions WHERE id IN ( + SELECT id FROM sessions + WHERE user_id = $1 AND expires_at > NOW() + ORDER BY created_at ASC + LIMIT 1 + )", + &[&user_id], ) .await .map_err(AppError::query)?; + } + + tx.execute( + "INSERT INTO sessions (user_id, token_hash, user_agent, expires_at) VALUES ($1, $2, $3, $4)", + &[&user_id, &token_hash, &None::, &expires_at], + ) + .await + .map_err(AppError::query)?; + + tx.commit().await.map_err(AppError::query)?; let cookie = session::session_cookie(&token, 30 * 24 * 60 * 60, session::cookie_secure()); // 通过 Dioxus FullstackContext 设置 HttpOnly Cookie 响应头。