|
|
@@ -458,329 +458,136 @@ async function handleRSAEncryptedFormLogin(config, credentials) {
|
|
|
async function handleHomeAssistantLogin(config, credentials) {
|
|
|
const { targetBaseUrl } = config;
|
|
|
|
|
|
- console.log('=== Home Assistant 登录 ===');
|
|
|
+ console.log('=== Home Assistant 登录 (OAuth2 模式) ===');
|
|
|
console.log(`目标URL: ${targetBaseUrl}`);
|
|
|
console.log(`用户名: ${credentials.username}`);
|
|
|
console.log(`密码: ${'*'.repeat(credentials.password.length)}`);
|
|
|
|
|
|
- // 创建 Cookie jar 来保持会话
|
|
|
- const cookieJar = new CookieJar();
|
|
|
- const client = wrapper(axios.create({
|
|
|
- jar: cookieJar,
|
|
|
- withCredentials: true
|
|
|
- }));
|
|
|
-
|
|
|
const baseHeaders = {
|
|
|
'Content-Type': 'application/json',
|
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
|
'Accept': 'application/json, text/plain, */*',
|
|
|
- 'Origin': targetBaseUrl,
|
|
|
- 'Referer': `${targetBaseUrl}/`
|
|
|
+ 'Origin': targetBaseUrl
|
|
|
};
|
|
|
|
|
|
- // 先访问首页获取初始 Cookie(如果需要)
|
|
|
- let initialCookies = [];
|
|
|
- try {
|
|
|
- console.log('访问首页获取初始 Cookie...');
|
|
|
- const homeResponse = await client.get(`${targetBaseUrl}/`, {
|
|
|
- headers: {
|
|
|
- 'User-Agent': baseHeaders['User-Agent']
|
|
|
- },
|
|
|
- maxRedirects: 5
|
|
|
- });
|
|
|
- initialCookies = homeResponse.headers['set-cookie'] || [];
|
|
|
- console.log(`获取到 ${initialCookies.length} 个初始 Cookie`);
|
|
|
- // 从 Cookie jar 中获取 Cookie
|
|
|
- const jarCookies = await cookieJar.getCookies(targetBaseUrl);
|
|
|
- console.log(`Cookie jar 中有 ${jarCookies.length} 个 Cookie`);
|
|
|
- } catch (error) {
|
|
|
- console.log('访问首页失败(可能不需要):', error.message);
|
|
|
- }
|
|
|
-
|
|
|
- // 步骤1: 创建登录流程
|
|
|
- console.log('步骤1: 创建登录流程...');
|
|
|
- let flowResponse;
|
|
|
try {
|
|
|
- flowResponse = await client.post(
|
|
|
+ // ==========================================
|
|
|
+ // 步骤1: 创建登录流程 (Init Flow)
|
|
|
+ // ==========================================
|
|
|
+ console.log('步骤1: 创建登录流程...');
|
|
|
+ const flowResponse = await axios.post(
|
|
|
`${targetBaseUrl}/auth/login_flow`,
|
|
|
{
|
|
|
- client_id: `${targetBaseUrl}/`,
|
|
|
+ client_id: `${targetBaseUrl}/`, // clientId 必须和浏览器访问的一致
|
|
|
handler: ['homeassistant', null],
|
|
|
- redirect_uri: `${targetBaseUrl}/`
|
|
|
+ redirect_uri: `${targetBaseUrl}/?auth_callback=1`
|
|
|
},
|
|
|
- {
|
|
|
+ {
|
|
|
headers: baseHeaders,
|
|
|
- maxRedirects: 0,
|
|
|
validateStatus: function (status) {
|
|
|
return status >= 200 && status < 500;
|
|
|
}
|
|
|
}
|
|
|
);
|
|
|
- } catch (error) {
|
|
|
- console.error('创建登录流程失败:', error.message);
|
|
|
- if (error.response) {
|
|
|
- console.error('响应状态:', error.response.status);
|
|
|
- console.error('响应数据:', JSON.stringify(error.response.data, null, 2));
|
|
|
+
|
|
|
+ console.log(`流程创建响应状态码: ${flowResponse.status}`);
|
|
|
+ console.log(`流程创建响应数据:`, JSON.stringify(flowResponse.data, null, 2));
|
|
|
+
|
|
|
+ if (flowResponse.status !== 200) {
|
|
|
return {
|
|
|
success: false,
|
|
|
- message: `创建登录流程失败: ${error.response.status} - ${JSON.stringify(error.response.data)}`,
|
|
|
- response: error.response.data
|
|
|
+ message: `创建登录流程失败,状态码: ${flowResponse.status}`,
|
|
|
+ response: flowResponse.data
|
|
|
};
|
|
|
}
|
|
|
- return {
|
|
|
- success: false,
|
|
|
- message: `创建登录流程失败: ${error.message}`,
|
|
|
- response: null
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- console.log(`流程创建响应状态码: ${flowResponse.status}`);
|
|
|
- console.log(`流程创建响应数据:`, JSON.stringify(flowResponse.data, null, 2));
|
|
|
-
|
|
|
- if (flowResponse.status !== 200) {
|
|
|
- return {
|
|
|
- success: false,
|
|
|
- message: `创建登录流程失败,状态码: ${flowResponse.status}`,
|
|
|
- response: flowResponse.data
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- const flowId = flowResponse.data?.flow_id;
|
|
|
- if (!flowId) {
|
|
|
- console.error('无法获取 flow_id');
|
|
|
- return {
|
|
|
- success: false,
|
|
|
- message: '无法获取 flow_id',
|
|
|
- response: flowResponse.data
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- console.log(`获取到 flow_id: ${flowId}`);
|
|
|
-
|
|
|
- // 步骤2: 提交用户名和密码
|
|
|
- console.log('步骤2: 提交用户名和密码...');
|
|
|
- let loginResponse;
|
|
|
- try {
|
|
|
- loginResponse = await client.post(
|
|
|
+
|
|
|
+ const flowId = flowResponse.data?.flow_id;
|
|
|
+ if (!flowId) {
|
|
|
+ console.error('无法获取 flow_id');
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: '无法获取 flow_id',
|
|
|
+ response: flowResponse.data
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(`获取到 flow_id: ${flowId}`);
|
|
|
+
|
|
|
+ // ==========================================
|
|
|
+ // 步骤2: 提交用户名和密码 (Submit Credentials)
|
|
|
+ // ==========================================
|
|
|
+ console.log('步骤2: 提交用户名和密码...');
|
|
|
+ const loginResponse = await axios.post(
|
|
|
`${targetBaseUrl}/auth/login_flow/${flowId}`,
|
|
|
{
|
|
|
username: credentials.username,
|
|
|
password: credentials.password,
|
|
|
- client_id: `${targetBaseUrl}/`
|
|
|
+ client_id: `${targetBaseUrl}/` // 保持一致
|
|
|
},
|
|
|
- {
|
|
|
+ {
|
|
|
headers: baseHeaders,
|
|
|
- maxRedirects: 0,
|
|
|
validateStatus: function (status) {
|
|
|
return status >= 200 && status < 500;
|
|
|
}
|
|
|
}
|
|
|
);
|
|
|
+
|
|
|
+ console.log(`登录响应状态码: ${loginResponse.status}`);
|
|
|
+ console.log(`登录响应数据:`, JSON.stringify(loginResponse.data, null, 2));
|
|
|
+
|
|
|
+ // ==========================================
|
|
|
+ // 步骤3: 构造浏览器跳转 URL (关键修正)
|
|
|
+ // ==========================================
|
|
|
+ const responseData = loginResponse.data || {};
|
|
|
+ const responseType = responseData.type;
|
|
|
+
|
|
|
+ console.log(`响应类型: ${responseType}`);
|
|
|
+
|
|
|
+ // 如果登录成功,type 通常是 'create_entry'
|
|
|
+ if (responseData.result) {
|
|
|
+ const authCode = responseData.result;
|
|
|
+ console.log(`登录成功!获取到 Authorization Code: ${authCode}`);
|
|
|
+
|
|
|
+ // 构造魔术链接,让浏览器去完成 Token 交换
|
|
|
+ // 格式: http://ha-url/?auth_callback=1&code=YOUR_CODE
|
|
|
+ const redirectUrl = `${targetBaseUrl}/?auth_callback=1&code=${encodeURIComponent(authCode)}`;
|
|
|
+
|
|
|
+ console.log('生成自动登录跳转链接:', redirectUrl);
|
|
|
+ console.log('Home Assistant 前端将自动识别 URL 中的 code 并完成 Token 交换');
|
|
|
+
|
|
|
+ // 返回跳转 URL,让浏览器直接跳转
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ redirectUrl: redirectUrl, // 告诉上层直接跳转这个 URL
|
|
|
+ cookies: [], // HA 不需要 Cookie
|
|
|
+ response: loginResponse.data
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ console.error('登录未返回 Authorization Code:', responseData);
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: '登录失败: 未返回授权码',
|
|
|
+ response: responseData
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
} catch (error) {
|
|
|
- console.error('提交登录信息失败:', error.message);
|
|
|
+ console.error('Home Assistant 登录流程异常:', error.message);
|
|
|
if (error.response) {
|
|
|
console.error('响应状态:', error.response.status);
|
|
|
console.error('响应数据:', JSON.stringify(error.response.data, null, 2));
|
|
|
return {
|
|
|
success: false,
|
|
|
- message: `提交登录信息失败: ${error.response.status} - ${JSON.stringify(error.response.data)}`,
|
|
|
+ message: `登录失败: ${error.response.status} - ${JSON.stringify(error.response.data)}`,
|
|
|
response: error.response.data
|
|
|
};
|
|
|
}
|
|
|
return {
|
|
|
success: false,
|
|
|
- message: `提交登录信息失败: ${error.message}`,
|
|
|
+ message: `登录失败: ${error.message}`,
|
|
|
response: null
|
|
|
};
|
|
|
}
|
|
|
-
|
|
|
- console.log(`登录响应状态码: ${loginResponse.status}`);
|
|
|
- console.log(`登录响应数据:`, JSON.stringify(loginResponse.data, null, 2));
|
|
|
-
|
|
|
- // 检查登录是否成功
|
|
|
- const responseData = loginResponse.data || {};
|
|
|
- const responseType = responseData.type;
|
|
|
-
|
|
|
- console.log(`响应类型: ${responseType}`);
|
|
|
-
|
|
|
- // Home Assistant 登录成功时,type 为 "create_entry"
|
|
|
- if (responseType === 'create_entry') {
|
|
|
- // 从 Cookie jar 中获取所有 Cookie
|
|
|
- console.log('从 Cookie jar 中获取 Cookie...');
|
|
|
- const jarCookies = await cookieJar.getCookies(targetBaseUrl);
|
|
|
- console.log(`Cookie jar 中有 ${jarCookies.length} 个 Cookie`);
|
|
|
-
|
|
|
- // 合并所有请求的 Cookie(从响应头和 Cookie jar)
|
|
|
- const flowCookies = flowResponse.headers['set-cookie'] || [];
|
|
|
- const loginCookies = loginResponse.headers['set-cookie'] || [];
|
|
|
- const allCookies = [...initialCookies, ...flowCookies, ...loginCookies];
|
|
|
-
|
|
|
- // 从 Cookie jar 转换为 Set-Cookie 格式
|
|
|
- jarCookies.forEach(cookie => {
|
|
|
- let cookieStr = `${cookie.key}=${cookie.value}`;
|
|
|
- if (cookie.path) cookieStr += `; Path=${cookie.path}`;
|
|
|
- if (cookie.domain) cookieStr += `; Domain=${cookie.domain}`;
|
|
|
- if (cookie.expires) cookieStr += `; Expires=${cookie.expires.toUTCString()}`;
|
|
|
- if (cookie.maxAge) cookieStr += `; Max-Age=${cookie.maxAge}`;
|
|
|
- if (cookie.secure) cookieStr += `; Secure`;
|
|
|
- if (cookie.httpOnly) cookieStr += `; HttpOnly`;
|
|
|
- if (cookie.sameSite) cookieStr += `; SameSite=${cookie.sameSite}`;
|
|
|
- allCookies.push(cookieStr);
|
|
|
- });
|
|
|
-
|
|
|
- // 去重 Cookie(保留最后一个)
|
|
|
- const cookieMap = new Map();
|
|
|
- allCookies.forEach(cookie => {
|
|
|
- const name = cookie.split('=')[0];
|
|
|
- cookieMap.set(name, cookie);
|
|
|
- });
|
|
|
- let uniqueCookies = Array.from(cookieMap.values());
|
|
|
-
|
|
|
- console.log(`登录成功!获取到 ${uniqueCookies.length} 个唯一 Cookie`);
|
|
|
- uniqueCookies.forEach((cookie, index) => {
|
|
|
- console.log(`Cookie ${index + 1}: ${cookie.substring(0, 100)}...`);
|
|
|
- });
|
|
|
-
|
|
|
- // 步骤3: 处理 OAuth2 授权流程
|
|
|
- // 登录成功后,Home Assistant 需要完成 OAuth2 授权才能访问主页面
|
|
|
- console.log('步骤3: 处理 OAuth2 授权流程...');
|
|
|
- try {
|
|
|
- // 构建 state 参数(base64 编码的 JSON)
|
|
|
- const stateData = {
|
|
|
- hassUrl: targetBaseUrl,
|
|
|
- clientId: `${targetBaseUrl}/`
|
|
|
- };
|
|
|
- const state = Buffer.from(JSON.stringify(stateData)).toString('base64');
|
|
|
-
|
|
|
- // 构建授权 URL
|
|
|
- const redirectUri = `${targetBaseUrl}/?auth_callback=1`;
|
|
|
- const clientId = `${targetBaseUrl}/`;
|
|
|
- const authorizeUrl = `${targetBaseUrl}/auth/authorize?response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&client_id=${encodeURIComponent(clientId)}&state=${encodeURIComponent(state)}`;
|
|
|
-
|
|
|
- console.log(`访问授权端点: ${authorizeUrl}`);
|
|
|
-
|
|
|
- // 使用 Cookie jar 的客户端访问授权端点,跟随重定向
|
|
|
- const authorizeResponse = await client.get(authorizeUrl, {
|
|
|
- headers: {
|
|
|
- 'User-Agent': baseHeaders['User-Agent'],
|
|
|
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
|
|
- 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
|
|
- 'Referer': `${targetBaseUrl}/`
|
|
|
- },
|
|
|
- maxRedirects: 10, // 增加重定向次数
|
|
|
- validateStatus: function (status) {
|
|
|
- return status >= 200 && status < 400;
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- console.log(`授权响应状态码: ${authorizeResponse.status}`);
|
|
|
- const finalUrl = authorizeResponse.request?.res?.responseUrl || authorizeResponse.config?.url || authorizeUrl;
|
|
|
- console.log(`授权最终 URL: ${finalUrl}`);
|
|
|
-
|
|
|
- // 检查是否是重定向到回调 URL
|
|
|
- if (finalUrl.includes('auth_callback=1') || finalUrl.includes('code=')) {
|
|
|
- console.log('检测到授权回调,访问回调 URL 获取 Cookie...');
|
|
|
- try {
|
|
|
- const callbackResponse = await client.get(finalUrl, {
|
|
|
- headers: {
|
|
|
- 'User-Agent': baseHeaders['User-Agent'],
|
|
|
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
|
|
- 'Referer': authorizeUrl
|
|
|
- },
|
|
|
- maxRedirects: 5,
|
|
|
- validateStatus: function (status) {
|
|
|
- return status >= 200 && status < 400;
|
|
|
- }
|
|
|
- });
|
|
|
- console.log(`回调响应状态码: ${callbackResponse.status}`);
|
|
|
- } catch (callbackError) {
|
|
|
- console.log('访问回调 URL 失败:', callbackError.message);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 尝试访问主页面来触发 Cookie 设置
|
|
|
- console.log('访问主页面以触发 Cookie 设置...');
|
|
|
- try {
|
|
|
- const homePageResponse = await client.get(`${targetBaseUrl}/`, {
|
|
|
- headers: {
|
|
|
- 'User-Agent': baseHeaders['User-Agent'],
|
|
|
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
|
|
- 'Referer': finalUrl
|
|
|
- },
|
|
|
- maxRedirects: 5,
|
|
|
- validateStatus: function (status) {
|
|
|
- return status >= 200 && status < 400;
|
|
|
- }
|
|
|
- });
|
|
|
- console.log(`主页面响应状态码: ${homePageResponse.status}`);
|
|
|
- } catch (homeError) {
|
|
|
- console.log('访问主页面失败:', homeError.message);
|
|
|
- }
|
|
|
-
|
|
|
- // 从 Cookie jar 中获取授权后的 Cookie
|
|
|
- const authorizeJarCookies = await cookieJar.getCookies(targetBaseUrl);
|
|
|
- console.log(`授权后 Cookie jar 中有 ${authorizeJarCookies.length} 个 Cookie`);
|
|
|
-
|
|
|
- // 获取授权响应中的 Cookie
|
|
|
- const authorizeCookies = authorizeResponse.headers['set-cookie'] || [];
|
|
|
- console.log(`授权响应获取到 ${authorizeCookies.length} 个 Cookie`);
|
|
|
-
|
|
|
- // 从 Cookie jar 转换为 Set-Cookie 格式并合并
|
|
|
- authorizeJarCookies.forEach(cookie => {
|
|
|
- let cookieStr = `${cookie.key}=${cookie.value}`;
|
|
|
- if (cookie.path) cookieStr += `; Path=${cookie.path}`;
|
|
|
- if (cookie.domain) cookieStr += `; Domain=${cookie.domain}`;
|
|
|
- if (cookie.expires) cookieStr += `; Expires=${cookie.expires.toUTCString()}`;
|
|
|
- if (cookie.maxAge) cookieStr += `; Max-Age=${cookie.maxAge}`;
|
|
|
- if (cookie.secure) cookieStr += `; Secure`;
|
|
|
- if (cookie.httpOnly) cookieStr += `; HttpOnly`;
|
|
|
- if (cookie.sameSite) cookieStr += `; SameSite=${cookie.sameSite}`;
|
|
|
- cookieMap.set(cookie.key, cookieStr);
|
|
|
- });
|
|
|
-
|
|
|
- // 合并授权响应中的 Cookie
|
|
|
- authorizeCookies.forEach(cookie => {
|
|
|
- const name = cookie.split('=')[0];
|
|
|
- cookieMap.set(name, cookie);
|
|
|
- });
|
|
|
-
|
|
|
- uniqueCookies = Array.from(cookieMap.values());
|
|
|
-
|
|
|
- console.log(`授权完成!最终获取到 ${uniqueCookies.length} 个唯一 Cookie`);
|
|
|
-
|
|
|
- // 如果还是没有 Cookie,尝试使用登录结果中的 token
|
|
|
- if (uniqueCookies.length === 0 && responseData.result) {
|
|
|
- console.log(`尝试使用登录结果中的 token: ${responseData.result}`);
|
|
|
- // 这里可以尝试使用 token 来获取访问令牌
|
|
|
- // 但 Home Assistant 的 token 通常需要通过浏览器来设置 Cookie
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.log('授权流程失败(可能不需要):', error.message);
|
|
|
- if (error.response) {
|
|
|
- console.log('授权响应状态:', error.response.status);
|
|
|
- console.log('授权响应 URL:', error.response.request?.res?.responseUrl || error.config?.url);
|
|
|
- }
|
|
|
- // 授权失败不影响登录,继续使用已有的 Cookie
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- success: true,
|
|
|
- cookies: uniqueCookies,
|
|
|
- response: loginResponse.data
|
|
|
- };
|
|
|
- } else {
|
|
|
- console.error(`登录失败!响应:`, responseData);
|
|
|
- const errorMessage = responseData.errors?.base?.[0]
|
|
|
- || responseData.errors?.username?.[0]
|
|
|
- || responseData.errors?.password?.[0]
|
|
|
- || responseData.message
|
|
|
- || `登录失败,响应类型: ${responseType}`;
|
|
|
- return {
|
|
|
- success: false,
|
|
|
- message: errorMessage,
|
|
|
- response: responseData
|
|
|
- };
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
// 处理普通表单登录(未加密)
|
|
|
@@ -1027,6 +834,16 @@ app.get('/api/auto-login/:siteId', async (req, res) => {
|
|
|
|
|
|
console.log(`[${requestId}] 登录成功!`);
|
|
|
|
|
|
+ // 对于 Home Assistant,如果返回了 redirectUrl,直接重定向
|
|
|
+ if (config.loginMethod === 'home-assistant' && loginResult.redirectUrl) {
|
|
|
+ console.log(`[${requestId}] Home Assistant 登录成功,直接重定向到: ${loginResult.redirectUrl}`);
|
|
|
+ console.log(`[${requestId}] 总耗时: ${Date.now() - startTime}ms`);
|
|
|
+ console.log('='.repeat(80) + '\n');
|
|
|
+
|
|
|
+ // 直接重定向到带有 auth code 的 URL
|
|
|
+ return res.redirect(loginResult.redirectUrl);
|
|
|
+ }
|
|
|
+
|
|
|
// 解析 Cookie
|
|
|
const cookieData = parseCookies(loginResult.cookies);
|
|
|
console.log(`[${requestId}] 解析到 ${cookieData.length} 个 Cookie:`);
|
|
|
@@ -1034,24 +851,8 @@ app.get('/api/auto-login/:siteId', async (req, res) => {
|
|
|
console.log(`[${requestId}] Cookie ${index + 1}: ${cookie.name} = ${cookie.value.substring(0, 20)}...`);
|
|
|
});
|
|
|
|
|
|
- // 生成跳转 HTML(添加更多调试信息)
|
|
|
- // 对于 Home Assistant,如果没有 Cookie,让浏览器端完成登录
|
|
|
+ // 生成跳转 HTML
|
|
|
let redirectUrl = `http://${config.targetHost}/`;
|
|
|
- let homeAssistantLoginData = null;
|
|
|
-
|
|
|
- if (config.loginMethod === 'home-assistant' && cookieData.length === 0) {
|
|
|
- console.log(`[${requestId}] Home Assistant 无 Cookie,准备在浏览器端完成登录流程`);
|
|
|
- // 传递登录信息给浏览器端
|
|
|
- homeAssistantLoginData = {
|
|
|
- targetBaseUrl: config.targetBaseUrl,
|
|
|
- username: credentials.username,
|
|
|
- password: credentials.password,
|
|
|
- // 如果后端已获取到 result token,也传递过去
|
|
|
- resultToken: loginResult.response?.result
|
|
|
- };
|
|
|
- redirectUrl = config.targetBaseUrl;
|
|
|
- console.log(`[${requestId}] 将在浏览器端执行 Home Assistant 登录流程`);
|
|
|
- }
|
|
|
|
|
|
console.log(`[${requestId}] 生成跳转页面,目标: ${redirectUrl}`);
|
|
|
const html = generateRedirectHTML(
|
|
|
@@ -1060,7 +861,7 @@ app.get('/api/auto-login/:siteId', async (req, res) => {
|
|
|
config.targetDomain,
|
|
|
requestId,
|
|
|
redirectUrl,
|
|
|
- homeAssistantLoginData
|
|
|
+ null
|
|
|
);
|
|
|
|
|
|
// 在响应头中设置 Cookie
|