Files
eSIM-Tools/netlify/functions/verify-cookie.js
Abner 33b8e20440 ♻️ refactor: 抽取内部请求头构建并规范 Cookie 验证返回语义
- 新增共享模块 netlify/functions/_shared/internal-headers.js,集中构建内部函数互调请求头,统一 ACCESS_KEY 未配置时的 500 抛错逻辑
- 更新 giffgaff-graphql.js / giffgaff-mfa-challenge.js / giffgaff-mfa-validation.js,移除各自重复的 getInternalHeaders 实现,改为复用 getInternalHeaders
- 调整 verify-cookie.js 返回语义:以 success 表示 HTTP 层请求是否成功、以 valid 表示是否具备可用于后续 API 的 JWT 令牌
- 修正 verify-cookie.js 错误提示文案中的乱码问题,确保提示“无法通过Cookie获取访问令牌”表述正确
2026-05-31 20:30:31 +08:00

181 lines
5.2 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, context, { auth, body }) => {
// 输入验证
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 字段作为能否继续的唯一判断依据。
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;
}
}