關注微信公眾號:K哥爬蟲,持續分享爬蟲進階、JS/安卓逆向等技術乾貨!
宣告
本文章中所有內容僅供學習交流,抓包內容、敏感網址、資料介面均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關,若有侵權,請在公眾號聯絡我立即刪除!
逆向目標
本次的目標是拉勾網職位的爬取,涉及到的一些關鍵引數如下:
- 請求頭引數:
traceparent
、X-K-HEADER
、X-S-HEADER
、X-SS-REQ-HEADER
、x-anit-forge-code
、x-anit-forge-token
- Cookie 值:
user_trace_token
、X_HTTP_TOKEN
、__lg_stoken__
- POST 請求資料加密,返回的加密職位資訊解密,AES 演算法
引數比較多,但事實上有些引數固定、或者直接不要,也是可以的,比如 Cookie 的三個值,請求頭的 X-K-HEADER
、X-SS-REQ-HEADER
等可以固定,x-anit-forge-code
和 x-anit-forge-token
可有可無。儘管如此,本文還是把每個引數的來源都分析了,可根據你實際情況靈活處理。
另外即便是把所有引數都補齊了,拉勾網對於單個 IP 還有頻率限制,抓不了幾次就要求登入,可自行搭配代理進行抓取,或者複製賬號登入後的 cookies 到程式碼裡,可以解除限制,如果是賬號登入後訪問,請求頭多了兩個引數,即 x-anit-forge-code
和 x-anit-forge-token
,經過測試這兩個引數其實不要也行。
抓包分析
搜尋職位,點選翻頁,就可以看到一條名為 positionAjax.json 的 Ajax 請求,不難判斷這就是返回的職位資訊。重點引數已在圖中框出來了。
未登入,正常 IP,正常請求,Header 以及 Cookies:
異常 IP,登入賬號後再請求,Header 以及 Cookies:
請求資料和返回資料都經過了加密:
Cookies 引數
先看 cookies 裡的關鍵引數,主要是 user_trace_token
、X_HTTP_TOKEN
和 __lg_stoken__
。
user_trace_token
通過介面返回的,直接搜尋就可以找到,如下圖所示:
請求引數,time 是時間戳,a 值隨便,沒有都可以,不影響,其他值都是定值,獲取的關鍵程式碼如下:
def get_user_trace_token() -> str:
# 獲取 cookie 中的 user_trace_token
json_url = "https://a.脫敏處理.com/json"
headers = {
"Host": "a.脫敏處理.com",
"Referer": "https://www.脫敏處理.com/",
"User-Agent": UA
}
params = {
"lt": "trackshow",
"t": "ad",
"v": 0,
"dl": "https://www.脫敏處理.com/",
"dr": "https://www.脫敏處理.com",
"time": str(int(time.time() * 1000))
}
response = requests.get(url=json_url, headers=headers, params=params)
user_trace_token = response.cookies.get_dict()["user_trace_token"]
return user_trace_token
X_HTTP_TOKEN
直接搜尋沒有值,直接上 Hook 大法,小白朋友不清楚的話可以看 K 哥以前的文章,都有詳細教程,這裡不再細說。
(function () {
'use strict';
var cookieTemp = "";
Object.defineProperty(document, 'cookie', {
set: function (val) {
console.log('Hook捕獲到cookie設定->', val);
if (val.indexOf('X_HTTP_TOKEN') != -1) {
debugger;
}
cookieTemp = val;
return val;
},
get: function () {
return cookieTemp;
}
});
})();
往上跟棧除錯,是一個小小的 OB 混淆,_0x32e0d2
就是最後的 X_HTTP_TOKEN
值了,如下圖所示:
直接梭哈,才300多行,不必扣了,全部 copy 下來,本地執行,發現會報錯 document 未定義,定位到程式碼位置,下斷點除錯一下,發現是正則匹配 cookie 中的 user_trace_token
的值,那麼我們直接定義一下即可:var document = {"cookie": cookie}
,cookie 值把 user_trace_token
傳過來即可。
補全 document 後,再次執行,又會報錯 window 未定義,再次定位到原始碼,如下圖所示:
分析一下,取了 window XMLHttpRequest 物件,向 wafcheck.json 這個介面傳送了一個 Ajax GET 請求,然後取了 Response Header 的 Date 值賦值給 _0x309ac8
,注意這個 Date 值比正常時間晚了8個小時,然而取 Date 值並沒有什麼用,因為後面又 new 了一個新 Date 標準時間,賦值給了 _0x150c4d
,new Date(_0x309ac8[_0x3551('0x2d')](/-/g, '/'))
語句雖然用到了前面的舊 Date,然而實際上是 replace()
替換方法,與舊的 Date 並沒有什麼關係,然後呼叫 Date.parse()
方法將新 Date 轉換成時間戳賦值給 _0x4e6d5d
,所以不需要這麼複雜,直接本地把 _0x89ea429
方法修改一下就行了:
// 原方法
// function _0x89ea42() {
// var _0x372cc0 = null;
// if (window[_0x3551('0x26')]) {
// _0x372cc0 = new window[(_0x3551('0x26'))]();
// } else {
// _0x372cc0 = new ActiveObject(_0x3551('0x27'));
// }
// _0x372cc0[_0x3551('0x28')](_0x3551('0x29'), _0x3551('0x2a'), ![]);
// _0x372cc0[_0x3551('0x2b')](null);
// var _0x309ac8 = _0x372cc0[_0x3551('0x2c')]('Date');
// var _0x150c4d = new Date(_0x309ac8[_0x3551('0x2d')](/-/g, '/'));
// var _0x4e6d5d = Date[_0x3551('0x2e')](_0x150c4d);
// return _0x4e6d5d / 0x3e8;
// }
// 本地改寫
function _0x89ea42() {
var _0x150c4d = new Date();
var _0x4e6d5d = Date.parse(_0x150c4d);
return _0x4e6d5d / 0x3e8;
}
本地測試 OK:
\_\_lg_stoken\_\_
__lg_stoken__
這個引數是在點選搜尋後才開始生成的,直接搜尋同樣沒值,Hook 一下,往上跟棧,很容易找到生成位置:
可以看到 d 就是 __lg_stoken__
的值,d = (new g()).a()
、g = window.gt
,window.gt
實際上是呼叫了 _0x11db59
跟進混淆的 JS 看一下,就會發現末尾的這段程式碼是關鍵,這裡用到了 prototype 原型物件,我們直接 window.gt.prototype.a()
或者 (new window.gt).a()
就能獲取到 __lg_stoken__
,如下圖所示:
到這裡也許你想下斷點去除錯一下,看看能不能扣個邏輯出來,但是你會發現重新整理之後斷不下,因為這個混淆 JS 檔案是一直在變化的,之前的斷點就不管用了,然後你就可能會想到直接替換掉這個 JS,讓檔名固定下來,就可以斷點除錯了,如果你這樣操作的話,重新重新整理會發現一直在載入中,開啟控制檯會發現報錯了,造成這樣的原因就在於這個混淆 JS 不僅檔名會改變,他的內容也會改變,當然,內容也不僅僅是改變了變數名那麼簡單,有些值也是動態變化的,比如:
這裡我們先不管那麼多,直接把所有的混淆程式碼 copy 下來,先在本地除錯一下,看看能不能跑通,除錯過程中,先後會提示 window is not defined
、Cannot read properties of undefined (reading 'hostname')
,定位到程式碼,有個取 window.location.hostname
的操作,本地定義一下就行了:
再次除錯又會報錯 Cannot read properties of undefined (reading 'substr')
,substr()
方法可在字串中抽取從指定下標開始的、指定數目的字元,是字串物件 stringObject 具有的方法,我們定位到程式碼,發現是 window.location.search
物件呼叫了 substr()
方法,所以同樣的,我們本地也要補齊。
本地補齊引數後,執行結果與網頁一致:
執行結果沒問題了,那麼還有一個問題,window.location.search
的值就是待加密引數了,是咋來的呢?我們直接搜尋,就可以看到是一個介面302跳轉的地址,用的時候直接取就行了,這個介面是你搜尋內容組成的,搜尋不同引數,這個跳轉地址也是不一樣的:
除錯成功後,我們隨便換一個搜尋關鍵詞,將得到的302跳轉地址拿到這個 JS 中,加密一下,發現會報錯,這說明混淆 JS 傳入的引數和 JS 內容應該是相對應的,這裡的做法是直接請求拿到這個 JS 檔案內容,然後把要補的 window 和獲取 __lg_stoken__
的方法加進去,然後直接執行就行了。
獲取 __lg_stoken__
的關鍵程式碼如下(original_data
為原始搜尋資料):
def get_lg_stoken(original_data: dict) -> str:
# 獲取 cookie 中的 __lg_stoken__
token_url = "https://www.脫敏處理.com/wn/jobs"
token_headers = {
"Host": "www.脫敏處理.com",
"Referer": "https://www.脫敏處理.com/",
"User-Agent": UA
}
params = {
"kd": original_data["kd"],
"city": original_data["city"]
}
token_response = requests.get(url=token_url, params=params, headers=token_headers, cookies=global_cookies, allow_redirects=False)
if token_response.status_code != 302:
raise Exception("獲取跳轉連結異常!檢查 global_cookies 是否已包含 __lg_stoken__!")
# 獲取 302 跳轉的地址
security_check_url = token_response.headers["Location"]
if "login" in security_check_url:
raise Exception("IP 被關進小黑屋啦!需要登入!請補全登入後的 Cookie,或者自行新增代理!")
parse_result = parse.urlparse(security_check_url)
# url 的引數為待加密物件
security_check_params = parse_result.query
# 取 name 引數,為混淆 js 的檔名
security_check_js_name = parse.parse_qs(security_check_params)["name"][0]
# 傳送請求,獲取混淆的 js
js_url = "https://www.脫敏處理.com/common-sec/dist/" + security_check_js_name + ".js"
js_headers = {
"Host": "www.脫敏處理.com",
"Referer": security_check_url,
"User-Agent": UA
}
js_response = requests.get(url=js_url, headers=js_headers, cookies=global_cookies).text
# 補全 js,新增 window 引數和一個方法,用於獲取 __lg_stoken__ 的值
lg_js = """
window = {
"location": {
"hostname": "www.脫敏處理.com",
"search": '?%s'
}
}
function getLgStoken(){
return window.gt.prototype.a()
}
""" % security_check_params + js_response
lg_stoken = execjs.compile(lg_js).call("getLgStoken")
return lg_stoken
請求頭引數
請求頭引數比較多,有 traceparent
、X-K-HEADER
、X-S-HEADER
、X-SS-REQ-HEADER
、x-anit-forge-code
、x-anit-forge-token
,其中最後兩個 x-anit
開頭的引數是登入後才有的,實際測試中,即便是登入了,不加這兩個好像也行。不過還是分析一下吧。
x-anit-forge-code / x-anit-forge-token
這兩個值是首次點選搜尋生成的,第一次訪問搜尋介面,返回的 HTML 裡面夾雜了一個 JSON 檔案,裡面的 submitCode
和 submitToken
就是 x-anit-forge-code
和 x-anit-forge-token
的值,如下圖所示:
請求這個介面要注意帶上登入後的 cookies,有用的只有四個值,正確的 cookies 類似於:
cookies = {
"login": "true",
"gate_login_token": "54a31e93aa904a6bb9731bxxxxxxxxxxxxxx",
"_putrc": "9550E53D830BE8xxxxxxxxxxxxxx",
"JSESSIONID": "ABAAAECABIEACCA79BFxxxxxxxxxxxxxx"
}
注意,JSESSIONID 即便不登入也會有,但是登入時應該會攜帶這個值,進行一個啟用操作,如果你請求獲取到的 submitCode、submitToken 為空,那麼就有可能 JSESSIONID 是無效的,以上所有值都必須登入後複製過來!
獲取 x-anit-forge-code
、x-anit-forge-token
的關鍵程式碼如下(original_data
為原始搜尋資料):
def update_x_anit(original_data: dict) -> None:
# 更新 x-anit-forge-code 和 x-anit-forge-token
url = "https://www.脫敏處理.com/wn/jobs"
headers = {
"Host": "www.脫敏處理.com",
"Referer": "https://www.脫敏處理.com/",
"User-Agent": UA
}
params = {
"kd": original_data["kd"],
"city": original_data["city"]
}
response = requests.get(url=url, params=params, headers=headers, cookies=global_cookies)
tree = etree.HTML(response.text)
next_data_json = json.loads(tree.xpath("//script[@id='__NEXT_DATA__']/text()")[0])
submit_code = next_data_json["props"]["tokenData"]["submitCode"]
submit_token = next_data_json["props"]["tokenData"]["submitToken"]
# 注意 JSESSIONID 必須是登入驗證後的!
if not submit_code or not submit_token:
raise Exception("submitCode & submitToken 為空,請檢查 JSESSIONID 是否正確!")
global x_anit
x_anit["x-anit-forge-code"] = submit_code
x_anit["x-anit-forge-token"] = submit_token
traceparent
同樣的 Hook 大法,跟棧:
(function () {
var org = window.XMLHttpRequest.prototype.setRequestHeader;
window.XMLHttpRequest.prototype.setRequestHeader = function (key, value) {
console.log('Hook 捕獲到 %s 設定 -> %s', key, value);
if (key == 'traceparent') {
debugger;
}
return org.apply(this, arguments);
};
})();
觀察上面的程式碼,三元表示式,t.sampled
為 true
,所以 e
值為 01
,n
值為 t.id
,重點在於 t.traceId
和 t.id
了,跟棧發現很難調,直接搜尋關鍵字,可找到生成的位置:
把 E()
方法扣出來就行了,改寫一下即可:
getRandomValues = require('get-random-values')
function E(t) {
for (var b = [], w = 0; w < 256; ++w)
b[w] = (w + 256).toString(16).substr(1);
var T = new Uint8Array(16);
return function(t) {
for (var e = [], n = 0; n < t.length; n++)
e.push(b[t[n]]);
return e.join("")
}(getRandomValues(T)).substr(0, t)
}
function getTraceparent(){
return "00-" + E() + "-" + E(16) + "-" + "01"
}
// 測試輸出
// console.log(getTraceparent())
X-K-HEADER / X-SS-REQ-HEADER
X-K-HEADER
和 X-SS-REQ-HEADER
資料是一樣的,只不過後者是鍵值對形式,先直接全域性搜尋關鍵字,發現都是從本地拿這兩個值,清除 cookie 就為空了,那麼直接搜尋值,發現是 agreement 這個介面返回的,secretKeyValue
值就是我們要的,有可能瀏覽器抓包直接搜尋的話搜尋不到,使用抓包工具,比如 Fiddler 就能搜到了,如下圖所示:
這個介面是 post 請求,請求帶了一個 json 資料,secretKeyDecode
,直接搜尋關鍵字,就一個值,定位跟棧:
zt()
是從本地快取中取,At()
是重新生成:
這裡就非常明顯了,t 是32位隨機字串,賦值為 aesKey
,後面緊接著一個 RSA 加密了 aesKey
,賦值為 rsaEncryptData
,而 rsaEncryptData
就是前面 agreement 介面請求的 secretKeyValue
值。
這裡先說一下,最終搜尋職位請求的 data 和返回資料都是 AES 加密解密,會用到這個 aesKey
,請求頭的另一個引數 X-S-HEADER
也會用到,如果這個 key 沒有經過 RSA 加密並通過 agreement 介面驗證的話,是無效的,可以理解為 agreement 介面既是為了獲取 X-K-HEADER
和 X-SS-REQ-HEADER
,也是為了啟用這個 aesKey
。
這部分的 JS 程式碼和 Python 程式碼大致如下:
JSEncrypt = require("jsencrypt")
function getAesKeyAndRsaEncryptData() {
var aesKey = function (t) {
for (var e = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", r = "", n = 0; n < t; n++) {
var i = Math.floor(Math.random() * e.length);
r += e.substring(i, i + 1)
}
return r
}(32);
var e = new JSEncrypt();
e.setPublicKey("-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnbJqzIXk6qGotX5nD521Vk/24APi2qx6C+2allfix8iAfUGqx0MK3GufsQcAt/o7NO8W+qw4HPE+RBR6m7+3JVlKAF5LwYkiUJN1dh4sTj03XQ0jsnd3BYVqL/gi8iC4YXJ3aU5VUsB6skROancZJAeq95p7ehXXAJfCbLwcK+yFFeRKLvhrjZOMDvh1TsMB4exfg+h2kNUI94zu8MK3UA7v1ANjfgopaE+cpvoulg446oKOkmigmc35lv8hh34upbMmehUqB51kqk9J7p8VMI3jTDBcMC21xq5XF7oM8gmqjNsYxrT9EVK7cezYPq7trqLX1fyWgtBtJZG7WMftKwIDAQAB-----END PUBLIC KEY-----");
var rsaEncryptData = e.encrypt(aesKey);
return {
"aesKey": aesKey,
"rsaEncryptData": rsaEncryptData
}
}
// 測試輸出
// console.log(getAesKeyAndRsaEncryptData())
def update_aes_key() -> None:
# 通過JS獲取 AES Key,並通過介面啟用,介面啟用後會返回一個 secretKeyValue,後續請求頭會用到
global aes_key, secret_key_value
url = "https://gate.脫敏處理.com/system/agreement"
headers = {
"Content-Type": "application/json",
"Host": "gate.脫敏處理.com",
"Origin": "https://www.脫敏處理.com",
"Referer": "https://www.脫敏處理.com/",
"User-Agent": UA
}
encrypt_data = lagou_js.call("getAesKeyAndRsaEncryptData")
aes_key = encrypt_data["aesKey"]
rsa_encrypt_data = encrypt_data["rsaEncryptData"]
data = {"secretKeyDecode": rsa_encrypt_data}
response = requests.post(url=url, headers=headers, json=data).json()
secret_key_value = response["content"]["secretKeyValue"]
X-S-HEADER
X-S-HEADER
你每次翻頁都會改變,直接搜尋關鍵字可定位:
中間有一個 SHA256 加密,最後返回的 Rt(JSON.stringify({originHeader: JSON.stringify(e), code: t}))
就是 X-S-HEADER
的值了,Rt()
是一個 AES 加密,比較關鍵的,Vt(r)
是一個 URL,比如你搜尋職位就是 positionAjax.json,搜尋公司就是 companyAjax.json,可根據實際情況定製,然後 Lt(t)
就是搜尋資訊,字串形式,包含了城市、頁碼、關鍵詞等。
獲取 X-S-HEADER
的 JS 程式碼大致如下:
CryptoJS = require('crypto-js')
jt = function(aesKey, originalData, u) {
var e = {deviceType: 1}
, t = "".concat(JSON.stringify(e)).concat(u).concat(JSON.stringify(originalData))
, t = (t = t, null === (t = CryptoJS.SHA256(t).toString()) || void 0 === t ? void 0 : t.toUpperCase());
return Rt(JSON.stringify({
originHeader: JSON.stringify(e),
code: t
}), aesKey)
}
Rt = function (t, aesKey) {
var Ot = CryptoJS.enc.Utf8.parse("c558Gq0YQK2QUlMc"),
Dt = CryptoJS.enc.Utf8.parse(aesKey),
t = CryptoJS.enc.Utf8.parse(t);
t = CryptoJS.AES.encrypt(t, Dt, {
iv: Ot,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return t.toString()
};
function getXSHeader(aesKey, originalData, u){
return jt(aesKey, originalData, u)
}
// 測試樣例
// var url = "https://www.脫敏處理.com/jobs/v2/positionAjax.json"
// var aesKey = "dgHY1qVeo/Z0yDaF5WV/EEXxYiwbr5Jt"
// var originalData = {"first": "true", "needAddtionalResult": "false", "city": "全國", "pn": "2", "kd": "Java"}
// console.log(getXSHeader(aesKey, originalData, url))
請求/返回資料解密
前面抓包我們已經發現 positionAjax.json 是 POST 請求,Form Data 中的資料是加密的,返回的 data 也是加密的,我們分析請求頭引數的時候,就涉及到 AES 加密解密,所以我們直接搜尋 AES.encrypt
、AES.decrypt
,下斷點除錯:
非常明顯了,這部分的 JS 程式碼大致如下:
CryptoJS = require('crypto-js')
function getRequestData(aesKey, originalData){
return Rt(JSON.stringify(originalData), aesKey)
}
function getResponseData(encryptData, aesKey){
return It(encryptData, aesKey)
}
Rt = function (t, aesKey) {
var Ot = CryptoJS.enc.Utf8.parse("c558Gq0YQK2QUlMc"),
Dt = CryptoJS.enc.Utf8.parse(aesKey),
t = CryptoJS.enc.Utf8.parse(t);
t = CryptoJS.AES.encrypt(t, Dt, {
iv: Ot,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return t.toString()
};
It = function(t, aesKey) {
var Ot = CryptoJS.enc.Utf8.parse("c558Gq0YQK2QUlMc"),
Dt = CryptoJS.enc.Utf8.parse(aesKey);
t = CryptoJS.AES.decrypt(t, Dt, {
iv: Ot,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString(CryptoJS.enc.Utf8);
try {
t = JSON.parse(t)
} catch (t) {}
return t
}
// 測試樣例,注意,encryptedData 資料太多,省略了,直接執行解密是會報錯的
// var aesKey = "dgHY1qVeo/Z0yDaF5WV/EEXxYiwbr5Jt"
// var encryptedData = "r4MqbduYxu3Z9sFL75xDhelMTCYPHLluKaurYgzEXlEQ1Rg......"
// var originalData = {"first": "true", "needAddtionalResult": "false", "city": "全國", "pn": "2", "kd": "Java"}
// console.log(getRequestData(aesKey, originalData))
// console.log(getResponseData(encryptedData, aesKey))
大致的 Python 程式碼如下:
def get_header_params(original_data: dict) -> dict:
# 後續請求資料所需的請求頭引數
# 職位搜尋 URL,如果是搜尋公司,那就是 https://www.脫敏處理.com/jobs/companyAjax.json,根據實際情況更改
u = "https://www.脫敏處理.com/jobs/v2/positionAjax.json"
return {
"traceparent": lagou_js.call("getTraceparent"),
"X-K-HEADER": secret_key_value,
"X-S-HEADER": lagou_js.call("getXSHeader", aes_key, original_data, u),
"X-SS-REQ-HEADER": json.dumps({"secret": secret_key_value})
}
def get_encrypted_data(original_data: dict) -> str:
# AES 加密原始資料
encrypted_data = lagou_js.call("getRequestData", aes_key, original_data)
return encrypted_data
def get_data(original_data: dict, encrypted_data: str, header_params: dict) -> dict:
# 攜帶加密後的請求資料和完整請求頭,拿到密文,AES 解密得到明文職位資訊
url = "https://www.脫敏處理.com/jobs/v2/positionAjax.json"
referer = parse.urljoin("https://www.脫敏處理.com/wn/jobs?", parse.urlencode(original_data))
headers = {
# "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"Host": "www.脫敏處理.com",
"Origin": "https://www.脫敏處理.com",
"Referer": referer,
"traceparent": header_params["traceparent"],
"User-Agent": UA,
"X-K-HEADER": header_params["X-K-HEADER"],
"X-S-HEADER": header_params["X-S-HEADER"],
"X-SS-REQ-HEADER": header_params["X-SS-REQ-HEADER"],
}
# 新增 x-anit-forge-code 和 x-anit-forge-token
headers.update(x_anit)
data = {"data": encrypted_data}
response = requests.post(url=url, headers=headers, cookies=global_cookies, data=data).json()
if "status" in response:
if not response["status"] and "操作太頻繁" in response["msg"]:
raise Exception("獲取資料失敗!msg:%s!可以嘗試補全登入後的 Cookies,或者新增代理!" % response["msg"])
else:
raise Exception("獲取資料異常!請檢查資料是否完整!")
else:
response_data = response["data"]
decrypted_data = lagou_js.call("getResponseData", response_data, aes_key)
return decrypted_data
最終整合所有程式碼,成功拿到資料:
逆向小技巧
瀏覽器開發者工具 Application - Storage 選項,可以一鍵清除所有 Cookies,也可以自定義儲存配額:
Storage - Cookies 可以檢視每個站點的所有 Cookies,HttpOnly 打勾的表示是伺服器返回的,選中一條 Cookie,右鍵可以直接定位到哪個請求帶了這個 Cookie,也可以直接編輯值,還可以刪除單個 Cookie,當你登入了賬號,但又需要清除某個 Cookie,且不想重新登入時,這個功能或許有用。
完整程式碼
文中給出了部分關鍵程式碼,不能直接執行,部分細節可能沒提及到,完整程式碼已放 GitHub,均有詳細註釋,歡迎 Star。所有內容僅供學習交流,嚴禁用於商業用途、非法用途,否則由此產生的一切後果均與作者無關,在倉庫中下載的檔案學習完畢之後請於 24 小時內刪除!
倉庫地址:https://github.com/kgepachong...
常見問題
- JS 程式碼裡引用了三個庫,npm install 安裝一下即可,如果安裝了還提示找不到庫,那就是路徑問題,推薦在當前目錄下執行命令安裝,或者在 Python 程式碼裡指定完整路徑,具體方法可自行百度。
- jsencrypt 這個庫,本地執行可能會報錯
window is not defined
,在 \node_modules\jsencrypt\bin\jsencrypt.js 原始碼中加入var window = global;
即可,這是實現 RSA 加密的庫,當然還有很多其他實現方法或者庫,都可以。 - execjs 執行 JS 的時候,可能會報編碼錯誤
"gbk" can't decode byte...
,有兩種解決方法,一是找到官方原始碼 subprocess.py,搜尋encoding=None
改成encoding='utf-8'
,二是直接在 Python 程式碼裡面加入以下程式碼即可:
import subprocess
from functools import partial
subprocess.Popen = partial(subprocess.Popen, encoding="utf-8")