feat: 使用 Playwright 实现自动化 cookie 获取和验证
主要变更: - 新增 Playwright 登录认证服务 (services/playwright-auth.ts) - 重构 get-activity.ts 使用 Playwright 替代 Axios 登录 - 实现自动 cookie 过期检测和重试机制 - 优化 Docker 配置支持 Playwright 浏览器运行 - 添加启动脚本自动验证和刷新 cookies - 完善错误处理:区分 4xx(认证失败) 和 5xx(服务器错误) 技术细节: - 删除旧版 login_template.txt 和 nkcs-engage.cookie.txt - 添加 startup.sh 启动时自动验证 cookies - 改进 cookie 验证逻辑,添加指数退避重试 - Dockerfile 安装 Playwright 系统依赖 - docker-compose.yaml 添加 volumes 和 health checks 测试: - 添加 auth.spec.ts 自动化测试 - 添加 get-cookies.ts 和 test-cookies-validity.ts 工具脚本 - 验证 401/500/000 等错误场景处理正确
This commit is contained in:
32
services/cookies.json
Normal file
32
services/cookies.json
Normal file
@@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"name": "ASP.NET_SessionId",
|
||||
"value": "fjurweoenv1wdcvhcyvhreqh",
|
||||
"domain": "engage.nkcswx.cn",
|
||||
"path": "/",
|
||||
"expires": -1,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "None"
|
||||
},
|
||||
{
|
||||
"name": "usernameCookie",
|
||||
"value": "1hDdyhfXMJP9S2CpwOgJZDKOXrEEXLB%23EXOogV55NRskzh6XKDU2rym1YrGfnoklj",
|
||||
"domain": "engage.nkcswx.cn",
|
||||
"path": "/",
|
||||
"expires": 1778095681.649044,
|
||||
"httpOnly": true,
|
||||
"secure": false,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": ".ASPXFORMSAUTH",
|
||||
"value": "8E4B9D03FE08C5A2C6EB323B35110D6290CCF3408B68940F9783D1F9D37FA326A38457C956652C8A55D68218AA681485AB8C2533E4E7C85B2BF3413847009C18918281566DF940EA26761F8C0424B93AE79F519DDD0585DF19907E1F9231F5C020039960F5BFC53B7D429B1F3F83B5655F83D796",
|
||||
"domain": "engage.nkcswx.cn",
|
||||
"path": "/",
|
||||
"expires": -1,
|
||||
"httpOnly": true,
|
||||
"secure": false,
|
||||
"sameSite": "Lax"
|
||||
}
|
||||
]
|
||||
188
services/playwright-auth.ts
Normal file
188
services/playwright-auth.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { chromium, type BrowserContext, type Cookie } from 'playwright';
|
||||
import { logger } from '../utils/logger';
|
||||
import * as fs from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const LOGIN_URL = 'https://engage.nkcswx.cn/Login.aspx';
|
||||
const COOKIE_FILE_PATH = resolve(import.meta.dir, 'cookies.json');
|
||||
|
||||
let _inMemoryCookies: Cookie[] | null = null;
|
||||
|
||||
/**
|
||||
* Login using Playwright and extract cookies
|
||||
*/
|
||||
export async function loginWithPlaywright(username: string, password: string): Promise<Cookie[]> {
|
||||
logger.info('Starting Playwright login process...');
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
try {
|
||||
const context = await browser.newContext({
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
logger.info(`Navigating to login page: ${LOGIN_URL}`);
|
||||
await page.goto(LOGIN_URL, {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
logger.info('Login page loaded. Filling form...');
|
||||
|
||||
const usernameField = page.locator('input[name="ctl00$PageContent$loginControl$txtUN"]');
|
||||
await usernameField.fill(decodeURIComponent(username));
|
||||
|
||||
const passwordField = page.locator('input[name="ctl00$PageContent$loginControl$txtPwd"]');
|
||||
await passwordField.fill(decodeURIComponent(password));
|
||||
|
||||
const rememberMe = page.locator('input[name="ctl00$PageContent$loginControl$cbRememberMe"]');
|
||||
await rememberMe.check().catch(() => {
|
||||
logger.debug('Could not check remember me checkbox (optional)');
|
||||
});
|
||||
|
||||
const loginButton = page.locator('input[name="ctl00$PageContent$loginControl$btnLogin"]');
|
||||
logger.info('Clicking login button...');
|
||||
await loginButton.click();
|
||||
|
||||
await page.waitForLoadState('networkidle', { timeout: 30000 });
|
||||
|
||||
const isLoggedIn = await checkLoginSuccess(page);
|
||||
|
||||
if (!isLoggedIn) {
|
||||
const errorMessage = await page.locator('.error, .errorMessage, [class*="error"]').first().textContent();
|
||||
throw new Error(`Login failed. Possible error: ${errorMessage || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
logger.info('Login successful! Extracting cookies...');
|
||||
|
||||
const cookies = await context.cookies();
|
||||
logger.info(`Extracted ${cookies.length} cookies`);
|
||||
|
||||
await saveCookiesToCache(cookies);
|
||||
logImportantCookies(cookies);
|
||||
|
||||
await browser.close();
|
||||
return cookies;
|
||||
} catch (error) {
|
||||
logger.error('Error during Playwright login:', error);
|
||||
await browser.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if login was successful
|
||||
*/
|
||||
async function checkLoginSuccess(page: any): Promise<boolean> {
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const currentUrl = page.url();
|
||||
const notOnLoginPage = !currentUrl.includes('Login.aspx');
|
||||
|
||||
const hasLogoutLink = await page.locator('text=Logout, text=退出,text=Sign Out').count() > 0;
|
||||
const hasWelcomeText = await page.locator('text=Welcome, text=欢迎').count() > 0;
|
||||
|
||||
return notOnLoginPage || hasLogoutLink || hasWelcomeText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log important cookies for debugging
|
||||
*/
|
||||
function logImportantCookies(cookies: Cookie[]): void {
|
||||
const importantCookieNames = [
|
||||
'ASP.NET_SessionId',
|
||||
'.ASPXFORMSAUTH',
|
||||
];
|
||||
|
||||
logger.debug('Important cookies:');
|
||||
cookies.forEach(cookie => {
|
||||
if (importantCookieNames.some(name => cookie.name.includes(name))) {
|
||||
logger.debug(` ${cookie.name}: ${cookie.value.substring(0, 50)}${cookie.value.length > 50 ? '...' : ''}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load cookies from cache file
|
||||
*/
|
||||
export async function loadCachedCookies(): Promise<Cookie[] | null> {
|
||||
if (_inMemoryCookies) {
|
||||
logger.debug('Using in-memory cached cookies.');
|
||||
return _inMemoryCookies;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(COOKIE_FILE_PATH)) {
|
||||
logger.debug('Cookie cache file not found. No cached cookies loaded.');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const cookies = JSON.parse(fs.readFileSync(COOKIE_FILE_PATH, 'utf-8')) as Cookie[];
|
||||
_inMemoryCookies = cookies;
|
||||
logger.debug(`Loaded ${cookies.length} cookies from file cache.`);
|
||||
return cookies;
|
||||
} catch (error: any) {
|
||||
logger.warn('Error loading cookies from file:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save cookies to cache file
|
||||
*/
|
||||
export async function saveCookiesToCache(cookies: Cookie[]): Promise<void> {
|
||||
if (!cookies || cookies.length === 0) {
|
||||
logger.warn('Attempted to save empty or null cookies. Aborting save.');
|
||||
return;
|
||||
}
|
||||
|
||||
_inMemoryCookies = cookies;
|
||||
|
||||
try {
|
||||
await fs.promises.writeFile(COOKIE_FILE_PATH, JSON.stringify(cookies, null, 2), 'utf-8');
|
||||
logger.debug('Cookies saved to file cache.');
|
||||
} catch (error: any) {
|
||||
logger.error('Error saving cookies to file:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cookie cache
|
||||
*/
|
||||
export async function clearCookieCache(): Promise<void> {
|
||||
_inMemoryCookies = null;
|
||||
|
||||
try {
|
||||
await fs.promises.unlink(COOKIE_FILE_PATH);
|
||||
logger.debug('Cookie cache file deleted.');
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
logger.error('Error deleting cookie file:', error.message);
|
||||
} else {
|
||||
logger.debug('Cookie cache file did not exist, no need to delete.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert cookies array to cookie string for axios
|
||||
*/
|
||||
export function cookiesToString(cookies: Cookie[]): string {
|
||||
return cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cookie string from cache
|
||||
*/
|
||||
export async function getCachedCookieString(): Promise<string | null> {
|
||||
const cookies = await loadCachedCookies();
|
||||
if (!cookies || cookies.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return cookiesToString(cookies);
|
||||
}
|
||||
Reference in New Issue
Block a user