import { H3Event, parseCookies } from 'h3'; import { CookieKVValue, getMpCookie, setMpCookie } from '~/server/kv/cookie'; // 表示一条 set-cookie 记录的解析结果 export type CookieEntity = Record; // 公众号所有的 set-cookie 解析结果 export class AccountCookie { private readonly _token: string; private _cookie: CookieEntity[]; /** * @param token * @param cookies response.headers.getSetCookie() 的结果,是一个字符串数组 */ constructor(token: string, cookies: string[]) { this._token = token; this._cookie = AccountCookie.parse(cookies); } static create(token: string, cookies: CookieEntity[]): AccountCookie { const value = new AccountCookie(token, []); value._cookie = cookies; return value; } public toString(): string { return this.stringify(this._cookie); } public toJSON(): CookieKVValue { return { token: this._token, cookies: this._cookie, }; } public get(name: string): CookieEntity | undefined { return this._cookie.find(cookie => cookie.name === name); } public get token() { return this._token; } // 根据 cookie 中的 expires 来确定是否已过期 public get isExpired(): boolean { // todo return false; } public static parse(cookies: string[]): CookieEntity[] { // key 为 cookie 的 name const cookieMap = new Map(); for (const cookie of cookies) { const cookieObj: CookieEntity = {}; // 分割 cookie 字符串为各个属性 const parts = cookie.split(';').map(str => str.trim()); // 第一个部分是name=value const [nameValue] = parts; if (nameValue) { const [name, ...valueParts] = nameValue.split('='); const cookieName = name.trim(); cookieObj.name = cookieName; cookieObj.value = valueParts.join('=').trim(); // 处理值中可能包含的等号 // 处理其他属性(如Expires, Path, Domain等) for (const part of parts.slice(1)) { const [key, ...valueParts] = part.split('='); const value = valueParts.join('=').trim(); // 处理值中可能包含的等号 if (key) { const keyLower = key.toLowerCase(); cookieObj[keyLower] = value || 'true'; // 无值属性(如HttpOnly)设为true // 如果是expires字段,添加时间戳 if (keyLower === 'expires' && value) { try { const timestamp = Date.parse(value); if (!isNaN(timestamp)) { cookieObj.expires_timestamp = timestamp; // 添加时间戳(毫秒) } } catch (e) { // 如果日期解析失败,忽略时间戳字段 } } } } // Only add valid cookies to the map (overwrite if duplicate name) if (cookieObj.name) { cookieMap.set(cookieName, cookieObj); } } } return Array.from(cookieMap.values()); } private stringify(parsedCookie: CookieEntity[]): string { return parsedCookie .filter(cookie => cookie.value && cookie.value !== 'EXPIRED') .map(cookie => `${cookie.name}=${cookie.value}`) .join('; '); } } // 所有用户的 cookie 仓库 class CookieStore { // key 为 authKey, value 为 AccountCookie 实例 // 使用 Map 的插入顺序特性实现 LRU 淘汰 store: Map = new Map(); // 内存缓存最大条目数,防止无限增长 private readonly maxSize: number = 1000; async getAccountCookie(authKey: string): Promise { // 优先从本地内存取 let cachedAccountCookie = this.store.get(authKey); if (cachedAccountCookie) { // LRU: 访问时将条目移到末尾(最近使用) this.store.delete(authKey); this.store.set(authKey, cachedAccountCookie); return cachedAccountCookie; } // 如果内存没有,则从 kv 数据库取 const cookieValue = await getMpCookie(authKey); if (!cookieValue) { return null; } cachedAccountCookie = AccountCookie.create(cookieValue.token, cookieValue.cookies); this.evictIfNeeded(); this.store.set(authKey, cachedAccountCookie); return cachedAccountCookie; } /** * 检索用户的cookie * @param authKey * @return 适合作为请求头的Cookie字符串 */ async getCookie(authKey: string): Promise { const accountCookie = await this.getAccountCookie(authKey); if (!accountCookie) { return null; } return accountCookie.toString(); } /** * 存储用户的cookie * @param authKey * @param token * @param cookie 原始的 set-cookie 字符串数组 */ async setCookie(authKey: string, token: string, cookie: string[]): Promise { const accountCookie = new AccountCookie(token, cookie); // 如果已存在则先删除(保证 LRU 顺序正确) this.store.delete(authKey); this.evictIfNeeded(); this.store.set(authKey, accountCookie); return await setMpCookie(authKey, accountCookie.toJSON()); } /** * 移除用户的 cookie(用于登出等场景) * @param authKey */ removeCookie(authKey: string): void { this.store.delete(authKey); } /** * 当内存缓存达到上限时,淘汰最久未使用的条目 */ private evictIfNeeded(): void { while (this.store.size >= this.maxSize) { // Map 迭代器按插入顺序返回,第一个即为最久未使用 const oldestKey = this.store.keys().next().value; if (oldestKey !== undefined) { this.store.delete(oldestKey); } else { break; } } } /** * 检索用户的 token * @param authKey */ async getToken(authKey: string): Promise { const accountCookie = await this.getAccountCookie(authKey); if (!accountCookie) { return null; } return accountCookie.token; } /** * 转换为 json 格式,方便存储与传输 * 返回一个对象,键为 uuid,值为解析后的 cookie 对象 */ toJSON(): Record { const json: Record = {}; for (const [authKey, accountCookie] of this.store) { json[authKey] = accountCookie; } return json; } } export const cookieStore = new CookieStore(); /** * 从 CookieStore 中获取 cookie 字符串 * * @description 根据请求中的 X-Auth-Key header 或者 auth-key cookie,从 CookieStore 中检索用户登录信息的 cookie,这些 cookie 会透传给微信 * @param event */ export async function getCookieFromStore(event: H3Event): Promise { let cookie: string | null = null; // 优先根据自定义的 X-Auth-Key 检索 let authKey = getRequestHeader(event, 'X-Auth-Key'); if (authKey) { cookie = await cookieStore.getCookie(authKey); if (cookie) { return cookie; } } // 从 cookie 中的 token 检索 const cookies = parseCookies(event); authKey = cookies['auth-key']; if (authKey) { cookie = await cookieStore.getCookie(authKey); if (cookie) { return cookie; } } return null; } /** * 从 CookieStore 中获取公众号的 token * * @description 根据请求中的 X-Auth-Key header 或者 auth-key cookie,从 CookieStore 中检索用户登录时绑定的 token * @param event */ export async function getTokenFromStore(event: H3Event): Promise { let token: string | null = null; // 优先根据自定义的 X-Auth-Key 检索 let authKey = getRequestHeader(event, 'X-Auth-Key'); if (authKey) { token = await cookieStore.getToken(authKey); if (token) { return token; } } // 从 cookie 中的 token 检索 const cookies = parseCookies(event); authKey = cookies['auth-key']; if (authKey) { token = await cookieStore.getToken(authKey); if (token) { return token; } } return null; } /** * 从请求中获取 cookie 字符串 * * @description 用于登录过程中 uuid cookie 透传给微信 * @param event */ export function getCookiesFromRequest(event: H3Event): string { const cookies = parseCookies(event); return Object.keys(cookies) .map(key => `${key}=${encodeURIComponent(cookies[key])}`) .join(';'); } /** * 从 response 中获取指定的 set-cookie 的 value 部分 * @param name cookie 名 * @param response */ export function getCookieFromResponse(name: string, response: Response): string | null { const cookies = AccountCookie.parse(response.headers.getSetCookie()); const targetCookie = cookies.find(cookie => cookie.name === name); if (targetCookie) { return targetCookie.value as string; } return null; }