feat(auth): 登录态改为滑动续期(后台 24h/7d,打手 7d/30d)
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user