Spaces:
Sleeping
Sleeping
File size: 9,174 Bytes
6e41657 | 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 | 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;
}
|