wechat-article / server /utils /CookieStore.ts
asemxin2000's picture
Deploy wechat-article-exporter to Space
6e41657 verified
Raw
History Blame Contribute Delete
9.17 kB
import { H3Event, parseCookies } from 'h3';
import { CookieKVValue, getMpCookie, setMpCookie } from '~/server/kv/cookie';
// 表示一条 set-cookie 记录的解析结果
export type CookieEntity = Record<string, string | number>;
// 公众号所有的 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<string, CookieEntity>();
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<string, AccountCookie> = new Map<string, AccountCookie>();
// 内存缓存最大条目数,防止无限增长
private readonly maxSize: number = 1000;
async getAccountCookie(authKey: string): Promise<AccountCookie | null> {
// 优先从本地内存取
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<string | null> {
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<boolean> {
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<string | null> {
const accountCookie = await this.getAccountCookie(authKey);
if (!accountCookie) {
return null;
}
return accountCookie.token;
}
/**
* 转换为 json 格式,方便存储与传输
* 返回一个对象,键为 uuid,值为解析后的 cookie 对象
*/
toJSON(): Record<string, AccountCookie> {
const json: Record<string, AccountCookie> = {};
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<string | null> {
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<string | null> {
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;
}