Files
CloudFlare-ImgBed/functions/utils/auth/sessionManager.js
T

226 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 会话管理工具
* 使用数据库存储会话,通过 HttpOnly Cookie 传递会话 Token
* 管理端和用户端使用独立的 Cookieadmin_session / user_session
*/
import { generateSessionToken } from './passwordHash.js';
import { getDatabase } from '../databaseAdapter.js';
import { fetchSecurityConfig } from '../sysConfig.js';
import { normalizeSessionMaxAgeDays, sessionMaxAgeDaysToTtl } from './sessionConfig.js';
const SESSION_PREFIX = 'manage@session@';
// Cookie 名称映射
const COOKIE_NAMES = {
admin: 'admin_session',
user: 'user_session',
};
/**
* 创建新会话
* @param {Object} env - 环境变量
* @param {string} authType - 认证类型 ('admin' | 'user')
* @param {string} [username] - 用户名(管理员登录时使用)
* @returns {Promise<{token: string, cookie: string}>}
*/
export async function createSession(env, authType, username = '') {
// 读取安全策略配置
const securityConfig = await fetchSecurityConfig(env);
const accessConfig = securityConfig.access || {};
const secure = accessConfig.sessionSecure ?? false;
const rawMaxAgeDays = authType === 'admin'
? (accessConfig.adminSessionMaxAge ?? 14)
: (accessConfig.userSessionMaxAge ?? 14);
const maxAgeDays = normalizeSessionMaxAgeDays(rawMaxAgeDays);
const maxAge = sessionMaxAgeDaysToTtl(maxAgeDays);
const db = getDatabase(env);
const token = generateSessionToken();
const sessionData = {
authType,
username,
createdAt: Date.now(),
expiresAt: Date.now() + maxAge * 1000,
};
await db.put(`${SESSION_PREFIX}${token}`, JSON.stringify(sessionData), {
expirationTtl: maxAge,
});
const cookieName = COOKIE_NAMES[authType] || 'session';
const cookie = buildSessionCookie(cookieName, token, maxAge, secure);
return { token, cookie };
}
/**
* 验证会话(按 authType 读取对应的 Cookie
* @param {Object} env - 环境变量
* @param {Request} request - 请求对象
* @param {string} authType - 要验证的认证类型 ('admin' | 'user')
* @returns {Promise<{valid: boolean, session?: Object}>}
*/
export async function validateSession(env, request, authType) {
const cookieName = COOKIE_NAMES[authType] || 'session';
const token = getCookieValue(request, cookieName);
if (!token) {
return { valid: false };
}
const db = getDatabase(env);
const sessionStr = await db.get(`${SESSION_PREFIX}${token}`);
if (!sessionStr) {
return { valid: false };
}
try {
const session = JSON.parse(sessionStr);
// 验证 authType 匹配
if (session.authType !== authType) {
return { valid: false };
}
if (Date.now() > session.expiresAt) {
await db.delete(`${SESSION_PREFIX}${token}`);
return { valid: false };
}
return { valid: true, session };
} catch {
return { valid: false };
}
}
/**
* 验证任意有效会话(不限 authType,用于 sessionCheck 接口)
* @param {Object} env - 环境变量
* @param {Request} request - 请求对象
* @returns {Promise<{valid: boolean, session?: Object}>}
*/
export async function validateAnySession(env, request) {
// 优先检查 admin,再检查 user
const adminResult = await validateSession(env, request, 'admin');
if (adminResult.valid) return adminResult;
const userResult = await validateSession(env, request, 'user');
if (userResult.valid) return userResult;
return { valid: false };
}
/**
* 销毁会话
* @param {Object} env - 环境变量
* @param {Request} request - 请求对象
* @param {string} [authType] - 要销毁的认证类型,不传则销毁所有
* @returns {Promise<string|string[]>} 清除 Cookie 的 Set-Cookie 头
*/
export async function destroySession(env, request, authType) {
// 读取安全策略配置
const securityConfig = await fetchSecurityConfig(env);
const secure = securityConfig.access?.sessionSecure ?? false;
const db = getDatabase(env);
if (authType) {
// 销毁指定类型的会话
const cookieName = COOKIE_NAMES[authType] || 'session';
const token = getCookieValue(request, cookieName);
if (token) {
await db.delete(`${SESSION_PREFIX}${token}`);
}
return buildSessionCookie(cookieName, '', 0, secure);
} else {
// 销毁所有类型的会话
const cookies = [];
for (const [type, cookieName] of Object.entries(COOKIE_NAMES)) {
const token = getCookieValue(request, cookieName);
if (token) {
await db.delete(`${SESSION_PREFIX}${token}`);
}
cookies.push(buildSessionCookie(cookieName, '', 0, secure));
}
return cookies;
}
}
/**
* 按认证类型批量清除会话
* @param {Object} env - 环境变量
* @param {string} authType - 要清除的认证类型 ('admin' | 'user')
* @returns {Promise<number>} 清除的会话数量
*/
export async function destroySessionsByAuthType(env, authType) {
const db = getDatabase(env);
let destroyed = 0;
let cursor = undefined;
let hasMore = true;
while (hasMore) {
const listOptions = { prefix: SESSION_PREFIX };
if (cursor) {
listOptions.cursor = cursor;
}
const result = await db.list(listOptions);
const keys = result.keys || [];
for (const key of keys) {
try {
const sessionStr = await db.get(key.name);
if (sessionStr) {
const session = JSON.parse(sessionStr);
if (session.authType === authType) {
await db.delete(key.name);
destroyed++;
}
}
} catch {
await db.delete(key.name);
destroyed++;
}
}
cursor = result.cursor;
hasMore = !result.list_complete && cursor;
}
return destroyed;
}
/**
* 从请求中提取指定 Cookie 的值
* @param {Request} request - 请求对象
* @param {string} name - Cookie 名称
* @returns {string|null}
*/
function getCookieValue(request, name) {
const cookieHeader = request.headers.get('Cookie');
if (!cookieHeader) return null;
const regex = new RegExp('(^|;\\s*)' + name + '=([^;]+)');
const match = cookieHeader.match(regex);
return match ? match[2] : null;
}
/**
* 构建 Set-Cookie 头的值
* @param {string} name - Cookie 名称
* @param {string} token - 会话 Token
* @param {number} maxAge - 最大存活时间(秒)
* @param {boolean} secure - 是否添加 Secure 属性
* @returns {string}
*/
function buildSessionCookie(name, token, maxAge, secure = false) {
const parts = [
`${name}=${token}`,
`Path=/`,
`HttpOnly`,
`SameSite=Strict`,
`Max-Age=${maxAge}`,
];
if (secure) {
parts.push('Secure');
}
return parts.join('; ');
}