从厌烦不停的登录公司内网到使用Electron制作自己的客户端工具(一)

lxf2023-03-08 09:27:01

起因

事情的起因是这样,由于本人是在一家与政府机关有合作的单位工作,处于部分保密性原因,公司对员工电脑的外网连接是有限制的,即:商业部门每个人上网都需要登录自己的网络账号。本身这个问题是不大的,但是令人烦恼的是,每当你有一段空当时间没有上网,这个登录就过期了...然后你会突然看到如下画面: 从厌烦不停的登录公司内网到使用Electron制作自己的客户端工具(一)

其实这也没什么问题,再重新登录就好咯~但是作为一个极致懒惰的程序员,我不允许自己像个傻子一样:一天得打开那个登录页n多次,重复的输入用户名、密码,点击我同意什么什么协议,再点击一个登录button,再告诉我“哦亲爱的的垃圾前端,你可以上网了!”。这简直无法忍受!

从厌烦不停的登录公司内网到使用Electron制作自己的客户端工具(一)

于是我就思考了:能不能写一个程序能自己检测现在的外网连接,如果发现断网了,就自动帮我登录呢?

听起来是完全可行的,因为大致思路上的伪代码已经有了:写一个定时器,每间隔几秒钟ping一下百度,如果ping不通,那么就打开内网登录页自动登录或直接发个登录请求,如此就ok了。

那么说干就干,走起。

使用Nodejs脚本

思考这个程序的时候,我首先想到的就是写一个nodejs的脚本,毕竟是做前端的,所以对js比较熟悉。在脚本中做我想做的事,然后命令行node xxx.js就好了。很简单,于是唰唰唰就把基本函数写好了:

// main.js

const ping = require('ping');

const username = 'dyggod'
const password = 'password'
const time = 3000;
let timer = null;

/**
 * 启动一个定时器,调用监听函数,并再次执行自己
 */
function start() {
  timer = setTimeout(() => {
    watchInternet();
    start();
  }, time);
}

/**
 * 清空定时器并释放内存
 */
function pause() {
  clearTimeout(timer);
  timer = null;
}


/**
 * 调用系统ping命令,检测电脑是否联网
 */
async function isOnline() {
    const { alive } = await ping.promise.probe('www.baidu.com');
    return alive;
};


/**
 * 监听网络状态,如果断网,则调用login函数
 */
async function watchInternet() {
  const online = await isOnline();
  if (!online) {
    try {
      console.log('\x1b[33m%s\x1b[0m', '网络断开,尝试重新登录...');
      await login();
    } catch (error) {
      console.log('\x1b[31m%s\x1b[0m', '登录失败,正在重试...');
    }
  }
}

/**
 * 登录函数
 */
async function login() {
  console.log('登录成功')
}

start();

这里说明一下这个脚本引入了ping这个依赖,它可以允许我们像命令行中的ping那样检查网络是否是通的。

写成这个样子,其实大部分的事情已经做完了,运行这个js文件,在没有外网的时候,是会有对应的打印内容,剩下的无非是补充login函数中的代码,写成真实的逻辑。

但是在此之前,我要把这个脚本优化一下,因为这种把用户名、密码、定时时间写死的方式太不优雅了,我还想着如果效果不错,能分享给我的同事使用呢。于是,让我来安装commander依赖,接收命令行参数,让它更加的通用:

// main.js

const ping = require('ping');
const commander = require('commander');

const { Builder } = require('selenium-webdriver');

const program = new commander.Command();
program.command('start')
  .description('开始监测内网登录状态......')
  .option('-u, --username <username>', '用户名', 'dyggod')
  .option('-p, --password <password>', '密码', 'password')
  .option('-t, --time <time>', '检测时间间隔', 3000)
  .action(main)

let username;
let password;
let time;
let timer = null;

/**
 * 主函数,配置内网登录的用户名和密码
 * 执行启动函数
 */
function main(cwd, options) {
  console.log('监听正在执行,参数列表为:', cwd);
  username = cwd.username;
  password = cwd.password;
  time = cwd.time;
  start();
};

/**
 * 启动一个定时器,调用监听函数,并再次执行自己
 */
function start() {
  timer = setTimeout(() => {
    watchInternet();
    start();
  }, time);
}

/**
 * 调用系统ping命令,检测电脑是否联网
 */
async function isOnline() {
    const { alive } = await ping.promise.probe('www.baidu.com');
    return alive;
};


/**
 * 监听网络状态,如果断网,则调用login函数
 */
async function watchInternet() {
  const online = await isOnline();
  if (!online) {
    try {
      console.log('\x1b[33m%s\x1b[0m', '网络断开,尝试重新登录...');
      await login();
    } catch (error) {
      console.log('\x1b[31m%s\x1b[0m', '登录失败,正在重试...');
    }
  }
}

/**
 * 登录函数
 */
async function login() {
  console.log('登录成功')
}


// 执行
program.parse(process.argv).version('1.0.0');

commander是一个方便我们定义并接收node执行时的命令行参数的依赖库,我们现在定义了username、password、time三个参数,那么如果是别人使用,只需用node main.js start -u xxx -p xxx -t 5000就可以使用自己的账户去运行了。

到现在为止,我们的运行输出如下:

从厌烦不停的登录公司内网到使用Electron制作自己的客户端工具(一)

接下来,就是需要完成我们的登录逻辑,让这个脚本可以真正解决我的问题。那么首先,我们要去看登录内网的那个网站的请求构成是怎样的。我用控制台将网络设置为脱机,登录内网观察它的过程看到的情况:

从厌烦不停的登录公司内网到使用Electron制作自己的客户端工具(一)

哇喔哇喔哇喔,瞅瞅,瞅瞅我们看到了什么??php!

此时我的内心:

从厌烦不停的登录公司内网到使用Electron制作自己的客户端工具(一)

在我原有的思路中,是在login函数中发送ajax请求,将用户名、密码等参数发过去,实现登录的功能。但现在看起来不太可能了,因为众所周知,php是服务端语言,虽然有部分逻辑是运行在浏览器里的,但发送请求的时候其实是调用的服务端的资源脚本,如果我们直接发送请求,大概率会被服务器限制,因为所处的环境不一样,被服务端隔离限制。我用postman试一下,果然:

从厌烦不停的登录公司内网到使用Electron制作自己的客户端工具(一)

返回NOT Found,被限制了,至于想搞清楚它到底需要再携带什么头参数才能绕过限制,是很麻烦的,或者服务端干脆不允许任何外部请求也说不定。

并且我们看到它的pwd参数其实是做了加密的,这个加密逻辑在查看网络源代码的时候,可以看到:

从厌烦不停的登录公司内网到使用Electron制作自己的客户端工具(一)

其实也是很麻烦。

所以我决定不再使用请求去登录,而是:模拟页面登录!意思是我在登录函数中,直接启动一个模拟浏览器,像执行前端集成测试一样,自动输入用户名、密码、选中复选框,点击登录,岂不美哉?说干就干。

那么于是乎,登录函数就变成下下面这个样子:

const { Builder } = require('selenium-webdriver');

/**
 * 打开chrome浏览器,填写用户名和密码,然后提交
 */
async function login() {
  const loginUrl = 'http://xxx.xx.x.xxx/ac_portal/20220831163936/pc.html';
  const driver = await new Builder().forBrowser('chrome').build();
  await driver.get(loginUrl);
  await driver.findElement({ id: 'password_name' }).sendKeys(username);
  await driver.findElement({ id: 'password_pwd' }).sendKeys(password);
  await driver.findElement({ id: 'password_disclaimer' }).click();
  await driver.findElement({ id: 'password_submitBtn' }).click();
  console.log('\x1b[32m%s\x1b[0m', '登录成功');
  // 关闭浏览器并退出driver
  await driver.close();
  await driver.quit();
}

这里我使用了selenium-webdriver依赖,它可以在nodejs环境中调用一个无头浏览器,来做模拟人的交互操作,忽然仿佛回到了用python写测试脚本的辛酸时候...其中输入框、按钮等元素的id是可以通过控制台找到的。

到这里,看起来似乎我已经成功了呢,那么,就来一发:

从厌烦不停的登录公司内网到使用Electron制作自己的客户端工具(一)

哇哦!大功告成,随着我故意断开网络连接,数秒后看到一闪而过几乎看不到的浏览器窗口划过,在我再次打开百度看到网页的时候,证明这个脚本确实已经可以解决我的问题了。

当然,只是登录一下还不能说明什么,不过我把这个程序运行了一天,也确实能证明它是在时刻监控并执行登录的。

至此,nodejs脚本版的自动登录工具就做完了。

不过...还是不太满意,因为这是个nodejs的脚本,如果拿给别人用,比如我们的UI大人,他的电脑不会装nodejs环境,难道还要人家为了运行这个脚本特意装一下?不行不行,不够优雅,我应该让他不用在意环境而直接使用。那么...似乎...可以把它做成一个桌面端应用软件,因为我们都是window系统,所以如果是一个.exe文件,那么自然就不用去装什么破烂环境啦!

从厌烦不停的登录公司内网到使用Electron制作自己的客户端工具(一)

于是乎,这个小小登录脚本衍生出了它的Electron版本,请大家看本篇的下章内容。