import dayjs from 'dayjs'; import { H3Event, parseCookies } from 'h3'; import { v4 as uuidv4 } from 'uuid'; import { isDev, USER_AGENT } from '~/config'; import { RequestOptions } from '~/server/types'; import { cookieStore, getCookieFromStore } from '~/server/utils/CookieStore'; import { logRequest, logResponse } from '~/server/utils/logger'; /** * 代理微信公众号请求 * @description 备注:只有登录请求(`action=login`)中的 `set-cookie` 才会被写入到 CookieStore 中 * @param options 请求参数 */ export async function proxyMpRequest(options: RequestOptions) { const runtimeConfig = useRuntimeConfig(); const headers = new Headers({ Referer: 'https://mp.weixin.qq.com/', Origin: 'https://mp.weixin.qq.com', 'User-Agent': USER_AGENT, 'Accept-Encoding': 'identity', // 禁用压缩,避免出现response.clone() bug }); // 优先读取参数中的 cookie,若无则从 CookieStore 中读取 const cookie: string | null = options.cookie || (await getCookieFromStore(options.event)); if (cookie) { headers.set('Cookie', cookie); } const requestInit: RequestInit = { method: options.method, headers: headers, redirect: options.redirect || 'follow', }; // 处理参数 if (options.query) { options.endpoint += '?' + new URLSearchParams(options.query as Record).toString(); } if (options.method === 'POST' && options.body) { requestInit.body = new URLSearchParams(options.body as Record).toString(); } // 构造请求 const request = new Request(options.endpoint, requestInit); // 记录请求报文 const requestId = uuidv4().replace(/-/g, ''); if (process.env.NUXT_DEBUG_MP_REQUEST && isDev) { await logRequest(requestId, request.clone()); } // 转发请求 const mpResponse = await fetch(request); // 记录响应报文 if (process.env.NUXT_DEBUG_MP_REQUEST && isDev) { await logResponse(requestId, mpResponse.clone()); } let setCookies: string[] = []; // 处理登录请求的 uuid cookie if (options.action === 'start_login') { // 提取出 uuid 这个 cookie,并透传给客户端 setCookies = mpResponse.headers.getSetCookie().filter(cookie => cookie.startsWith('uuid=')); } // 处理登录成功请求的 cookie // 只有登录请求才会将 Cookie 数据写入 CookieStore // 返回给客户端的一个 auth-key 的 cookie else if (options.action === 'login') { // 提取出 token 和 cookies try { const authKey = crypto.randomUUID().replace(/-/g, ''); const body = await mpResponse.clone().json(); const redirectUrl = body?.redirect_url; if (!redirectUrl || typeof redirectUrl !== 'string') { throw new Error(`登录响应中未找到 redirect_url,响应内容: ${JSON.stringify(body)}`); } const token = new URL(`http://localhost${redirectUrl}`).searchParams.get('token'); if (!token) { throw new Error(`redirect_url 中未找到 token 参数: ${redirectUrl}`); } console.log('token', token); const success = await cookieStore.setCookie(authKey, token, mpResponse.headers.getSetCookie()); if (!success) { throw new Error('cookie 写入 KV 存储失败'); } console.log('cookie 写入成功'); setCookies = [ `auth-key=${authKey}; Path=/; Expires=${dayjs().add(4, 'days').toString()}; Secure; HttpOnly`, // 登录成功后,删除浏览器的 uuid cookie `uuid=EXPIRED; Path=/; Expires=${dayjs().subtract(1, 'days').toString()}; Secure; HttpOnly`, ]; } catch (error) { console.error('action(login) failed:', error); // 登录失败时返回错误响应,而不是静默继续 return new Response(JSON.stringify({ base_resp: { ret: -1, err_msg: `登录处理失败: ${error}` } }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } } // 处理切换公众号的请求 else if (options.action === 'switch_account') { const authKey = getAuthKeyFromRequest(options.event); if (authKey) { setCookies = ['switch_account=1']; } } // 这里是否需要执行? // 更新 CookieStore 中的 cookie else { // updateCookies(options.event, mpResponse.headers.getSetCookie()); } // 构造返回给客户端的响应 const responseHeaders = new Headers(mpResponse.headers); responseHeaders.delete('set-cookie'); setCookies.forEach(setCookie => { responseHeaders.append('set-cookie', setCookie); }); const finalResponse = new Response(mpResponse.body, { status: mpResponse.status, statusText: mpResponse.statusText, headers: responseHeaders, }); if (!options.parseJson) { return finalResponse; } else { return finalResponse.json(); } } export function getAuthKeyFromRequest(event: H3Event): string { let authKey = getRequestHeader(event, 'X-Auth-Key'); if (!authKey) { const cookies = parseCookies(event); authKey = cookies['auth-key']; } return authKey; } // function updateCookies(event: H3Event, cookies: string[]): void { // const authKey = getAuthKeyFromRequest(event); // if (authKey) { // cookieStore.updateCookie(authKey, cookies); // } // }