案例引入
网站: https://spa7.scrape.center
这里是一个简单的 NBA 球星的网站, 用卡片形式展示了一些球星的基本信息。另外,每张卡片其实都有一个加密字符串,这个加密字符串其实和球星的信息是由关联的, 并且每个球星的加密字符串也是不同的。
所以,这里我们要做的就是找出这个加密字符串的加密算法并且用程序把加密字符串的生成过程模拟出来
准备工作
这里我们要使用 Python 来执行 JS , 需要用的库叫作 PyExecJS
安装: pip install pyexecjs
PyExecJS 是用于执行 JS 的, 但执行 JS 的功能需要依赖于 JS 的运行环境, 所以除了安装这个库之外,还需要一个 JS 的环境, 例如 Node.js , 具体安装方式请查阅资料
都安装好之后检测环境
import execjs print(execjs.get().name)
Node.js (V8)
如果你成功安装了,那么对应的输出就一样,如果是其他环境,那么就找对应的测试方法
分析
我们在这里很快就可以找到加密字符串的生成逻辑(因为这里主要说的是在 Python 执行 JS)
这里首先声明了 球员的列表, 然后对每个球员调用加密算法对其进行信息加密,这里加入断点看一下,其实 getToken 方法的输入就是单个球员的信息, 就是上面列表的每个元素信息,this.key 就是一个固定的字符串。整个加密逻辑就是提取球员的各项信息,先进行 Base64 编码,然后进行 DES 加密,最后返回结果
加密算法怎么实现的呢 ? 其实是依赖了 crypto-js 库, 使用 CryptoJS 对象来实现的。
那么 , CryptoJS 这个对象哪里来的呢? 其实这个网站直接引用 crypto-js 库
执行 crypto-js 库对应的这个 JS 文件之后 , CryptoJS 就会被注入浏览器全局环境下,一次我们就可以在别的方法里直接使用 CryptoJS 对象里的方法了
模拟调用
首先我们要模拟的其实就是 getToken 方法, 输入球员相关信息,得到最终的加密字符串。这里我们直接把 key 替换下, 把 getToken 方法稍微改写一下
function getToken(player) { let key = CryptoJS.enc.Utf8.parse("fipFfVsZsTda94hJNKJfLoaqyqMZFFimwLt"); const {name, birthday, height, weight} = player; let base64Name = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(name)); let encrypted = CryptoJS.DES.encrypt( `${base64Name}${birthday}${height}${weight}`, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7, } ); return encrypted.toString(); }
因为这个方法的模拟执行需要 CryptoJS 对象, 如果我们直接调用这个方法,肯定会报 CryptoJS 未定义的错误
我们只需要再模拟执行一下刚才看到的 crypto-js.min.js 就好了
因此我们需要模拟执行的内容就是一下两部分
1. 模拟运行 crypto-js.min.js 里面的 JavaScript 。 用于声明 CryptoJS 对象
2. 模拟运行 getToken 方法的定义,用于声明 getToken 方法
接着我们就把 crypto-js.min.js 里面的代码和 上面的 getToken 方法的代码复制一下,都粘贴到一个 JS 文件里面, 比如叫作 crypto.js
接下来,我们就用 PyExecJS 模拟执行一下
import execjs import json item = { 'name': '凯文-杜兰特', 'image': 'durant.png', 'birthday': '1988-09-29', 'height': '208cm', 'weight': '108.9KG' } file = 'crypto.js' node = execjs.get() ctx = node.compile(open(file).read()) js = f"getToken({json.dumps(item, ensure_ascii=False)})" print(js) result = ctx.eval(js) print(result)
这里单独定义了一个球员的信息, 并将其赋值 item 变量。然后使用execjs 的 get 方法获取 JS 执行环境, 赋值为 node
接着我们调用 node 的 compile 方法, 这里给它传入刚才定义的 crypto.js 文件的文本内容。 compile 方法会返回一个 JS 的上下文对象, 我们将其赋给 ctx 。 执行到这里,其实就可以理解为, ctx 对象里面就执行过了 crypto-js.min.js , CryptoJS 就声明好了,然后紧接着 getToken 方法的声明代码也被执行,所以 getToken 方法也定义好了, 相当于完成了一些初始化工作
接着我们只需要定义我们想要执行的 JS 代码。 我们定义了 js 变量,其实就是模拟调用了 getToken 方法并传入了球员信息。 并打印出了 球员信息
接着,调用 ctx 对象的 eval 方法并传入了 js 变量, 其实就是模拟执行这句代码, 照理说最终返回的就是加密字符串了,但是却报错了
这里说 CryptoJs is not defined
问题其实出现在 crypto-js.min.js ,可以看其中声明了一个 JS 的自执行方法
!function(t, e) { "object" == typeof exports ? module.exports = exports = e() : "function" == typeof define && define.amd ? define([], e) : t.CryptoJS = e() }(this, function()
自执行方法: 就是声明了一个方法,然后接着调用执行。 格式:
!(function(a,b){ console.log('result', a, b)})(1, 2)
这里我们先声明了一个 function , 它接收 a 和 b 两个参数,然后把内容输出出来,接着我们把这个 function 用小括号括起来。 这其实就是一个方法, 可以被直接调用,怎么调用呢? 后面在跟上对应的参数就好了, 比如这里传入 1, 2 执行结果就是
result 1 2
同理,crypto-js.min.js 也符合这个格式,它接收 t, e 两个参数, t 就是 this , 其实就是浏览器中的 widow 对象, e 就是一个 function (用于定义 CryptoJS 和核心内容)
我们再来看下 crypto-js.min.js 开头的定义
"object" == typeof exports
? module.exports = exports = e()
: "function" == typeof define && define.amd
? define([], e)
: t.CryptoJS = e() }(this, function()
在 Node.js 中, 其实 exports 用来就一些对象的定义导出, 这里 " object " == typeof exports 的结果其实就是 true , 所以执行了 module.exports = exports = e() 这段代码, 这相当于把 e() 作为整体导出, 而这个 e()其实就是对应后面整个 function 里面定义了加密相关的各个实现,其实就是代指整个加密算法库
但是在在浏览器中,其结果就不一样了, 浏览器环境中并没有 exorts 和 define 这两个对象,所以,上述代码在浏览器中最后执行的就是 t.CryptoJS = e() 这段代码, 其实这里就是把 CryptoJS 对象挂在到了 this 上面, 而 this 就是浏览器中的全局 window 对选哪个, 后面就可以直接用了。如果我们把这段代码放在浏览器中,没有任何问题
然而,使用的 PyExecJS 是依赖于一个 Node.js 执行环境的, 所以上述代码其实执行的是 module.exports = exports = e() , 这里面饼没有声明 CryptoJS 对象,也没有把 CryptoJS 挂在到全局对象里面, 所以就报错了
我们在这里只需要声明一个 CryptoJS 变量, 然后手动声明一下它的初始化就好了
!function(t, e) { CryptoJS = e(); "object" == typeof exports ? module.exports = exports = e() : "function" == typeof define && define.amd ? define([], e) : t.CryptoJS = e() }(this, function()
我们就加了 CryptoJS = e(); 然后重新运行
DG1uMMq1M7OeHhds71HlSMHOoI2tFpWCB4ApP00cVFqptmlFKjFu9RluHo2w3mUw
得到了加密 字符串