server.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. import express from 'express';
  2. import cors from 'cors';
  3. import axios from 'axios';
  4. import { NodeRSA } from 'node-rsa';
  5. import cookieParser from 'cookie-parser';
  6. import { readFileSync } from 'fs';
  7. import { fileURLToPath } from 'url';
  8. import { dirname, join } from 'path';
  9. const __filename = fileURLToPath(import.meta.url);
  10. const __dirname = dirname(__filename);
  11. const app = express();
  12. const PORT = process.env.PORT || 8888;
  13. // 中间件
  14. app.use(cors({
  15. origin: true,
  16. credentials: true
  17. }));
  18. app.use(express.json());
  19. app.use(express.urlencoded({ extended: true }));
  20. app.use(cookieParser());
  21. // 加载自动登录配置
  22. let autoLoginConfig = {};
  23. try {
  24. const configPath = join(__dirname, 'auto-login-config.json');
  25. const configData = readFileSync(configPath, 'utf-8');
  26. autoLoginConfig = JSON.parse(configData);
  27. console.log('已加载自动登录配置:', Object.keys(autoLoginConfig).join(', '));
  28. } catch (error) {
  29. console.error('加载自动登录配置失败:', error.message);
  30. console.log('将使用默认配置');
  31. }
  32. // RSA 加密函数
  33. function encryptWithRSA(text, publicKey) {
  34. const key = new NodeRSA(publicKey);
  35. return key.encrypt(text, 'base64');
  36. }
  37. // 解析 Cookie
  38. function parseCookies(setCookieHeaders) {
  39. return setCookieHeaders.map(cookie => {
  40. const match = cookie.match(/^([^=]+)=([^;]+)/);
  41. if (match) {
  42. const name = match[1];
  43. const value = match[2];
  44. // 提取其他属性
  45. const pathMatch = cookie.match(/Path=([^;]+)/);
  46. const expiresMatch = cookie.match(/Expires=([^;]+)/);
  47. const maxAgeMatch = cookie.match(/Max-Age=([^;]+)/);
  48. const httpOnlyMatch = cookie.match(/HttpOnly/);
  49. const secureMatch = cookie.match(/Secure/);
  50. const sameSiteMatch = cookie.match(/SameSite=([^;]+)/);
  51. return {
  52. name,
  53. value,
  54. path: pathMatch ? pathMatch[1] : '/',
  55. expires: expiresMatch ? expiresMatch[1] : null,
  56. maxAge: maxAgeMatch ? maxAgeMatch[1] : null,
  57. httpOnly: !!httpOnlyMatch,
  58. secure: !!secureMatch,
  59. sameSite: sameSiteMatch ? sameSiteMatch[1] : null
  60. };
  61. }
  62. return null;
  63. }).filter(Boolean);
  64. }
  65. // 生成跳转 HTML
  66. function generateRedirectHTML(cookieData, targetHost, targetDomain) {
  67. return `
  68. <!DOCTYPE html>
  69. <html lang="zh-CN">
  70. <head>
  71. <meta charset="UTF-8">
  72. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  73. <title>自动登录中...</title>
  74. <style>
  75. body {
  76. display: flex;
  77. justify-content: center;
  78. align-items: center;
  79. height: 100vh;
  80. margin: 0;
  81. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  82. background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  83. }
  84. .loading {
  85. text-align: center;
  86. }
  87. .spinner {
  88. border: 4px solid #f3f3f3;
  89. border-top: 4px solid #3498db;
  90. border-radius: 50%;
  91. width: 50px;
  92. height: 50px;
  93. animation: spin 1s linear infinite;
  94. margin: 0 auto 20px;
  95. }
  96. @keyframes spin {
  97. 0% { transform: rotate(0deg); }
  98. 100% { transform: rotate(360deg); }
  99. }
  100. .message {
  101. color: #333;
  102. font-size: 18px;
  103. }
  104. </style>
  105. </head>
  106. <body>
  107. <div class="loading">
  108. <div class="spinner"></div>
  109. <div class="message">正在自动登录,请稍候...</div>
  110. </div>
  111. <iframe id="cookieFrame" style="display:none;"></iframe>
  112. <script>
  113. (function() {
  114. const cookies = ${JSON.stringify(cookieData)};
  115. const targetUrl = 'http://${targetHost}/';
  116. const targetDomain = '${targetDomain}';
  117. console.log('准备设置 Cookie:', cookies);
  118. // 方法1: 尝试直接设置 Cookie(可能因为跨域限制而失败)
  119. cookies.forEach(function(cookie) {
  120. try {
  121. // 构建 Cookie 字符串
  122. let cookieStr = cookie.name + '=' + cookie.value;
  123. cookieStr += '; path=' + cookie.path;
  124. if (cookie.maxAge) {
  125. cookieStr += '; max-age=' + cookie.maxAge;
  126. }
  127. if (cookie.expires) {
  128. cookieStr += '; expires=' + cookie.expires;
  129. }
  130. if (cookie.secure) {
  131. cookieStr += '; secure';
  132. }
  133. if (cookie.sameSite) {
  134. cookieStr += '; samesite=' + cookie.sameSite;
  135. }
  136. // 注意:Domain 属性无法通过 JavaScript 设置跨域 Cookie
  137. // 但我们可以尝试设置(浏览器会忽略跨域的 Domain)
  138. cookieStr += '; domain=' + targetDomain;
  139. document.cookie = cookieStr;
  140. console.log('尝试设置 Cookie:', cookie.name);
  141. } catch(e) {
  142. console.error('设置 Cookie 失败:', cookie.name, e);
  143. }
  144. });
  145. // 方法2: 使用隐藏的 iframe 加载目标站点,让服务器设置 Cookie
  146. // 然后跳转到目标站点
  147. const iframe = document.getElementById('cookieFrame');
  148. iframe.src = targetUrl;
  149. // 延迟跳转,确保 iframe 加载完成
  150. setTimeout(function() {
  151. console.log('跳转到目标站点:', targetUrl);
  152. window.location.href = targetUrl;
  153. }, 1000);
  154. })();
  155. </script>
  156. </body>
  157. </html>
  158. `;
  159. }
  160. // 处理 RSA 加密表单登录
  161. async function handleRSAEncryptedFormLogin(config, credentials) {
  162. const { targetBaseUrl, loginUrl, loginMethodConfig } = config;
  163. const { publicKey, usernameField, passwordField, captchaField, captchaRequired, contentType, successCode, successField } = loginMethodConfig;
  164. // 加密用户名和密码
  165. const usernameEncrypted = encryptWithRSA(credentials.username, publicKey);
  166. const passwordEncrypted = encryptWithRSA(credentials.password, publicKey);
  167. console.log('用户名和密码已加密');
  168. // 构建请求数据
  169. const requestData = {
  170. [usernameField]: usernameEncrypted,
  171. [passwordField]: passwordEncrypted
  172. };
  173. if (captchaField) {
  174. requestData[captchaField] = captchaRequired ? '' : '';
  175. }
  176. // 发送登录请求
  177. const headers = {};
  178. let requestBody;
  179. if (contentType === 'application/x-www-form-urlencoded') {
  180. headers['Content-Type'] = 'application/x-www-form-urlencoded';
  181. requestBody = new URLSearchParams(requestData).toString();
  182. } else if (contentType === 'application/json') {
  183. headers['Content-Type'] = 'application/json';
  184. requestBody = JSON.stringify(requestData);
  185. } else {
  186. requestBody = requestData;
  187. }
  188. const loginResponse = await axios.post(
  189. `${targetBaseUrl}${loginUrl}`,
  190. requestBody,
  191. {
  192. headers,
  193. withCredentials: true,
  194. maxRedirects: 0,
  195. validateStatus: function (status) {
  196. return status >= 200 && status < 400;
  197. }
  198. }
  199. );
  200. // 检查登录是否成功
  201. const responseData = loginResponse.data || {};
  202. const successValue = successField ? responseData[successField] : responseData.code;
  203. if (successValue === successCode) {
  204. return {
  205. success: true,
  206. cookies: loginResponse.headers['set-cookie'] || [],
  207. response: loginResponse.data
  208. };
  209. } else {
  210. return {
  211. success: false,
  212. message: responseData.msg || responseData.message || '登录失败',
  213. response: responseData
  214. };
  215. }
  216. }
  217. // 处理普通表单登录(未加密)
  218. async function handlePlainFormLogin(config, credentials) {
  219. const { targetBaseUrl, loginUrl, loginMethodConfig } = config;
  220. const { usernameField, passwordField, captchaField, contentType, successCode, successField } = loginMethodConfig;
  221. // 构建请求数据
  222. const requestData = {
  223. [usernameField]: credentials.username,
  224. [passwordField]: credentials.password
  225. };
  226. if (captchaField) {
  227. requestData[captchaField] = '';
  228. }
  229. // 发送登录请求
  230. const headers = {};
  231. let requestBody;
  232. if (contentType === 'application/x-www-form-urlencoded') {
  233. headers['Content-Type'] = 'application/x-www-form-urlencoded';
  234. requestBody = new URLSearchParams(requestData).toString();
  235. } else if (contentType === 'application/json') {
  236. headers['Content-Type'] = 'application/json';
  237. requestBody = JSON.stringify(requestData);
  238. } else {
  239. requestBody = requestData;
  240. }
  241. const loginResponse = await axios.post(
  242. `${targetBaseUrl}${loginUrl}`,
  243. requestBody,
  244. {
  245. headers,
  246. withCredentials: true,
  247. maxRedirects: 0,
  248. validateStatus: function (status) {
  249. return status >= 200 && status < 400;
  250. }
  251. }
  252. );
  253. // 检查登录是否成功
  254. const responseData = loginResponse.data || {};
  255. const successValue = successField ? responseData[successField] : responseData.code;
  256. if (successValue === successCode) {
  257. return {
  258. success: true,
  259. cookies: loginResponse.headers['set-cookie'] || [],
  260. response: loginResponse.data
  261. };
  262. } else {
  263. return {
  264. success: false,
  265. message: responseData.msg || responseData.message || '登录失败',
  266. response: responseData
  267. };
  268. }
  269. }
  270. // 通用的自动登录端点
  271. app.get('/api/auto-login/:siteId', async (req, res) => {
  272. try {
  273. const { siteId } = req.params;
  274. console.log(`开始自动登录流程,网站ID: ${siteId}`);
  275. // 获取网站配置
  276. const config = autoLoginConfig[siteId];
  277. if (!config) {
  278. return res.status(404).json({
  279. success: false,
  280. message: `未找到网站ID "${siteId}" 的配置`
  281. });
  282. }
  283. console.log(`网站名称: ${config.name}`);
  284. // 获取登录凭据(优先使用环境变量)
  285. const credentials = {
  286. username: process.env[config.credentials.envUsername] || config.credentials.username,
  287. password: process.env[config.credentials.envPassword] || config.credentials.password
  288. };
  289. if (!credentials.username || !credentials.password) {
  290. return res.status(400).json({
  291. success: false,
  292. message: '登录凭据未配置'
  293. });
  294. }
  295. // 根据登录方法处理登录
  296. let loginResult;
  297. switch (config.loginMethod) {
  298. case 'rsa-encrypted-form':
  299. loginResult = await handleRSAEncryptedFormLogin(config, credentials);
  300. break;
  301. case 'plain-form':
  302. loginResult = await handlePlainFormLogin(config, credentials);
  303. break;
  304. default:
  305. return res.status(400).json({
  306. success: false,
  307. message: `不支持的登录方法: ${config.loginMethod}`
  308. });
  309. }
  310. if (!loginResult.success) {
  311. console.error('登录失败:', loginResult.message);
  312. return res.status(500).json({
  313. success: false,
  314. message: loginResult.message
  315. });
  316. }
  317. console.log('登录成功!');
  318. // 解析 Cookie
  319. const cookieData = parseCookies(loginResult.cookies);
  320. console.log('获取到 Cookie 数量:', cookieData.length);
  321. // 生成跳转 HTML
  322. const html = generateRedirectHTML(
  323. cookieData,
  324. config.targetHost,
  325. config.targetDomain
  326. );
  327. // 在响应头中设置 Cookie
  328. loginResult.cookies.forEach(cookie => {
  329. // 修改 Cookie 的 Domain,移除端口号
  330. let modifiedCookie = cookie.replace(/Domain=[^;]+/i, `Domain=${config.targetDomain}`);
  331. res.setHeader('Set-Cookie', modifiedCookie);
  332. });
  333. res.send(html);
  334. } catch (error) {
  335. console.error('自动登录错误:', error.message);
  336. if (error.response) {
  337. console.error('响应状态:', error.response.status);
  338. console.error('响应数据:', error.response.data);
  339. }
  340. res.status(500).json({
  341. success: false,
  342. message: '自动登录失败: ' + error.message
  343. });
  344. }
  345. });
  346. // 获取所有配置的网站列表
  347. app.get('/api/auto-login', (req, res) => {
  348. const sites = Object.keys(autoLoginConfig).map(siteId => ({
  349. id: siteId,
  350. name: autoLoginConfig[siteId].name,
  351. endpoint: `/api/auto-login/${siteId}`
  352. }));
  353. res.json({ sites });
  354. });
  355. // 健康检查端点
  356. app.get('/api/health', (req, res) => {
  357. res.json({ status: 'ok' });
  358. });
  359. app.listen(PORT, '0.0.0.0', () => {
  360. console.log(`后端服务器运行在 http://0.0.0.0:${PORT}`);
  361. console.log(`已配置的自动登录网站: ${Object.keys(autoLoginConfig).join(', ') || '无'}`);
  362. console.log(`自动登录端点格式: http://0.0.0.0:${PORT}/api/auto-login/:siteId`);
  363. });