從厭煩不停的登入公司內網到使用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版本,請大家看本篇的下章內容。