1. 程式人生 > >超詳細的Python實現百度雲盤模擬登陸(模擬登陸進階)

超詳細的Python實現百度雲盤模擬登陸(模擬登陸進階)

這是第二篇從簡書搬運過來的文章(大家別誤會,是我原創的)。因為前一篇文章,我看反響還挺好的,所以把這篇也搬運過來了,其實目的還是為宣傳自己的分散式微博爬蟲(該專案的內容和工作量都很飽滿啊,大家如果覺得有幫助,請多多支援啊)。大概從下一篇起,就會一步一步講解如何構建分散式爬蟲再到微博分散式爬蟲的方法了。因為關於初級爬蟲的文章太氾濫了,所以我就不會講比較基礎的東西。

今天我給大家講講如何模擬登陸百度雲盤(該分析過程也適用於百度別的產品,比如模擬登陸百度搜索首頁,它們的加密流程完全一樣,只是提交引數有微小差別)。方法不僅適用於百度雲,別的一些比較難以模擬登陸的網站都可以按照這種方式分析。

閱讀文章之前,有一些東西需要給大家闡述:
- 本文並沒有對驗證碼識別進行分析,因為我覺得寫爬蟲最主要的不是識別驗證碼,而是如何規避驗證碼,我自己寫的

分散式微博爬蟲也是一直想著規避驗證碼,而不是去識別它,因為我覺得那並不是爬蟲該做的事情(至少不是中低階爬蟲的事,我覺得那是模式識別的事情了)。
- 本文要求讀者具有模擬登陸(主要是抓包和閱讀js程式碼)和密碼學的基本知識。

模擬登陸微博的分析流程一樣,我們首先要做的是以正常人的流程完整的登入一遍百度網盤。在開啟瀏覽器之前,先開啟抓包工具,以前我在win平臺用的是fiddler,現在由於電腦是mac系統,所以選擇charles進行抓包。如果有同學沒有charles的使用經驗,那麼需要先了解如何讓charles能抓取到本機的https資料包。由於使用charles抓包不是本文的重點,所以我就簡略說一下:
1. 安裝charles證書。通過選單’help’->’ssl proxying’->’install charles root certificate’進行安裝,安裝過後把證書設定為始終信任
2. 修改charles的proxy settings。 選擇proxy->proxy settings,然後勾選“enable transparent HTTP proxying”
3. 修改Charles的SSL proxy settings。選擇proxy->ssl proxy settings,在彈出的對話方塊中勾選”enable ssl proxying”,並在location

部分點選add,新增需要捕獲的站點和443埠,如圖:

charles ssl proxy設定圖

charles設定好了之後,我們再使用瀏覽器直接開啟百度網盤首頁 。注意開啟之前如果以前登入過百度網盤,一定要先清除百度網盤的cookie,如果不清楚自己以前登入過沒,那麼最好把關於百度的cookie都清除了吧。如果清除得不徹底,很可能會錯過很關鍵的一步,我先按下不表。通過抓包,我們可以看到請求百度網盤的首頁,大概有這些請求:

百度網盤首頁請求資料

這裡other hosts是本機別的請求,所以直接被我忽略了(通過設定請求為“focus”或者“ignore”)。下圖給的是設定方法,主要是FocusDisable SSL Proxying

設定請求為focus,並且注意**disable ssl proxying**

大家可以檢視具體每個請求的內容和響應,由於篇幅限制,我就不囉嗦了。然後我們在登入頁輸入登入賬號(先別輸入密碼和點選登入,如果有想不明白的同學可以閱讀我的

微博登陸分析),然後觀察charles的請求,會發現又多了一條請求:

在輸入登入賬號過後js觸發的請求

我們看看它返回的內容:

輸入賬號後服務端返回的內容

可以看到有效資訊大概有兩個: pubkeykey,它們的用途我們都還不知道,但是看命名可知大概pubkey是某種加密演算法的公鑰。

然後我們輸入密碼,點選登入,可以看到charles的請求:

登入中的請求

上圖圈出來的請求就是提交的密碼和登入賬號等資訊,這個只有大家自己挨著請求檢視,才可以確定哪個是post的請求。我們檢視post的引數:

post引數

現在終於Get到重點了,主要就是要把這些提交的引數的生成方式弄清楚。如果有過模擬登陸或者爬蟲編寫經驗的同學,都應該知道請求引數構造之前必須分析清楚哪些引數是變的,哪些引數是不變的,變的哪些引數比較有規律,哪些沒有規律。這個分析過程是通過反覆登入和抓包,對比post資料來完成的。我們通過反覆登入和對比post的資料,可以發現:

staticpage、charset、 tpl、 subpro、apiver、codestring、 safeflg、u、 isPhone、detect、quick_user、logintype、logLoginType、idc、loginmerge、foreignusername、username、mem_pass、crypttype、countrycode、dv

等引數不會變化。所以我們只需要分析變化的引數。

變化的引數當中,tt看樣子基本可以確定是請求時間戳(需要分析它是多少位,精確到毫秒還是秒),其它好像都沒什麼規律。由於微博模擬登陸的經驗,我們基本可以判斷出password這個引數是最難分析的(從賬戶安全形度上來說也應該是加密最複雜的),我們放到後面分析。

那麼我們先來分析token欄位吧。引數不可能憑空產生,來源只有兩種可能:一種是通過服務端返回, 另外一種是通過請求回來的js構造。在分析token產生的時候,我們需要用到charles的查詢功能(良心推薦,很強大),它可以查詢到整個登入流程中,包含某個查詢字串的所有請求和響應。下圖中的”望遠鏡”圖示就是查詢功能。

使用charles查詢包含token值的請求和響應

通過查詢,我們可以看到有14個地方包含了token的值,我們發現基本都是使用token作為請求引數的,不過有一個結果是返回token值的:

token的返回

它的請求url是

請求引數格式化一下,可能更方便檢視:

獲取token的請求引數

根據上面介紹的分析變數與不變數的思路,這裡我們可以看到要獲取到token,需要知道gidcallback的構造方法。然後用和分析token同樣的方式,我們來分析gid的產生。通過在charles中查詢gid的值,我們發現找到的結果全是在請求中,並沒有在響應中找到該值,說明該值是通過js生成的而不是通過服務端返回的。既然是通過js生成的,我們需要找到該js檔案。怎麼找呢?我們在charles中輸入gid,再來看看查詢的結果,注意這次我們重點關注哪個js檔案中出現gid,否則查詢的結果太多,看起來會比較費力。通過查詢,可以看到名為login_tangram_c36ce25.js這個檔案中頻繁出現了*gid這個引數,基本可以確定這個js檔案很關鍵,這也是我先前說的在抓包分析之前需要把百度的cookie等歷史資料清除的原因,否則該js檔案可能已經快取了,charles中就查不到該js。我們把該js檔案下載下來,通過webstorm將其中的程式碼格式化,再查詢gid,可以看到這段程式碼

gid的生成方式

其中的this.guideRandom函式就是生成gid的函式,因為我們在webstorm中查詢gid字串的時候,可以發現很多如下圖所示的語句,只需要定位到guideRandom即可

gid的宣告

我們現在找到了gid的生成方式了,如果讀不懂這段js也沒關係,可以直接使用pyv8或者pyexecjs等庫將執行後的js結果返回給python使用。然後我們再回到獲取token的請求引數那張圖,發現還有個callback引數需要分析。同gid分析過程一樣,我們先搜尋callback的值bd__cbs__v2xmbc,發現只有請求中包含,基本可以確定它是通過js產生的,而加密js檔案我們已經找到了。如果你害怕可能不是上面的那個js檔案,我們也可以通過在charles中搜索callback這個字串,可以發現就是該js檔案。通過在webstorm中搜索callback關鍵詞(通過前面多次登入抓包分析,可發現callback的bd__cbs_字首不會改變,這個也可以是搜尋依據),可以找到callback的生成方式

var i, r, o, a = this.url, s = document.createElement("SCRIPT"), u = "bd__cbs__", d = t || {},
    l = d.charset, c = d.queryField || "callback", f = d.timeOut || 0,
    p = new RegExp("(\\?|&)" + c + "=([^&]*)");
// 下面就是callback的生成邏輯
baidu.lang.isFunction(e) ? (i = u + Math.floor(2147483648 * Math.random()).toString(36)

截至目前,我們已經弄清楚了gidcallback的生成方式了,這樣我們就可以通過構造請求來獲取到token了。我們再返回post引數這張圖片,可以看到還有passwordrsakeyppui_logintime這三個欄位還需要分析。而通過搜尋rsakey的值,可以看到其實它就是 圖片輸入賬號後服務端返回的內容 中的key的值,我們可以通過

這個請求獲取到。請求的引數如圖,都是我們前面分析過並且能夠得到的引數:

獲取key和pubkey的請求引數

現在我們就只有ppui_logintimepassword兩個欄位沒分析了。
老規矩,我們先在charles中搜索ppui_logintime的值,發現只有一個請求中出現了。那麼它肯定是js生成的,它是如何生成的呢?我們又在我們獲取的login_tangram_c36ce25.js檔案中搜索ppui_logintime這個字串,可以發現這段程式碼:

  login: {
                memberPass: "mem_pass",
                safeFlag: "safeflg",
                isPhone: "isPhone",
                timeSpan: "ppui_logintime",
                logLoginType: "logLoginType"
            }

然後我們再看timeSpan是如何生成的。可以看到這段程式碼

r.timeSpan = (new Date).getTime() - e.initTime

大概是一個時間差:當前時間-初始化時間。當前時間容易獲取,那麼初始化時間到底是什麼初始化呢?繼續追蹤initTime可以發現這段程式碼

 _initApi: function (e) {
            var t = this;
            t.initialized = !0, t.initTime = (new Date).getTime(), passport.data.getApiInfo({
                apiType: "login",
                gid: t.guideRandom || "",
                loginType: t.config && t.config.diaPassLogin ? "dialogLogin" : "basicLogin"
            })
.....

initApi中的initTime大概就是頁面請求完成的時間,所以ppui_time應該就是登入頁面初始化完成到點選登入按鈕的時間差,為了方便,我們只需要取抓包獲取的值即可。

現在分析password引數,這個引數也是分析難度最大的引數了。這次我們直接在加密js檔案中搜索password關鍵詞,可以搜尋到很多地方有password這個字串,那麼如何做篩選呢?需要我們有一點js的基礎知識,在每個匹配到password的地方都讀讀原始碼,大概知道它做什麼的就行了。最後,我們可以定位到這段程式碼:

var r = baidu.form.json(e.getElement("form"));
r.token = e.bdPsWtoken, passport.data.setContext(baidu.extend({}, e.config)), r.foreignusername && (r.foreignusername = e._SBCtoDBC(r.foreignusername)), r.userName = e._SBCtoDBC(r.userName), r.verifyCode = e._SBCtoDBC(r.verifyCode);
var o = e._SBCtoDBC(e.getElement("password").value);
if (e.RSA && e.rsakey) {
        var a = o;
        a.length < 128 && !e.config.safeFlag && (r.password = baidu.url.escapeSymbol(e.RSA.encrypt(a)), r.rsakey = e.rsakey, r.crypttype = 12)
                       }
var s, u = e.getElement("submit"), d = 15e3;

上述程式碼既有rsakeyform又有password關鍵字,那麼十有八九就是加密password的方法了。主要加密語句是:

e.RSA.encrypt(a)

我們檢視encrypt()的實現

Jn.prototype.encrypt = function (e) {
    try {
            return xn(this.getKey().encrypt(e))
        } catch (t) {
            return !1
        }
}

這裡的過程大概就是先用this.getKey()返回的物件對e進行加密,然後再進行一次xn(),這裡js的程式碼十分複雜,如果想把對應的js轉化為python實現,需要很深的js和python功底,但是這個轉換已經有人幫我們做了。這裡的encrypt()即是使用rsa非對稱加密演算法對密碼進行加密。而xn()是base64編碼方法。判斷encrypt()是rsa加密演算法的依據是該js檔案中出現了多次rsakey,並且也有

 fn.prototype.getPrivateKey = function () {
            var e = "-----BEGIN RSA PRIVATE KEY-----\n";
            return e += this.wordwrap(this.getPrivateBaseKeyB64()) + "\n", e += "-----END RSA PRIVATE KEY-----"
        }, fn.prototype.getPublicKey = function () {
            var e = "-----BEGIN PUBLIC KEY-----\n";
            return e += this.wordwrap(this.getPublicBaseKeyB64()) + "\n", e += "-----END PUBLIC KEY-----"
        }

這類程式碼作為佐證。判斷後者是base64演算法的依據是xn()函式中出現了

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

這類字串,它是base64編碼的一個基礎部分。

所以說這裡的分析需要大家有基本的密碼學知識,否則分析會比較困難。這裡友情提示一句,目前主流的大中型網站都會使用rsa演算法對密碼進行加密,所以大家需要有這個意識。但是不要求大家會實現rsa等加密演算法,因為無論是python還是js還是java都有相關的實現了,我們只需要會分析會使用就行了。

到這裡所有的引數分析就結束了,我們可以通過程式碼進行驗證。

上面詳細介紹了百度整個登入流程。我們來總結一下:
1. 先通過加密js檔案獲取到gid,callback引數
2. 根據https://passport.baidu.com/v2/api/?getapi&...這個(get)請求獲取到token
3. 根據https://passport.baidu.com/v2/getpublickey?token=...這個(get)請求獲取到rsakeypubkey
4. 根據獲取到的pubkey對password進行加密,然後再進行base64編碼操作
5. 將所有固定和構造的引數進行post請求,post請求的url為https://passport.baidu.com/v2/api/?login,如果該post返回err_no=0,那麼模擬登陸就成功了,否則則失敗,會返回響應的err_no

前面費了這麼大的力氣分析百度的登入流程,如果實在是想走捷徑的,可以使用selenium自動化的方式登入,這個我也給了相關實現。讀過我的新浪微博模擬登陸的同學大概對介於直接登入和使用selenium自動化登入之間的方法還有一些印象吧,這裡我並沒有使用該方法,因為如果要使用該方法的話,需要改動一些js來使程式碼跑通。有興趣的同學可以試試,應該比較有意思。

如果有同學感覺本文有一些難度,可以嘗試一些簡單的模擬登陸,比如知乎和CSDN等,我寫過一篇關於CSDN模擬登陸的文章,微博模擬登陸應該比本文的分析難度稍微要小一點,如果有興趣,也可以讀讀。

我把程式碼放到我的開源專案smart_login上了,點選這裡可以檢視百度模擬登陸流程的實現,如果有不清楚的同學,建議對照程式碼再來讀本文,可能會更加清晰,如果實際動手按本文的分析流程
走一遍,那麼可能會有一些收穫。

此外,打一個廣告,如果對如何構建分散式爬蟲或者大規模微博資料採集感興趣的話,可以專注一下我的開源分散式微博爬蟲專案,目前還在快速迭代,單從功能角度來講,抓取部分基本上快實現和測試完了。