在用瀏覽器進行網頁訪問時,會向網頁所在的服務器發送http協議的GET或者POST等請求,在請求中除了指定所請求的方法以及URI之外,后面還跟隨着一段Request Header。Request Header的內容主要用於描述本地信息,如所用的瀏覽器、所用的系統、語言、所能接受的返回數據的編碼格式等,其中有一個非常重要的Header項就是Cookie,Cookie可以說是網站的自定義數據集。由於服務器端無法無法控制本地(瀏覽器)的內存數據,但服務器又有必要搜集與自己所提供的服務相關的本地狀態信息,而Cookie就承載了這一功能,目的是記錄用戶在網站的狀態信息。
在用python對網頁進行訪問的時候,如果希望得到與在網頁端相同的結果,用該網頁在瀏覽器中所保留的Cookie作為python的請求Cookie是一個比較值得推薦的做法。
本文主要討論如何提取瀏覽器保存在本地的Cookie,所用的瀏覽器為Chrome。
Cookie文件
Chrome用sqlite來維護Cookie,Cookie中的信息被保存在sqlite數據庫當中,如果系統為Windows,那么數據庫文件所在的位置為[1]:
C:\Users\{UserName}\AppData\Local\Google\Chrome\User Data\Default\Cookies
其中該路徑的{UserName}是當前系統的用戶名。
username = os.environ.get('USERNAME') cookie_file = 'C:\Users\{UserName}\AppData\Local\Google\Chrome\User Data\Default\Cookies'.format(UserName=username)
Cookie表單
通過Cookie文件路徑,我們可以建立數據庫連接,然后提取出數據庫的信息。
con = sqlite3.connect(cookie_file) cursor = con.cursor()
數據庫中的cookies表就是用於保存瀏覽器Cookie的。提取表中各列的名稱
cursor.execute('SELECT * FROM cookies') for description in cursor.description: print(description[0])
各字段的描述如下
- creation_utc:Cookie產生的utc時間
- host_key:Cookie所在的網頁(domain)
- name:Cookie名稱
- value:不加密的Cookie值,由於Chrome幾乎都會對Cookie值加密后再存儲,因此這個字段基本都是空的
- path:如果服務器需要設置Cookies,那么服務器在響應瀏覽器請求的時候會返回Set-Cookie的響應,並且附帶所要設置的Cookies,這里的path的默認值就是返回Set-Cookie的那個頁面。path以'/'為開頭。
- expires_utc:Cookie的有效期限
- is_secure:指示在瀏覽器與服務器之間傳輸該Cookie時需要采用加密通道,即https
- is_httponly:當設置了該值為1時,在瀏覽器上運行的JS不能讀取到該Cookie,該Cookie只能由http請求讀取。這個標記主要目的是提高Cookie的安全性,防止無關的JS腳本竊取Cookie中的重要信息
- last_access_utc:上一次訪問到該Cookie的時間
- has_expires:Cookie的期限是否有效
- is_persistent:如果expires_utc不為0,那么這個值為1
- priority:Cookie的刪除優先級,Cookie也有存儲上限的,當超出上限則需要刪除,此時會有特定的刪除策略來刪除不同priority的Cookie
- encrypted_value:加密后的Cookie值
- firstpartyonly:first-party以及third-party是HTTP Request的一種分類,first-party指的是當前所發送的HTTP請求的URL跟瀏覽器地址欄上的URL一致;否則就是third-party。如我們平常看到的很多網頁上的圖片或者廣告,其實都是third-party request。無論first-party或者third-party,都是HTTP請求,在往服務器發送請求的時候會帶上host為該URL的Cookies,不過如果一個Cookie指定了firstpartyonly,那么如果請求為thrid-party,在發送請求的時候不會附帶該Cookie。以上面所說的網頁中的圖片為例子,如果該圖片URL的Cookie設定為firstpartyonly,在瀏覽網站時,通過third-party訪問了該圖片所在的URL,就不會發送該Cookie。
上述字段中有些是只有瀏覽器才會用到的,這里沒有必要用上,因此我們僅需要提取一些必要字段
cursor.execute('SELECT host_key, name, value, path, expires_utc, is_secure, encrypted_value ' 'FROM cookies WHERE host_key like "%{}%";'.format(domain_name))
解密Cookie
我們前面說過,Chrome在對Cookie的值保存之前會進行加密處理,並保存在數據庫的encrypt_value字段中。在Windows系統中,Cookie加密采用的是系統提供的函數CryptProtectData,我們在解密的時候也需要調用系統提供的函數CryptUnprotectData[1]。解密的Windows用戶必須與加密的用戶一致才能成功解密。
不過系統提供的是C函數,python通過ctypes庫來實現對C函數的調用。
CryptUnprotectData需要7個參數:
- pDataIn:一個指向DATA_BLOB結構體的指針,該DATA_BLOB內需存放被解密的數據。DATA_BLOB結構體內含兩個成員:cbData,數據的所占用的字節數;pbData,指向數據所在內存的指針。
- ppszDataDescr:描述該加密數據的信息,如果在進行加密操作的時候添加了描述,那么在解密的時候也能得到該描述信息。獲得該描述后需要調用系統提供的LocalFree釋放ppszDataDescr指向的內存。如果不需要,設為NULL即可。
- pOptionalEntropy:一個指向含有密鑰DATA_BLOB的指針,不過在進行Cookie加密時通常不會用到。
- pvReserved:保留參數,設為NULL即可。
- pPromptStruct:解密是一個有安全風險的操作,可能需要彈出風險提升,如果不需要彈出提示設置為NULL即可。
- dwFlags:安全相關的標志,設置為0即可。
- pDataOut:一個指向解密后的數據的DATA_BLOB,獲得解密數據后需要調用系統提供的LocalFree函數釋放pbData指向的內存。
class DATA_BLOB(ctypes.Structure): _fields_ = [("cbData", ctypes.wintypes.DWORD), ("pbData", ctypes.POINTER(ctypes.c_char))] def __init__(self, data): string = str(data) self.cbData = len(string) self.pbData = ctypes.create_string_buffer(string) def descrypt(cipher): #parameters DataIn = DATA_BLOB(cipher) Descr = ctypes.c_wchar_p() DataEntropy = DATA_BLOB('') Reserved = None PromptStruct = None CRYPTPROTECT_UI_FORBIDDEN = 0x00 DataOut = DATA_BLOB('') #win call ret = ctypes.windll.crypt32.CryptUnprotectData(ctypes.byref(DataIn), Descr, ctypes.byref(DataEntropy), Reserved, PromptStruct, CRYPTPROTECT_UI_FORBIDDEN, ctypes.byref(DataOut) ) if not ret: raise RuntimeError("failed to descrypt") buf = ctypes.create_string_buffer(int(DataOut.cbData)) ctypes.memmove(buf, DataOut.pbData, DataOut.cbData) ctypes.windll.kernel32.LocalFree(Descr) ctypes.windll.kernel32.LocalFree(DataOut.pbData) return buf.value
創建Cookie並填充CookieJar
python並不推薦我們去自行創建Cookie,因為作為用戶通常不需要去改動,甚至沒有必要知道Cookie的內容,不過此處出於特殊的需求,需要通過在Cookie數據庫中獲取的數據來創建Cookie。
首先我們來簡單了解一下Set-Cookie。我們知道Cookie都是服務器為瀏覽器設置的,設置Cookie是通過服務器返回的Response Header,如果header中包含有Set-Cookiie相關字段,則能進行Cookie的設置。
http.cookiejar.Cookie的初始化需要提供18個參數[2]:
- version:比較老版本的Cookie(如rfc2109,已淘汰)要求Cookie必須有Version字段,不過較新的版本(如rfc6265)的Cookie去掉了這一字段,因此這里填0就行。
- name:Cookie名稱
- value:Cookie值
- port:http端口,指定了該字段的Cookie只能發送到服務器的指定端口,只有rfc2965中的Set-Cookie2才用到該字段,rfc2695是一個已淘汰的版本,這里填None就行。
- port_specified:是否有指定端口,同樣是已淘汰的參數,填False。
- domain:服務器域名
- domain_specified:是否指定了服務器域名
- domain_initial_dot:服務器域名是否以"."作為開頭
- path:服務器路徑
- path_specified:是否指定了服務器路徑
- secure:是否采用安全通道
- expires:Cookie期限
- discard:是否為一次性Cookie,同上方的is_persistent,填False
- comment:Cookie注釋,填None
- comment_url:Cookie注釋所在的URL,填None
- rest:存儲該Cookie的一些非標准的屬性,填{}
- rfc2109:是否為rfc2109標准,默認值為False
cj = http.cookiejar.CookieJar() for row in cursor.fetchall(): host, name, value, path, expires, secure, encrypted_value = row[:] data = descrypt(encrypted_value) c = http.cookiejar.Cookie(0, name, data, None, False, host, host.startswith('.'), host.startswith('.'), path, True, secure, expires, False, None, None, {}) cj.set_cookie(c)
其中比較重要的domain相關參數請查看:rfc2109的Interpreting Set-Cookie、Rejecting Cookies以及rfc6265的The Domain Attribute、Storage Model
Reference: