背景
由于公司中台系统没有自己的登录入口,登录需要跳转至登陆中心进行登录,但是在开发项目时本地启服务项目中判断未登录会自动跳转至对应登录中心的域名页面,需要f12将登录的token手动复制出来后再写死到项目中进行开发,非常繁琐,所以想在运行开发项目指令时自动登录实现token写入功能;
实现思路
编写js自动登录脚本,脚本主要作用就是获取到token并且写入项目的对应js文件中,在运行项目开发脚本npm run dev前先运行自动登录js脚本,然后项目正常启动;
puppeteer介绍
uppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行,但是可以通过修改配置文件运行“有头”模式。
获取登录token主要采用puppeteer打开无头浏览器模拟用户登陆操作,监听登录接口的返回,然后在命令行接收输入的环境,用户名,密码来模拟登录,脚本中拿到登录信息之后再写入项目,脚本中应有登录失败后的重新输入机制;
代码实现
// auto-login.js
const fs = require('fs');
const puppeteer = require('puppeteer');
const readlineSync = require('readline-sync')
// const { createWorker } = require('tesseract.js');
const URL_JS_FILE_PATH = './src/services/url.js';
const TARGET_TOKEN_LINE_NUMBER = 15;
const TARGET_BRANCH_LINE_NUMBER = 3;
const TARGET_ENV_LINE_NUMBER = 14;
const LOGIN_USER_INFO = {
userName: '',
passWord: '',
env: 'v5'
};
function getServiceInfo(ENV) {
let loginApi, host;
if (ENV === 'test') {
host = 'https://test.com.cn/login-manager/#/login';
loginApi = '/desp/oper/lg/loginA';
} else {
host = `https://qapublic.com.cn/${ENV}/login-manager/#/login`;
loginApi = `/${ENV}/desp-gateway/desp/oper/lg/loginA`;
}
return { loginApi, host }
}
function delay(timeout) {
return new Promise(r => {
setTimeout(r, timeout)
})
}
async function initPuppeteer() {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
return { browser, page };
}
function getNewTokenLineContent(token) {
return ` window.localStorage.setItem('one-token', '${token}')`;
}
function getNewEnvLineContent(env) {
return ` environment = '${env}'`;
}
// 新token写入url.js
function writeTokenIntoFile(token) {
return new Promise((resolve, reject) => {
fs.readFile(URL_JS_FILE_PATH, 'utf8', (err, data) => {
if (err) {
console.error('读取文件时发生错误:', err);
reject(err)
return;
}
const lines = data.split('\n');
lines[TARGET_TOKEN_LINE_NUMBER - 1] = getNewTokenLineContent(token);
if (LOGIN_USER_INFO.env === 'test') {
lines[TARGET_ENV_LINE_NUMBER - 1] = getNewEnvLineContent('test');
} else {
lines[TARGET_ENV_LINE_NUMBER - 1] = getNewEnvLineContent('qa');
lines[TARGET_BRANCH_LINE_NUMBER - 1] = `let branchName = '${LOGIN_USER_INFO.env}';`;
}
const newData = lines.join('\n');
fs.writeFile(URL_JS_FILE_PATH, newData, 'utf8', (err) => {
if (err) {
console.error('token写入文件时发生错误:', err);
reject(err);
}
console.log('token已写入');
resolve();
});
});
})
}
function getInputEnvInfo() {
const envList = ['v5', 'v1', 'test'];
LOGIN_USER_INFO.env = envList[readlineSync.keyInSelect(envList, 'Please choose an env')];
console.log('env:', LOGIN_USER_INFO.env);
}
function getInputLolginInfo() {
LOGIN_USER_INFO.userName = readlineSync.question('Please enter your username:');
console.log('userName:', LOGIN_USER_INFO.userName);
LOGIN_USER_INFO.passWord = readlineSync.question('Please enter your password:');
console.log('passWord:', LOGIN_USER_INFO.passWord);
}
async function login() {
const { page, browser } = await initPuppeteer();
const { loginApi, host } = getServiceInfo(LOGIN_USER_INFO.env);
page.on('response', async function Request(res) {
if (new URL(res.url()).pathname === loginApi && res.ok()) {
try {
const resPromise = res.json();
const resData = await resPromise;
if (resData.success) { // 登陆成功
const token = resData.data.token;
console.log('token', token);
await writeTokenIntoFile(token);
} else {
console.log(resData.msg || resData.message || '登陆失败');
await page.close();
await browser.close();
run();
}
// page.removeListener('response', Request);
await page.close();
await browser.close();
console.log('存储完成,自动化脚本退出');
} catch {}
}
});
console.log('host:', host);
await page.goto(host);
await page.waitForNavigation();
await page.waitForSelector('input[placeholder="请输入账号"]');
await page.type('input[placeholder="请输入账号"]', LOGIN_USER_INFO.userName);
await page.waitForSelector('input[placeholder="请输入密码"]');
await page.type('input[placeholder="请输入密码"]', LOGIN_USER_INFO.passWord);
await page.waitForSelector('input[placeholder="请输入验证码"]');
await page.type('input[placeholder="请输入验证码"]', '1234');
await delay(500);
await page.click('button[type=button]');
// const worker = await createWorker();
// await worker.load();
// await worker.loadLanguage('eng');
// await worker.initialize('eng');
// const { data: { text } } = await worker.recognize(
// 'https://qapublic.wxb.com.cn/v5/desp-gateway/desp/oper/lg/getCaptcha',
// )
// await worker.terminate();
// console.log('text.trim()===', text.trim());
}
async function run() {
getInputLolginInfo();
login();
}
getInputEnvInfo();
run();
踩坑
- 由于公司前后端采用CORS方案处理跨域,接口请求会有预检请求,如果是预检请求的返回在puppeteer的page.on('response')监听回调中取res.json()会报错:Could not load body for this request. This might happen if the request is a preflight request.
- 命令行输入交互使用的库readline-sync在提示中文的时候会乱码,如果非要提示中文的话可以使用inquirer或者二次封装一下node原生的readline;
- 在监听到登录接口返回token后记得要browser.close()关闭puppeteer创建的浏览器对象,否则脚本一直pending状态会卡住后面脚本的执行;
package.json新增login指令
// package.json
{
"name": "xxx",
"version": "1.4.2",
"private": true,
"scripts": {
"dev": "cross-env NODE_ENV=development vite --host",
"build": "vite build",
"lint": "eslint --fix --ext .js,.vue src",
"prepare": "npx husky install",
// 此处新增login指令
"login": "node ./auto-login.js && npm run dev"
},
}