Files
eSIM-Tools/netlify/functions/verify-cookie.js
Abner 574506bcbf feat: 添加结构化日志支持并优化错误处理
- 在 netlify/functions/_shared/server-logger.js 中重构日志函数,统一日志输出格式,支持 context 字段覆盖基础字段(如 message)
- 在 netlify/functions/_shared/middleware.js 中增强 withAuth 中间件,注入结构化 logger 到上下文,增加 request_start / request_end / request_error 日志追踪,并记录请求耗时
- 为所有 Netlify Function(giffgaff-sms-activate、giffgaff-graphql、auto-activate-esim、verify-cookie、giffgaff-token-exchange、giffgaff-mfa-challenge、giffgaff-mfa-validation、health)添加结构化日志,替换原有 console 输出,包含请求入参、状态和耗时
- 在 netlify/edge-functions/bff-proxy.js 和 markdown-negotiation.js 中实现 Deno 内联结构化日志,生成 requestId 并统一日志输出格式,支持 INFO/WARN/ERROR/DEBUG 级别控制
- 更新测试用例 tests/modules/server-logger.test.js 和 tests/functions/middleware-logging.test.js,新增日志级别过滤、context 覆盖、多实例隔离、OPTIONS 预检处理等测试场景
2026-06-26 21:28:21 +08:00

192 lines
5.5 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.
/**
* Netlify Function: Cookie验证服务
* 将Giffgaff Cookie转换为Access Token尽可能接近真实登录状态
*/
const axios = require('axios');
const cheerio = require('cheerio');
const { withAuth, validateInput, AuthError } = require('./_shared/middleware');
const { isRateLimited } = require('./_shared/rate-limiter');
// 输入验证schema
const verifyCookieSchema = {
cookie: {
required: true,
type: 'string',
minLength: 10,
maxLength: 8192
}
};
exports.handler = withAuth(async (event, ctx, { auth, body }) => {
const logger = ctx.logger;
const startTime = Date.now();
logger.info('invoked', { hasCookie: !!body.cookie });
// 输入验证
validateInput(verifyCookieSchema, body);
// 限流检查(优先使用 Netlify Blobs降级为内存限流
const ip = (event.headers['x-forwarded-for'] || '').split(',')[0] ||
event.headers['client-ip'] ||
event.headers['x-real-ip'] ||
'unknown';
if (await isRateLimited(ip)) {
throw new AuthError('请求过于频繁,请稍后再试', 429);
}
const { cookie } = body;
// 验证Cookie并获取Access Token
const result = await validateCookieAndGetToken(cookie);
// success: Cookie 验证请求本身是否成功HTTP 层面)
// valid: Cookie 是否可用于后续 API 调用(需同时拿到可用的 JWT 令牌)
// 调用方(前端 & 内部函数)应以 valid 字段作为能否继续的唯一判断依据。
logger.info('cookie_verify_result', {
success: result.success,
hasAccessToken: !!result.accessToken,
duration: Date.now() - startTime,
});
if (result.success) {
const looksLikeJwt = typeof result.accessToken === 'string' &&
result.accessToken.includes('.') &&
result.accessToken.length > 200;
// Cookie 验证通过但未拿到可用 JWT → success=true, valid=false
// 表示服务可达、Cookie 有效,但缺少可用于 API 的令牌(需走 OAuth 或补充会话)
if (!looksLikeJwt) {
return {
statusCode: 200,
body: JSON.stringify({
success: true,
valid: false,
accessToken: null,
memberId: result.memberId || null,
emailSignature: null,
message: 'Cookie验证通过但未获取可用于API的访问令牌请使用OAuth或确保包含 id.giffgaff.com 域的完整会话后重试'
})
};
}
// Cookie 验证通过且拿到可用 JWT → success=true, valid=true
return {
statusCode: 200,
body: JSON.stringify({
success: true,
valid: true,
accessToken: result.accessToken,
memberId: result.memberId || null,
emailSignature: null,
message: 'Cookie验证成功'
})
};
}
// Cookie 验证失败 → success=false, valid=false
return {
statusCode: 200,
body: JSON.stringify({
success: false,
valid: false,
accessToken: null,
emailSignature: null,
message: result.error || '无法通过Cookie获取访问令牌'
})
};
}, {
validateSchema: verifyCookieSchema
});
/**
* 验证Cookie并尝试获取Access Token
*/
async function validateCookieAndGetToken(cookie) {
try {
// 访问主页尝试从HTML中提取登录状态或令牌
const response = await axios.get('https://www.giffgaff.com/', {
headers: {
'Cookie': cookie,
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
},
timeout: 15000
});
const $ = cheerio.load(response.data);
// 尝试从页面中提取member ID
const memberId = extractMemberId($, response.data);
// 尝试从Cookie中提取或通过后续请求获取token
const accessToken = await extractAccessToken(cookie);
return {
success: true,
valid: !!memberId,
memberId,
accessToken
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* 从页面中提取Member ID
*/
function extractMemberId($, html) {
// 尝试多种方法提取member ID
const metaMemberId = $('meta[name="member-id"]').attr('content');
if (metaMemberId) return metaMemberId;
const dataGgMember = $('[data-gg-member]').attr('data-gg-member');
if (dataGgMember) return dataGgMember;
const matchMemberId = html.match(/memberId["\s:]+(\d+)/i);
if (matchMemberId && matchMemberId[1]) return matchMemberId[1];
return null;
}
/**
* 尝试提取或获取Access Token
*/
async function extractAccessToken(cookie) {
try {
// 尝试访问需要认证的API端点来触发token生成
const apiResponse = await axios.get('https://www.giffgaff.com/auth/user-status', {
headers: {
'Cookie': cookie,
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
'Accept': 'application/json'
},
timeout: 10000
});
// 检查响应中是否包含token
const token = apiResponse.data?.accessToken ||
apiResponse.data?.token ||
apiResponse.headers['x-access-token'];
return token || null;
} catch (error) {
// 尝试从Set-Cookie中提取
const setCookie = error.response?.headers['set-cookie'];
if (setCookie && Array.isArray(setCookie)) {
const tokenCookie = setCookie.find(c => c.includes('access_token='));
if (tokenCookie) {
const match = tokenCookie.match(/access_token=([^;]+)/);
return match ? match[1] : null;
}
}
return null;
}
}