feat(auth): 登录态改为滑动续期(后台 24h/7d,打手 7d/30d)

This commit is contained in:
yml2213
2026-10-25 13:28:58 +08:00
parent 81edf530db
commit 2fb7aae13f
31 changed files with 510 additions and 64 deletions
@@ -60,16 +60,47 @@ export async function getActiveAdminSession(
return result.rows[0] || null
}
export async function touchAdminSession(sessionId: string, now: string): Promise<boolean> {
const result = await query(
/** 活跃请求的写入节流:last_seen_at 与滑动过期最多 5 分钟刷新一次。 */
const ADMIN_SESSION_TOUCH_MIN_INTERVAL = '5 minutes'
/**
* 校验会话有效并滑动续期。
* 返回续期后的空闲到期时间;会话无效/已过期返回 null。
*/
export async function touchAdminSession(input: {
sessionId: string
now: string
idleExpiresAt: string
absoluteExpiresAt: string
}): Promise<string | null> {
const result = await query<{ expires_at: string | Date }>(
`
UPDATE admin_sessions
SET last_seen_at = $2, updated_at = $2
WHERE session_id = $1 AND status = 'active' AND expires_at > $2
WITH target AS (
SELECT session_id, expires_at
FROM admin_sessions
WHERE session_id = $1 AND status = 'active' AND expires_at > $2
), touched AS (
UPDATE admin_sessions s
SET last_seen_at = $2,
updated_at = $2,
expires_at = LEAST($3::timestamptz, $4::timestamptz)
FROM target
WHERE s.session_id = target.session_id
AND s.last_seen_at < ($2::timestamptz - INTERVAL '${ADMIN_SESSION_TOUCH_MIN_INTERVAL}')
AND s.expires_at < LEAST($3::timestamptz, $4::timestamptz)
RETURNING s.expires_at
)
SELECT COALESCE(
(SELECT expires_at FROM touched),
(SELECT expires_at FROM target)
) AS expires_at
`,
[sessionId, now],
[input.sessionId, input.now, input.idleExpiresAt, input.absoluteExpiresAt],
)
return result.rowCount === 1
const expiresAt = result.rows[0]?.expires_at
if (!expiresAt) return null
const parsed = new Date(expiresAt).getTime()
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null
}
export async function revokeAdminSession(sessionId: string, now: string): Promise<boolean> {
@@ -84,40 +84,49 @@ export async function getWorkerSessionBySessionId(
}
/**
* 校验会话仍然有效,并顺带刷新 last_seen_at。
* 校验会话仍然有效,并滑动续期空闲窗口。
*
* 这里必须同时满足两个约束,所以不能用"直接跳过"的写法:
* 1. 返回值是**鉴权有效性**——上层 `!(await touchWorkerSession(...))` 为真就直接 401
* 「登录设备已被删除」,跳过查询会把所有正常请求打成未登录;
* 2. 也不能像以前那样每个请求都真写一次:32 天里 last_seen_at 被写了 3300 万次,
* 占数据库总执行时间的 2.8%,还在 worker_sessions 上堆出 14% 的死元组。
*
* 因此改成「一条语句拿到有效性 + 只在超过 5 分钟没刷新时才写」。
* 副作用只有 last_seen_at 精度变粗(用于设备列表排序与展示),
* 不影响会话有效期:expires_at 在建会话时确定,不做滑动续期。
* 约束:
* 1. 返回值是鉴权有效性——null 表示会话已失效/被删除,上层直接 401;
* 2. 写入要节流:32 天里 last_seen_at 被写了 3300 万次,占数据库总执行时间的 2.8%;
* 3. 续期后的 expires_at 不得超过 Token 里的绝对上限,绝对上限只能重新登录突破。
*/
const WORKER_SESSION_TOUCH_MIN_INTERVAL = '5 minutes'
export async function touchWorkerSession(sessionId: string, now: string): Promise<boolean> {
const result = await query<{ valid: boolean }>(
export async function touchWorkerSession(input: {
sessionId: string
now: string
idleExpiresAt: string
absoluteExpiresAt: string
}): Promise<string | null> {
const result = await query<{ expires_at: string | Date }>(
`
WITH target AS (
SELECT session_id
SELECT session_id, expires_at
FROM worker_sessions
WHERE session_id = $1 AND status = 'active' AND expires_at > $2
), touched AS (
UPDATE worker_sessions ws
SET last_seen_at = $2, updated_at = $2
SET last_seen_at = $2,
updated_at = $2,
expires_at = LEAST($3::timestamptz, $4::timestamptz)
FROM target
WHERE ws.session_id = target.session_id
AND ws.last_seen_at < ($2::timestamptz - INTERVAL '${WORKER_SESSION_TOUCH_MIN_INTERVAL}')
RETURNING ws.session_id
AND ws.expires_at < LEAST($3::timestamptz, $4::timestamptz)
RETURNING ws.expires_at
)
SELECT EXISTS (SELECT 1 FROM target) AS valid
SELECT COALESCE(
(SELECT expires_at FROM touched),
(SELECT expires_at FROM target)
) AS expires_at
`,
[String(sessionId || '').trim(), now],
[input.sessionId, input.now, input.idleExpiresAt, input.absoluteExpiresAt],
)
return Boolean(result.rows[0]?.valid)
const expiresAt = result.rows[0]?.expires_at
if (!expiresAt) return null
const parsed = new Date(expiresAt).getTime()
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null
}
export async function revokeWorkerSession(