Nodejs Playwright 2Captcha 驗證碼識別實現自動登陸
原文:https://lwebapp.com/zh/post/bypass-captcha
需求
日常工作當中,為了提高工作效率,我們可能會寫指令碼來自動執行任務。有些網站因為需要使用者登陸,所以指令碼的自動登陸功能必不可少。
不過我們在登陸網站的時候經常會出現驗證碼,驗證碼的目的就是為了防止機器登陸、自動化指令碼操作,那麼有沒有辦法讓指令碼能自動識別驗證碼實現登陸呢?
接下來我以 B 站為例給大家講解下,如何解決自動登陸指令碼中最關鍵的驗證碼問題。
探索
首先需要體驗下這個網站的登陸方式,瞭解下它的驗證碼型別。
開啟 B 站 https://www.bilibili.com/ ,開啟控制檯,點選登陸,這時候會彈出中間小的登陸框,通常輸入完賬號和密碼,就會彈出驗證碼框了,我們猜測驗證碼的介面此時已經請求了。
由於驗證碼的英文是 captcha
,我們在 network
面板裡搜 captcha
發現了一個和驗證碼相關的介面
https://passport.bilibili.com/x/passport-login/captcha
點開看介面返回結果,果然有一些有用的資訊,我們發現驗證碼型別是 geetest
。
{ "code": 0, "message": "0", "ttl": 1, "data": { "type": "geetest", "token": "b416c387953540608bb5da384b4e372b", "geetest": { "challenge": "aeb4653fb336f5dcd63baecb0d51a1f3", "gt": "ac597a4506fee079629df5d8b66dd4fe" }, "tencent": { "appid": "" } } }
通過搜尋發現了 B 站使用的驗證碼服務是 geetest
提供的,國內有很多網站都用的這個服務, geetest
驗證碼的特點是移動拼圖、按順序選擇文字或者數字。
那麼接下來,就來尋找可以識別 geetest
驗證碼的辦法。
小編了解了市面上提供的驗證碼解決方案,效果比較好的基本都是 OCR 服務商,對比之後發現 2Captcha 的服務非常好,解碼速度快、伺服器連線穩定、支援多種語言 API、價格公道,小編決定試用下 2Captcha
。
接下來就展示使用 Nodejs
+ Playwright
+ 2Captcha
來解決 B 站的登陸驗證碼問題。
如果想用其他語言和框架,比如
Python
+Selenium
,也可以參照這個教程,解決問題的思路都是一樣的。
解決
- 如何識別驗證碼
先仔細閱讀官方文件 2Captcha API Geetest,解決方案寫的很詳細,簡單來說
- 通過攔截網站介面,獲取
gt
、challenge
這兩個校驗碼引數,請求http://2captcha.com/in.php
,得到驗證碼ID
- 隔一段時間再請求
http://2captcha.com/res.php
,得到校驗成功的標識challenge
、validate
、seccode
- 如何應用驗證結果
拿到最關鍵的 validate
之後,模擬使用者填寫賬號密碼登陸,攔截驗證碼請求介面的返回引數,替換為校驗成功的引數,隨即觸發登陸介面。
接下來,我們分析下詳細步驟
環境準備
我們先搭建一下指令碼執行環境。
我們使用 Node.js
+ Playwright
來寫指令碼。
-
先確保你的電腦本地已經安裝了 Nodejs
-
再新建空專案,安裝
Playwright
mkdir bypass-captcha
cd bypass-captcha
npm init
npm i -D playwright
我們採用
Playwright
的庫模式,詳細文件:Playwright
- 在專案根目錄新建一個指令碼檔案
captcha.js
,填入以下內容,命令列執行node captcha.js
來簡單測試下是否能正常啟動專案
const {
chromium
} = require("playwright");
(async () => {
const browser = await chromium.launch({
headless: false,
});
const page = await browser.newPage();
await page.goto("https://www.bilibili.com/");
await browser.close();
})();
正常情況下會彈出一個谷歌瀏覽器介面,顯示 B 站首頁,隨後瀏覽器自動關閉。
請求 in.php
介面
- 首先整理下請求
http://2captcha.com/in.php
介面所需要的引數有哪些,可以看下引數列表,我們關注下必傳引數
引數 | 型別 | 必選 | 描述 |
---|---|---|---|
key | String | 是 | 您的 API key |
method | String | 是 | geetest - 定義您正在傳送的是 Geetest 的驗證碼 |
gt | String | 是 | 在目標網站上找到的 gt 引數 |
challenge | String | 是 | 在目標網站上找到的 challenge 引數 |
api_server | String | 否 | 在目標網站上找到的 api_server 引數 |
pageurl | String | 是 | 您看到 Geetest 驗證碼時所在網頁的完整 URL |
header_acao | Integer 預設: 0 | 否 | 0 - 禁用 1 - 啟用。 如果啟用 in.php 將在響應中包含 Access-Control-Allow-Origin:* 標頭。 用於 Web 應用程式中的跨域 AJAX 請求。 res.php 也支援 |
pingback | String | 否 | 解決驗證碼時將傳送的 pingback(回撥)響應的 URL。 URL 應該在伺服器上註冊。 更多資訊在這裡 |
json | Integer 預設: 0 | 否 | 0 - 伺服器將以純文字形式傳送響應 1 - 告訴伺服器以 JSON 格式傳送響應 |
soft_id | Integer | 否 | 軟體開發人員的 ID。 將他們的軟體與 2Captcha 整合的開發人員將獲得獎勵:軟體使用者支出的 10%。 |
proxy | String | 否 | 格式: login:[email protected]:3128 您可以找到更多關於代理的資訊這裡。 |
proxytype | String | 否 | 代理型別: HTTP, HTTPS, SOCKS4, SOCKS5. |
userAgent | String | 否 | 您的 userAgent 將傳遞給我們的工作人員並用於解決驗證碼。 |
-
key
是需要在 2Captcha 官網註冊賬戶後,後臺面板的賬戶設定中就有一個API key
,當然要起讓 key 生效的話還需要充值一定金額 -
method
是一個固定值geetest
-
gt
和challenge
之前已經在網站登入頁面的介面中看到了。不過這裡有個注意點,gt
是每個網站只有一個值,B 站這裡是固定的ac597a4506fee079629df5d8b66dd4fe
,但是challenge
是一個動態值,每次 API 請求都會獲得一個新的challenge
值。在頁面上載入驗證碼後,challenge
值就會失效。 所以要在網站登入頁載入的時候監聽https://passport.bilibili.com/x/passport-login/captcha
這個請求,每次重新識別出新的challenge
值。下面會講解如何監聽到。 -
pageurl
就是登入頁的地址https://www.bilibili.com/
於是我們可以得到類似這樣一個請求介面
http://2captcha.com/in.php?key=1abc234de56fab7c89012d34e56fa7b8&method=geetest>=ac597a4506fee079629df5d8b66dd4fe&challenge=12345678abc90123d45678ef90123a456b&pageurl=https://www.bilibili.com/
- 接下來就解決每次進首頁獲取新的
challenge
值
模擬使用者點選登入的流程
-
先啟動谷歌瀏覽器,開啟 B 站首頁
-
點選頂部的登入按鈕,會彈出登入框
-
這時候驗證碼介面已經發出,在這裡監聽驗證碼介面返回的引數,就能截獲到
gt
和challenge
的值
const {
chromium
} = require("playwright");
(async () => {
// 選擇Chrome瀏覽器,設定headless: false 能看到瀏覽器介面
const browser = await chromium.launch({
headless: false,
});
const page = await browser.newPage();
// 開啟B站
await page.goto("https://www.bilibili.com/");
const [response] = await Promise.all([
// 請求驗證碼介面
page.waitForResponse(
(response) =>
response.url().includes("/x/passport-login/captcha") &&
response.status() === 200
),
// 點選頂部的登入按鈕
page.click(".header-login-entry"),
]);
// 獲取到介面返回資訊
const responseJson = await response.body();
// 解析出 gt 和 challenge
const json = JSON.parse(responseJson);
const gt = json.data.geetest.gt;
const challenge = json.data.geetest.challenge;
console.log("得到 gt", gt, "challenge", challenge);
// 暫停5秒,防止瀏覽器關閉太快,來不及看到效果
sleep(5000);
// 關閉瀏覽器
await browser.close();
})();
/**
* 模擬sleep功能,延遲一定時間,單位毫秒
* Delay for a number of milliseconds
*/
function sleep(delay) {
var start = new Date().getTime();
while (new Date().getTime() < start + delay);
}
- 使用
request
庫來單獨請求in.php
介面
先安裝 request
npm i request
現在可以開始正式的請求 http://2captcha.com/in.php
介面了
// 請求 in.php 介面
const inData = {
key: API_KEY,
method: METHOD,
gt: gt,
challenge: challenge,
pageurl: PAGE_URL,
json: 1,
};
request.post(
"http://2captcha.com/in.php", {
json: inData
},
function(error, response, body) {
if (!error && response.statusCode == 200) {
console.log("response", body);
}
}
);
正常情況下,這時候會返回驗證碼 ID
,比如 {"status":1,"request":"2122988149"}
,取 request
欄位即可。
如果介面返回程式碼
ERROR_ZERO_BALANCE
,表明您的賬戶餘額不足,需要充值,我這裡充值了最低額度用於演示,大家根據自己需要適當體驗下。
擴充套件學習
為了提升安全性,我們將 API Key
寫在環境變數檔案中來引用。
- 在根目錄新建一個環境變數檔案
.env
,寫入API Key
的值
# .env檔案
API_KEY="d34y92u74en96yu6530t5p2i2oe3oqy9"
- 然後安裝
dotenv
庫,用來獲取到環境變數
npm i dotenv
- 在 js 指令碼中使用
require("dotenv").config();
這樣通過 process.env.API_KEY
就能取到 .env
中的變量了,通常 .env
檔案不上傳到程式碼倉庫,保證個人資訊的安全性。
- 如果不想把資訊寫到檔案中的同時確保安全性,也可以直接在控制檯輸入傳入 Node.js 環境變數,比如
API_KEY=d34y92u74en96yu6530t5p2i2oe3oqy9 node captcha.js
請求 res.php
介面
- 請求介面之前,我們也整理下所需引數
GET 引數 | 型別 | 必選 | 描述 |
---|---|---|---|
GET 引數 | 型別 | 必選 | 描述 |
key | String | 是 | 您的 API key |
action | String | 是 |
get - 得到您的驗證碼的答案 |
id | Integer | 是 | in.php 返回的驗證碼 ID |
json | Integer 預設: 1 | 否 | 伺服器將始終以 JSON 格式返回 Geetest 驗證碼的響應。 |
-
key
就是API_KEY
,上一個介面也用到了 -
action
就是固定值get
-
id
是剛剛in.php
返回的驗證碼ID
- 上一個請求 20 秒之後,再請求
http://2captcha.com/res.php
介面獲取驗證結果
request.get(
`http://2captcha.com/res.php?key=${API_KEY}&action=get&id=${ID}&json=1`,
function(error, response, body) {
if (!error && response.statusCode == 200) {
const data = JSON.parse(body);
if (data.status == 1) {
console.log(data.request);
}
}
}
);
介面會返回三個值 challenge
、 validate
和 seccode
,每一個引數都是一個字串
{
"geetest_challenge": "aeb4653fb336f5dcd63baecb0d51a1f3",
"geetest_validate": "9f36e8f3a928a7d382dad8f6c1b10429",
"geetest_seccode": "9f36e8f3a928a7d382dad8f6c1b10429|jordan"
}
其中 challenge
就是前面我們攔截到的引數, validate
是校驗結果標識, seccode
內容和 validate
基本一致,只多了一個單詞。我們需要將 validate
儲存下來備用。
這裡有時候會碰到驗證碼無法校驗通過的情況,可以多嘗試幾次,或者聯絡 2Captcha 官網排查問題
到這裡,驗證碼校驗結果的資訊已經獲取完整,接下來就是拿校驗結果去登陸了。
登陸
- 先來研究下正常使用者點選驗證碼校驗成功後的登陸流程
我們發現了三個介面
-
https://api.geetest.com/ajax.php
:驗證碼介面,用於生成驗證碼和校驗驗證碼是否通過。校驗介面返回資料中的validate
欄位就是 2Captcha 服務獲取到的geetest_validate
。
-
https://passport.bilibili.com/x/passport-login/web/key?_=1649087831803
:密碼加密介面,用於獲取 hash 和公鑰
-
https://passport.bilibili.com/x/passport-login/web/login
:登陸介面,入參包括賬號、密碼、token
、challenge
、validate
和seccode
等
我們分析這幾個介面,可以得出兩個登陸方案。
- 第一個方案,在
Node.js
環境下請求加密介面和登陸介面,獲取使用者 Cookie 資訊,後續使用者登陸直接攜帶 Cookie 資訊即可。這個方案的難點是需要單獨處理密碼加密,對新手不太友好。 - 第二個方案,用
Playwright
模擬使用者填寫賬號密碼登陸,隨機點選驗證碼觸發登陸,攔截驗證碼介面的返回引數,替換為校驗成功的標識,隨即觸發登陸介面。
我們採用第二種方案。
不過也遇到一個坑,就是 Node.js
環境下,驗證碼圖片載入不出來,後面發現驗證碼介面 https://api.geetest.com/ajax.php
同時負責拉取驗證碼圖片和校驗驗證碼,我們直接攔截拉取驗證碼圖片時的請求,替換校驗結果就能觸發登陸了,不需要等圖片驗證碼出來。這個細節很關鍵。
總結
以上就是針對自動化測試任務中,常見的自動登陸功能的一些研究。結合 Node.js
、 Playwright
、 2Captcha
這些工具的優勢,實現了驗證碼識別。完整的程式碼我已經上傳到了 GitHub。
可能還有很多待優化的地方,歡迎大家指出。
宣告:此指令碼只作為測試和學習的案例,風險自行評估。