feat(auth): 登录态改为滑动续期(后台 24h/7d,打手 7d/30d)
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
type HeaderWriter = {
|
||||
setHeader(name: string, value: string): unknown
|
||||
}
|
||||
|
||||
type SessionExpiryLike = {
|
||||
expiresAt?: string | null
|
||||
absoluteExpiresAt?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 把滑动续期后的空闲到期时间回传给前端。
|
||||
* Token 本身不重签(exp 是绝对上限),前端只更新本地空闲计时器。
|
||||
*/
|
||||
export function setSessionExpiryHeaders(res: HeaderWriter, session: SessionExpiryLike): void {
|
||||
if (session.expiresAt) {
|
||||
res.setHeader('X-Session-Expires-At', session.expiresAt)
|
||||
}
|
||||
if (session.absoluteExpiresAt) {
|
||||
res.setHeader('X-Session-Absolute-Expires-At', session.absoluteExpiresAt)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { resolveSessionExpiryWindow, resolveSlidingSessionExpiresAt } from './session-window.js'
|
||||
import { setSessionExpiryHeaders } from './session-headers.js'
|
||||
|
||||
test('登录时同时给出空闲到期与绝对到期(后台 24h / 7d)', () => {
|
||||
const window = resolveSessionExpiryWindow({
|
||||
now: '2026-10-25T00:00:00.000Z',
|
||||
idleHours: 24,
|
||||
absoluteHours: 168,
|
||||
})
|
||||
assert.equal(window.idleExpiresAt, '2026-10-26T00:00:00.000Z')
|
||||
assert.equal(window.absoluteExpiresAt, '2026-11-01T00:00:00.000Z')
|
||||
})
|
||||
|
||||
test('登录时同时给出空闲到期与绝对到期(打手 7d / 30d)', () => {
|
||||
const window = resolveSessionExpiryWindow({
|
||||
now: '2026-10-25T00:00:00.000Z',
|
||||
idleHours: 168,
|
||||
absoluteHours: 720,
|
||||
})
|
||||
assert.equal(window.idleExpiresAt, '2026-11-01T00:00:00.000Z')
|
||||
assert.equal(window.absoluteExpiresAt, '2026-11-24T00:00:00.000Z')
|
||||
})
|
||||
|
||||
test('绝对上限小于空闲窗口时以绝对上限为准', () => {
|
||||
const window = resolveSessionExpiryWindow({
|
||||
now: '2026-10-25T00:00:00.000Z',
|
||||
idleHours: 24,
|
||||
absoluteHours: 12,
|
||||
})
|
||||
assert.equal(window.idleExpiresAt, '2026-10-25T12:00:00.000Z')
|
||||
assert.equal(window.absoluteExpiresAt, '2026-10-25T12:00:00.000Z')
|
||||
})
|
||||
|
||||
test('滑动续期目标不得超过绝对上限', () => {
|
||||
assert.equal(
|
||||
resolveSlidingSessionExpiresAt({
|
||||
now: '2026-10-25T00:00:00.000Z',
|
||||
idleHours: 24,
|
||||
absoluteExpiresAt: '2026-11-01T00:00:00.000Z',
|
||||
}),
|
||||
'2026-10-26T00:00:00.000Z',
|
||||
)
|
||||
assert.equal(
|
||||
resolveSlidingSessionExpiresAt({
|
||||
now: '2026-10-31T18:00:00.000Z',
|
||||
idleHours: 24,
|
||||
absoluteExpiresAt: '2026-11-01T00:00:00.000Z',
|
||||
}),
|
||||
'2026-11-01T00:00:00.000Z',
|
||||
)
|
||||
})
|
||||
|
||||
test('绝对上限缺失或非法时仍返回 now + 空闲窗口', () => {
|
||||
assert.equal(
|
||||
resolveSlidingSessionExpiresAt({
|
||||
now: '2026-10-25T00:00:00.000Z',
|
||||
idleHours: 24,
|
||||
absoluteExpiresAt: 'invalid',
|
||||
}),
|
||||
'2026-10-26T00:00:00.000Z',
|
||||
)
|
||||
})
|
||||
|
||||
test('会话续期响应头同时回传空闲到期与绝对到期', () => {
|
||||
const headers = new Map<string, string>()
|
||||
setSessionExpiryHeaders(
|
||||
{
|
||||
setHeader(name, value) {
|
||||
headers.set(name, value)
|
||||
},
|
||||
},
|
||||
{
|
||||
expiresAt: '2026-10-26T00:00:00.000Z',
|
||||
absoluteExpiresAt: '2026-11-01T00:00:00.000Z',
|
||||
},
|
||||
)
|
||||
assert.equal(headers.get('X-Session-Expires-At'), '2026-10-26T00:00:00.000Z')
|
||||
assert.equal(headers.get('X-Session-Absolute-Expires-At'), '2026-11-01T00:00:00.000Z')
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
import { addHours } from './time.js'
|
||||
|
||||
export type SessionExpiryWindow = {
|
||||
/** 空闲窗口:每次访问可滑动续期到的目标时间。 */
|
||||
idleExpiresAt: string
|
||||
/** 绝对上限:从登录起算,任何续期都不能突破。 */
|
||||
absoluteExpiresAt: string
|
||||
}
|
||||
|
||||
function normalizeHours(value: unknown, fallback: number): number {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) && parsed >= 1 ? Math.floor(parsed) : fallback
|
||||
}
|
||||
|
||||
/** 登录时计算空闲到期与绝对到期;绝对上限小于空闲窗口时以绝对上限为准。 */
|
||||
export function resolveSessionExpiryWindow(input: {
|
||||
now: string
|
||||
idleHours: number
|
||||
absoluteHours: number
|
||||
}): SessionExpiryWindow {
|
||||
const requestedIdleHours = normalizeHours(input.idleHours, 24)
|
||||
const absoluteHours = normalizeHours(input.absoluteHours, requestedIdleHours)
|
||||
const idleHours = Math.min(requestedIdleHours, absoluteHours)
|
||||
return {
|
||||
idleExpiresAt: addHours(input.now, idleHours),
|
||||
absoluteExpiresAt: addHours(input.now, absoluteHours),
|
||||
}
|
||||
}
|
||||
|
||||
/** 滑动续期目标:now + 空闲窗口,但不得超过 Token 里的绝对上限。 */
|
||||
export function resolveSlidingSessionExpiresAt(input: {
|
||||
now: string
|
||||
idleHours: number
|
||||
absoluteExpiresAt: string
|
||||
}): string {
|
||||
const idleHours = normalizeHours(input.idleHours, 24)
|
||||
const target = addHours(input.now, idleHours)
|
||||
const absoluteMs = Date.parse(String(input.absoluteExpiresAt || ''))
|
||||
if (!Number.isFinite(absoluteMs)) return target
|
||||
return Date.parse(target) <= absoluteMs ? target : new Date(absoluteMs).toISOString()
|
||||
}
|
||||
Reference in New Issue
Block a user