1. 程式人生 > 其它 >Nodejs Playwright 2Captcha 驗證碼識別實現自動登陸

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

2Captcha 官網

接下來就展示使用 Nodejs + Playwright + 2Captcha 來解決 B 站的登陸驗證碼問題。

如果想用其他語言和框架,比如 Python

+ Selenium ,也可以參照這個教程,解決問題的思路都是一樣的。

解決

  1. 如何識別驗證碼

先仔細閱讀官方文件 2Captcha API Geetest,解決方案寫的很詳細,簡單來說

  • 通過攔截網站介面,獲取gtchallenge這兩個校驗碼引數,請求http://2captcha.com/in.php,得到驗證碼 ID
  • 隔一段時間再請求http://2captcha.com/res.php,得到校驗成功的標識 challengevalidateseccode
  1. 如何應用驗證結果

拿到最關鍵的 validate 之後,模擬使用者填寫賬號密碼登陸,攔截驗證碼請求介面的返回引數,替換為校驗成功的引數,隨即觸發登陸介面。

接下來,我們分析下詳細步驟

環境準備

我們先搭建一下指令碼執行環境。

我們使用 Node.js + Playwright 來寫指令碼。

  1. 先確保你的電腦本地已經安裝了 Nodejs

  2. 再新建空專案,安裝 Playwright

mkdir bypass-captcha
cd bypass-captcha
npm init
npm i -D playwright

我們採用 Playwright 的庫模式,詳細文件:Playwright

  1. 在專案根目錄新建一個指令碼檔案 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 介面

  1. 首先整理下請求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
  • gtchallenge 之前已經在網站登入頁面的介面中看到了。不過這裡有個注意點,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&gt=ac597a4506fee079629df5d8b66dd4fe&challenge=12345678abc90123d45678ef90123a456b&pageurl=https://www.bilibili.com/
  1. 接下來就解決每次進首頁獲取新的 challenge

模擬使用者點選登入的流程

  • 先啟動谷歌瀏覽器,開啟 B 站首頁

  • 點選頂部的登入按鈕,會彈出登入框

  • 這時候驗證碼介面已經發出,在這裡監聽驗證碼介面返回的引數,就能截獲到gtchallenge的值

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);
}
  1. 使用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 寫在環境變數檔案中來引用。

  1. 在根目錄新建一個環境變數檔案.env,寫入API Key的值
# .env檔案
API_KEY="d34y92u74en96yu6530t5p2i2oe3oqy9"
  1. 然後安裝dotenv庫,用來獲取到環境變數
npm i dotenv
  1. 在 js 指令碼中使用
require("dotenv").config();

這樣通過 process.env.API_KEY 就能取到 .env 中的變量了,通常 .env 檔案不上傳到程式碼倉庫,保證個人資訊的安全性。

  1. 如果不想把資訊寫到檔案中的同時確保安全性,也可以直接在控制檯輸入傳入 Node.js 環境變數,比如
API_KEY=d34y92u74en96yu6530t5p2i2oe3oqy9 node captcha.js

請求 res.php 介面

  1. 請求介面之前,我們也整理下所需引數
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
  1. 上一個請求 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);
            }
        }
    }
);

介面會返回三個值 challengevalidateseccode ,每一個引數都是一個字串

{
  "geetest_challenge": "aeb4653fb336f5dcd63baecb0d51a1f3",
  "geetest_validate": "9f36e8f3a928a7d382dad8f6c1b10429",
  "geetest_seccode": "9f36e8f3a928a7d382dad8f6c1b10429|jordan"
}

其中 challenge 就是前面我們攔截到的引數, validate 是校驗結果標識, seccode 內容和 validate 基本一致,只多了一個單詞。我們需要將 validate 儲存下來備用。

這裡有時候會碰到驗證碼無法校驗通過的情況,可以多嘗試幾次,或者聯絡 2Captcha 官網排查問題

到這裡,驗證碼校驗結果的資訊已經獲取完整,接下來就是拿校驗結果去登陸了。

登陸

  1. 先來研究下正常使用者點選驗證碼校驗成功後的登陸流程

我們發現了三個介面

  • 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:登陸介面,入參包括賬號、密碼、tokenchallengevalidateseccode

我們分析這幾個介面,可以得出兩個登陸方案。

  1. 第一個方案,在Node.js 環境下請求加密介面和登陸介面,獲取使用者 Cookie 資訊,後續使用者登陸直接攜帶 Cookie 資訊即可。這個方案的難點是需要單獨處理密碼加密,對新手不太友好。
  2. 第二個方案,用 Playwright 模擬使用者填寫賬號密碼登陸,隨機點選驗證碼觸發登陸,攔截驗證碼介面的返回引數,替換為校驗成功的標識,隨即觸發登陸介面。

我們採用第二種方案。

不過也遇到一個坑,就是 Node.js 環境下,驗證碼圖片載入不出來,後面發現驗證碼介面 https://api.geetest.com/ajax.php 同時負責拉取驗證碼圖片和校驗驗證碼,我們直接攔截拉取驗證碼圖片時的請求,替換校驗結果就能觸發登陸了,不需要等圖片驗證碼出來。這個細節很關鍵。

總結

以上就是針對自動化測試任務中,常見的自動登陸功能的一些研究。結合 Node.jsPlaywright2Captcha 這些工具的優勢,實現了驗證碼識別。完整的程式碼我已經上傳到了 GitHub。

倉庫:https://github.com/openHacking/bypass-captcha

原文:https://lwebapp.com/zh/post/bypass-captcha

可能還有很多待優化的地方,歡迎大家指出。

宣告:此指令碼只作為測試和學習的案例,風險自行評估。

參考