关注微信公众号:K哥爬虫,继续分享爬虫进阶、JS/安卓逆向等技术干货!

申明

本文章中所有内容仅供学习交换,抓包内容、敏感网址、数据接口均已做脱敏解决,严禁用于商业用途和非法用处,否则由此产生的所有结果均与作者无关,若有侵权,请在公众号分割我立刻删除!

逆向指标

本次的指标是拉勾网职位的爬取,波及到的一些要害参数如下:

  • 申请头参数:traceparentX-K-HEADERX-S-HEADERX-SS-REQ-HEADERx-anit-forge-codex-anit-forge-token
  • Cookie 值:user_trace_tokenX_HTTP_TOKEN__lg_stoken__
  • POST 申请数据加密,返回的加密职位信息解密,AES 算法

参数比拟多,但事实上有些参数固定、或者间接不要,也是能够的,比方 Cookie 的三个值,申请头的 X-K-HEADERX-SS-REQ-HEADER 等能够固定,x-anit-forge-codex-anit-forge-token 可有可无。尽管如此,本文还是把每个参数的起源都剖析了,可依据你理论状况灵活处理。

另外即使是把所有参数都补齐了,拉勾网对于单个 IP 还有频率限度,抓不了几次就要求登录,可自行搭配代理进行抓取,或者复制账号登录后的 cookies 到代码里,能够解除限制,如果是账号登录后拜访,申请头多了两个参数,即 x-anit-forge-codex-anit-forge-token,通过测试这两个参数其实不要也行。

抓包剖析

搜寻职位,点击翻页,就能够看到一条名为 positionAjax.json 的 Ajax 申请,不难判断这就是返回的职位信息。重点参数已在图中框出来了。

未登录,失常 IP,失常申请,Header 以及 Cookies:

异样 IP,登录账号后再申请,Header 以及 Cookies:

申请数据和返回数据都通过了加密:

Cookies 参数

先看 cookies 里的要害参数,次要是 user_trace_tokenX_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 规范工夫,赋值给了 _0x150c4dnew 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.gtwindow.gt 实际上是调用了 _0x11db59

跟进混同的 JS 看一下,就会发现开端的这段代码是要害,这里用到了 prototype 原型对象,咱们间接 window.gt.prototype.a() 或者 (new window.gt).a() 就能获取到 __lg_stoken__,如下图所示:

到这里兴许你想下断点去调试一下,看看能不能扣个逻辑进去,然而你会发现刷新之后断不下,因为这个混同 JS 文件是始终在变动的,之前的断点就不论用了,而后你就可能会想到间接替换掉这个 JS,让文件名固定下来,就能够断点调试了,如果你这样操作的话,从新刷新会发现始终在加载中,关上控制台会发现报错了,造成这样的起因就在于这个混同 JS 不仅文件名会扭转,他的内容也会扭转,当然,内容也不仅仅是扭转了变量名那么简略,有些值也是动态变化的,比方:

这里咱们先不论那么多,间接把所有的混同代码 copy 下来,先在本地调试一下,看看能不能跑通,调试过程中,先后会提醒 window is not definedCannot 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

申请头参数

申请头参数比拟多,有 traceparentX-K-HEADERX-S-HEADERX-SS-REQ-HEADERx-anit-forge-codex-anit-forge-token,其中最初两个 x-anit 结尾的参数是登录后才有的,理论测试中,即使是登录了,不加这两个如同也行。不过还是剖析一下吧。

x-anit-forge-code / x-anit-forge-token

这两个值是首次点击搜寻生成的,第一次拜访搜寻接口,返回的 HTML 外面夹杂了一个 JSON 文件,外面的 submitCodesubmitToken 就是 x-anit-forge-codex-anit-forge-token 的值,如下图所示:

申请这个接口要留神带上登录后的 cookies,有用的只有四个值,正确的 cookies 相似于:

cookies = {    "login": "true",    "gate_login_token": "54a31e93aa904a6bb9731bxxxxxxxxxxxxxx",    "_putrc": "9550E53D830BE8xxxxxxxxxxxxxx",    "JSESSIONID": "ABAAAECABIEACCA79BFxxxxxxxxxxxxxx"}

留神,JSESSIONID 即使不登录也会有,然而登录时应该会携带这个值,进行一个激活操作,如果你申请获取到的 submitCode、submitToken 为空,那么就有可能 JSESSIONID 是有效的,以上所有值都必须登录后复制过去!

获取 x-anit-forge-codex-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.sampledtrue,所以 e 值为 01n 值为 t.id,重点在于 t.traceIdt.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-HEADERX-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-HEADERX-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.encryptAES.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_datadef 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 subprocessfrom functools import partialsubprocess.Popen = partial(subprocess.Popen, encoding="utf-8")