從厭煩不停的登錄公司內網到使用Electron製作自己的客户端工具(一)

語言: CN / TW / HK

theme: smartblue highlight: a11y-dark


起因

事情的起因是這樣,由於本人是在一家與政府機關有合作的單位工作,處於部分保密性原因,公司對員工電腦的外網連接是有限制的,即:商業部門每個人上網都需要登錄自己的網絡賬號。本身這個問題是不大的,但是令人煩惱的是,每當你有一段空當時間沒有上網,這個登錄就過期了...然後你會突然看到如下畫面: uTools_1676353370711.png

其實這也沒什麼問題,再重新登錄就好咯~但是作為一個極致懶惰的程序員,我不允許自己像個傻子一樣:一天得打開那個登錄頁n多次,重複的輸入用户名、密碼,點擊我同意什麼什麼協議,再點擊一個登錄button,再告訴我“哦親愛的的垃圾前端,你可以上網了!”。這簡直無法忍受!

1628686964686.jpg

於是我就思考了:能不能寫一個程序能自己檢測現在的外網連接,如果發現斷網了,就自動幫我登錄呢?

聽起來是完全可行的,因為大致思路上的偽代碼已經有了:寫一個定時器,每間隔幾秒鐘ping一下百度,如果ping不通,那麼就打開內網登錄頁自動登錄或直接發個登錄請求,如此就ok了。

那麼説幹就幹,走起。

使用Nodejs腳本

思考這個程序的時候,我首先想到的就是寫一個nodejs的腳本,畢竟是做前端的,所以對js比較熟悉。在腳本中做我想做的事,然後命令行node xxx.js就好了。很簡單,於是唰唰唰就把基本函數寫好了:

```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依賴,接收命令行參數,讓它更加的通用:

```js // 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 ', '用户名', 'dyggod') .option('-p, --password ', '密碼', 'password') .option('-t, --time

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就可以使用自己的賬户去運行了。

到現在為止,我們的運行輸出如下:

image.png

接下來,就是需要完成我們的登錄邏輯,讓這個腳本可以真正解決我的問題。那麼首先,我們要去看登錄內網的那個網站的請求構成是怎樣的。我用控制枱將網絡設置為脱機,登錄內網觀察它的過程看到的情況:

image.png

哇喔哇喔哇喔,瞅瞅,瞅瞅我們看到了什麼??php!

此時我的內心:

15684496873735.jpg

在我原有的思路中,是在login函數中發送ajax請求,將用户名、密碼等參數發過去,實現登錄的功能。但現在看起來不太可能了,因為眾所周知,php是服務端語言,雖然有部分邏輯是運行在瀏覽器裏的,但發送請求的時候其實是調用的服務端的資源腳本,如果我們直接發送請求,大概率會被服務器限制,因為所處的環境不一樣,被服務端隔離限制。我用postman試一下,果然:

image.png

返回NOT Found,被限制了,至於想搞清楚它到底需要再攜帶什麼頭參數才能繞過限制,是很麻煩的,或者服務端乾脆不允許任何外部請求也説不定。

並且我們看到它的pwd參數其實是做了加密的,這個加密邏輯在查看網絡源代碼的時候,可以看到:

image.png

其實也是很麻煩。

所以我決定不再使用請求去登錄,而是:模擬頁面登錄!意思是我在登錄函數中,直接啟動一個模擬瀏覽器,像執行前端集成測試一樣,自動輸入用户名、密碼、選中複選框,點擊登錄,豈不美哉?説幹就幹。

那麼於是乎,登錄函數就變成下下面這個樣子:

```js 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是可以通過控制枱找到的。

到這裏,看起來似乎我已經成功了呢,那麼,就來一發:

image.png

哇哦!大功告成,隨着我故意斷開網絡連接,數秒後看到一閃而過幾乎看不到的瀏覽器窗口劃過,在我再次打開百度看到網頁的時候,證明這個腳本確實已經可以解決我的問題了。

當然,只是登錄一下還不能説明什麼,不過我把這個程序運行了一天,也確實能證明它是在時刻監控並執行登錄的。

至此,nodejs腳本版的自動登錄工具就做完了。

不過...還是不太滿意,因為這是個nodejs的腳本,如果拿給別人用,比如我們的UI大人,他的電腦不會裝nodejs環境,難道還要人家為了運行這個腳本特意裝一下?不行不行,不夠優雅,我應該讓他不用在意環境而直接使用。那麼...似乎...可以把它做成一個桌面端應用軟件,因為我們都是window系統,所以如果是一個.exe文件,那麼自然就不用去裝什麼破爛環境啦!

15838963755668.jpg

於是乎,這個小小登錄腳本衍生出了它的Electron版本,請大家看本篇的下章內容。