讓我大吃一塹的前後分離 web 站模擬登入

AsyncIns發表於2019-03-04

很多 Web 站都採用前後端分離的技術。以前儲存使用者身份資訊靠 Cookie,那前後分離這種技術組合靠什麼校驗使用者身份呢?看起來正常的資料,傳送過去為什麼總是 400 呢?

一、背景

scrapy 模擬登入相信大家都會,而且非常的熟練。但是技術一直在進步(尤其是前端領域),近幾年前後端分離的趨勢越來越明顯,很多 web 站都採用前後端分離的技術。以前儲存使用者身份資訊靠 Cookie,那前後分離這種技術組合靠什麼校驗使用者身份呢?

二、登入操作

前後端分離的專案,一般都是 react、vue 等 js 庫編寫的,進而湧現出了一批優秀的前端框架或元件,如阿里巴巴前端團隊的 AntDesign,餓了麼前端團隊的 ElementUI 等。由於前後端分離的原因,後端必定有 API,所以最好的爬取策略不是在頁面使用 CSS 定位或者 Xpath 定位,而是觀察網路請求記錄,找到 api 以及請求時傳送的引數並用 Python 進行構造、模擬請求。

輸入圖片說明
在這裡輸入圖片標題

以這裡的登入為例,通過css定位其實也可以,但是有不穩定的風險。所以還是看api和引數比較穩妥,前端變化的機率比後端高出太多。在頁面中開啟除錯工具,然後定位到『網路』選項卡,接著開啟登入頁並輸入使用者名稱密碼並登入。

輸入圖片說明
在這裡輸入圖片標題

在請求記錄中找到並選中方法為 post 的那條記錄就可以檢視此請求的詳細資訊,比如請求地址、請求頭和引數。請求詳情如下圖所示:

輸入圖片說明
在這裡輸入圖片標題

請求引數如下圖所示:

輸入圖片說明
在這裡輸入圖片標題

可以看到請求引數中有使用者名稱、密碼以及使用者名稱型別(比如手機號或郵箱)。得到完整的請求資訊後就可以根據請求地址、請求頭和引數來構造登入用的程式碼,Scrapy 常用登入程式碼如下:

    def start_requests(self):
        """ 過載start_requests方法 通過is_login方法判斷是否成功登入 """
        login_url = "http://xxx.yyy.ccc.aa/api/v1/oauth/login"
        login_data = {
            "username": "abcd@easub.com",
            "password": "faabbccddeeffggd5",
            "type": "email"
        }

        return [scrapy.FormRequest(url=login_url, formdata=login_data, callback=self.is_login)]

    def is_login(self, response):
        """
        根據返回值中的message值來判斷是否登入成功
            如果登入成功則對資料傳輸頁發起請求,並將結果回傳給parse方法
            如果登入失敗則提示
        由於後面的使用者許可權驗證需要用到token資訊,所以這裡取到登入後返回的token並傳遞給下一個方法
        """
        results = json.loads(response.text)
        if results[`message`] == "succeed":
            urls = `http://xxx.yyy.ccc.aa`
            access_token = results[`data`][`access_token`]
            print("登入成功,開始呼叫方法")
            yield Request(url=urls, callback=self.parse, meta={"access_token": access_token})
        else:
            print("登入失敗,請重新檢查")
複製程式碼

如果返回資訊的 json 裡面 message 值為 succeed 即認為登入成功並呼叫 parse 方法。

三、使用者許可權驗證

登入完畢後想執行其他的操作,比如上傳(post)資料的話,我應該怎麼做?

首先要跟剛才一樣,需要通過真實操作觀察請求記錄中對應記錄的請求詳情,根據 api 的地址和所需引數請求頭等資訊用程式碼進行構造,模擬真實的網路請求傳送場景。下圖為提交表單的請求詳情資訊:

輸入圖片說明
請求詳情
輸入圖片說明
請求引數

跟上面類似,根據返回的引數和請求頭構造程式碼,結果會如何?

結果返回的狀態碼是 401,由於 scrapy 預設只處理 2xx 和 3xx 狀態的請求、4開頭和5開頭的都不處理,但是我們又需要觀察401狀態返回的內容,這怎麼辦呢?

我們可以在settings.py中空白處新增程式碼:

""" 狀態碼處理 """
HTTPERROR_ALLOWED_CODES = [400, 401]
複製程式碼

然後在下一個方法中觀察response回來的資料(這個地方當時作為萌新的我是懵逼的,所以委屈各位讀者大佬跟我一起懵逼)。

後來查詢了401的意思:未獲得授權,也就是使用者許可權驗證不通過。經過多方資料查詢,發現請求頭中有這麼一條:

輸入圖片說明
在這裡輸入圖片標題

它就是用於使用者許可權驗證的,authorization 的值分為兩部分:type 和 credentials,前者是驗證採用的型別,後者是具體的引數值。這裡的型別可以看到用的是 Bearer 型別。

我又去觀察登入時候的返回值,發現登入成功後的返回值除了 succeed 之外,還有其他的一些返回值,裡面包括了一個叫 access_token 的欄位,看樣子它是 JWT 登入方式用來鑑權的 token 資訊,經過比對確認 authorization 用的也正好就是這個 token 作為值。

那麼程式碼就應該在第一次登入時候,取出access_token的值,並傳遞下去,用於後面請求的鑑權,所以程式碼改為:

    def is_login(self, response):
        """
        根據返回值中的message值來判斷是否登入成功
            如果登入成功則對資料傳輸頁發起請求,並將結果回傳給parse方法
            如果登入失敗則提示
        由於後面的使用者許可權驗證需要用到token資訊,所以這裡取到登入後返回的token並傳遞給下一個方法
        """
        results = json.loads(response.text)
        if results[`message`] == "succeed":
            urls = `http://xxx.yyy.ccc.aa`
            access_token = results[`data`][`access_token`]
            print("登入成功,開始呼叫方法")
            yield Request(url=urls, callback=self.parse, meta={"access_token": access_token})
        else:
            print("登入失敗,請重新檢查")
複製程式碼

下面的pase方法中,將 authorization 設定到 header 中以對資料進行請求:

header = {
            "authorization": "Bearer " + access_token
        }
複製程式碼

這樣就解決了使用者許可權的問題,不再出現401

四、postman傳送請求特殊格式資料(json)

在 parse 方法中根據瀏覽器觀察到的引數進行構造:

datas = {
                "url": "https://www.youtube.com/watch?v=eWeACm7v01Y",
                "title": "看上去可愛其實很笨的狗#動物萌寵#",
                "share_text": "看上去可愛其實很笨的狗#動物萌寵#[doge]",
                "categories": {`0`: `00e2e120-37fd-47a8-a96b-c6fec7eb563d`}
        }
複製程式碼

由於categories裡面是個陣列,所以在構造的時候也可以直接寫資料,然後用 scrapy.Formdata 來進行 post。發現返回的狀態是這次是 400,並且提示:categories 必須是陣列。

再次觀察請求頭資訊,發現請求頭資訊中還有:

輸入圖片說明
在這裡輸入圖片標題

我將這個叫做 content-type 的欄位和引數加入到 header 中:

        header = {
            "authorization": "Bearer " + access_token,
            "content-type": "application/json",
        }
複製程式碼

這樣關於 categories 必須是陣列的提示就沒有了。

但是返回的狀態碼依然是400,而且提示變成了 “url不能為空”。

這到底又是怎麼一回事?

多方探查都沒有結果。

真是傷心

後來我又想起了,既然這裡的文字型別 是application/json,那麼提交出去的文字應該是 json 型別資料,而不是 python 的 dict 字典。

於是開啟 json 線上解析,對傳遞的引數進行觀察,發現這樣的資料並不滿足json格式:

輸入圖片說明
在這裡輸入圖片標題

後來嘗試對它進行更改:

輸入圖片說明
在這裡輸入圖片標題

在外層增加了一對{},然後又將 categories 的值加上了雙引號,才是正確的 json 格式(我是真的又菜又蠢)。

將這樣的資料拿到 postman 中進行測試,發現是不行的。又經過我不斷的測試,最終確定了 postman 的請求格式為:

輸入圖片說明
在這裡輸入圖片標題
輸入圖片說明
在這裡輸入圖片標題
輸入圖片說明
在這裡輸入圖片標題

我是對 Auth、Headers 和 Raw 進行設定(請跟我一起懵逼),才終於成功傳送 post,返回正確的資訊!!!

五、Scrapy 傳送 Json 格式資料

在 postman 測試通過後,說明這樣的做法是可行的,但是程式碼上怎麼編寫呢?

用之前的 scrapy.Formdata 是不行的,它的 formdat= 預設使用 dict 格式,如果強行轉成 json 格式也是會報錯的。

經過群裡諮詢和搜尋,發現要用 scrapy.http 的 Requst 方法(平時經常用的這個):

access_token = response.meta[`access_token`]
        urls = "http://aaa.bbb.xxx.yy/api/v1/material/extract"
        datas = {
                "url": "https://www.youtube.com/watch?v=eWeACm7v01Y",
                "title": "看上去可愛其實很笨的狗#動物萌寵#",
                "share_text": "看上去可愛其實很笨的狗#動物萌寵#[doge]",
                "categories": {`0`: `00e2e120-37fd-47a8-a96b-c6fec7eb563d`}
        }
        header = {
            "authorization": "Bearer " + access_token,
            "content-type": "application/json",
        }
        yield Request(url=urls, method=`POST`, body=json.dumps(datas), headers=header, callback=self.parse_details)
複製程式碼

這樣傳送請求,終於成功了!!!

為什麼成功了?

首先看一看 json.dumps 函式的用途是什麼: json.dumps() 用於將 dict 型別的資料轉成 str。

雖然沒有摸清楚訊息傳送失敗的根本原因(有可能是目標網站後端對資料格式進行校驗,也有可能是 Scrapy 框架會在傳送請求前對引數進行處理所以導致的問題),但是已經可以猜出個大概。同時也在本次爬蟲任務中學習到了一些知識。

從本文中我們學會了三個知識:

第 1 是萌新要多問、多測試,沒有解決不了的計算機問題;

第 2 是爬取使用前後端分離技術的 Web 站時應該優先選擇從 API 下手;

第 3 是網路請求詳情中看到的引數格式並非是你認為的引數格式,它有可能是經過編碼的字串;

相關文章