session.js
2.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* 会话与 token 签发的共享编排 —— register / login / oauth / refresh 共用。
*
* 一次签发产出一对 access + refresh:access 是无状态 JWT(claims 含 sub/sid),
* refresh 是不透明随机串,其哈希连同会话族(family)落库以支持轮转与 token-reuse 检测。
* 签名/哈希全走注入 Port,本文件不碰密码学,只生成随机标识与组织 claims。
*/
const DEFAULT_ACCESS_TTL_MS = 15 * 60 * 1000 // 15 分钟
const DEFAULT_REFRESH_TTL_MS = 30 * 24 * 60 * 60 * 1000 // 30 天
/** 生成不透明随机标识(CSPRNG,非自研)。 @returns {string} */
export function randomId() {
return globalThis.crypto.randomUUID().replace(/-/g, '')
}
/**
* 为某 user + 会话族签发新 access+refresh,并把会话落库。
* 用于首次签发(family 新建)与轮转(沿用旧 family)。
*
* @param {Object} deps
* @param {import('./index.js').UserRepo} deps.userRepo
* @param {import('./index.js').TokenSigner} deps.tokenSigner
* @param {import('./index.js').Hasher} deps.hasher
* @param {number} [deps.accessTtlMs]
* @param {number} [deps.refreshTtlMs]
* @param {Object} args
* @param {string} args.user_id
* @param {string} [args.family] 轮转时沿用旧族;缺省新建。
* @returns {Promise<{ access_token: string, refresh_token: string, sid: string, family: string }>}
*/
export async function issueSession(
{ userRepo, tokenSigner, hasher, accessTtlMs = DEFAULT_ACCESS_TTL_MS, refreshTtlMs = DEFAULT_REFRESH_TTL_MS },
{ user_id, family },
) {
const sid = randomId()
const fam = family ?? randomId()
const refreshSecret = randomId() + randomId()
const refresh_hash = await hasher.hash(refreshSecret)
const expires_at = new Date(Date.now() + refreshTtlMs)
await userRepo.saveSession({ user_id, sid, refresh_hash, family: fam, expires_at })
const access_token = await tokenSigner.sign(
{ sub: user_id, sid, fam },
{ ttlMs: accessTtlMs },
)
// refresh 客户端态需携带 sid 以便 refresh 时定位会话(不暴露 hash)。
return { access_token, refresh_token: `${sid}.${refreshSecret}`, sid, family: fam }
}
/**
* 拆包客户端 refresh token → { sid, secret }。格式非法返 null。
* @param {string} token
* @returns {{ sid: string, secret: string }|null}
*/
export function parseRefreshToken(token) {
if (typeof token !== 'string') return null
const idx = token.indexOf('.')
if (idx <= 0 || idx === token.length - 1) return null
return { sid: token.slice(0, idx), secret: token.slice(idx + 1) }
}
export { DEFAULT_ACCESS_TTL_MS, DEFAULT_REFRESH_TTL_MS }