准备介绍: 当我们学习完整个 JS 逆向技巧后,这里是一次完整的分析爬取实战
案例介绍
本节案例网站不仅在 API 参数有加密, 而且前端 JS 也带有压缩混淆,其前端压缩打包工具使用 webpack , 混淆工具使用 javascript-obfuscator 。 分析该网站需要熟练掌握浏览器的开发者使用工具和一定的调试技巧,另外还需要用到一些 Hook 技术等辅助分析手段
案例网址: https://spa6.scrape.center
看着没什么不同,点进去看一下每部电影的 URL 的变化
可以看到详情页的 URL 包含了一个长字符串,看上去像是 Base64 编码
接下来看 Ajax 请求, 我们从第一页到第十页依次点击一下,看看 Ajax 请求的变化
可以看到, Ajax 接口的 URL 里多了一个 token ,而且不同的页码, token 都是不一样的,它们看上去同样是 Base64 编码的字符串
另外,更困难的是,这个接口还有时效性。 如果我们把 Ajax 接口的 URL 直接复制下来,短期内可以访问,但是过段时间就无法访问了,会直接返回 401 状态码
我们这里把第一部电影的返回结果全部展开了, 但是刚才我们观察到第一部电影的 URL 是
https://spa6.scrape.center/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx
看起来是 Base64 编码,我们对其进行解码, 结果为
ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb1
看起来毫无规律,这个解码后的结果怎么来的? 返回的结果里也并不包含这个字符串,这又是怎么构造的
还有这仅仅是详情页的 URL , 其真实数据是通过 Ajax 加载的, 那么 Ajax 请求又是怎样的呢?
这里我们发现其 Ajax 接口除了包含刚才所说的 URL 中携带的字符串,又多了一个 token ,同样也是类似 Base64 编码的内容,总结下来这个网站就有如下特点
列表页的 Ajax 接口带有加密的 token
详情页的 URL 带有加密 id
详情页的 Ajax 接口参数带有加密 id 和加密的 token
如果我们想要通过接口的形式爬取, 必须把这些加密 id 和 token 构造出来才行,而且必须一步步来。首先我们要构造出列表页 Ajax 接口的 token 参数,然后获取每部电影的数据信息,接着根据数据信息构造出加密 id 和加密 token
到此为止,我们知道了这个网站接口的加密情况,下一步就是去找这个加密实现逻辑
由于是网页,所以其加密逻辑一定藏在前端代码里,但是,前端为了保护其接口加密逻辑不被轻易的分析出来,会采取压缩,混淆等方式来加大分析难度。下面我们来看看这个网站的源代码和 JS 文件是怎样的
首先看网站源代码,我们在网站上点击右键, 此时会弹出快捷菜单,然后点击查看源代码
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><title>Scrape | Movie</title><link href=/css/chunk-19c920f8.2a6496e0.css rel=prefetch><link href=/css/chunk-2f73b8f3.5b462e16.css rel=prefetch><link href=/js/chunk-19c920f8.c3a1129d.js rel=prefetch><link href=/js/chunk-2f73b8f3.8f2fc3cd.js rel=prefetch><link href=/js/chunk-4dec7ef0.e4c2b130.js rel=prefetch><link href=/css/app.ea9d802a.css rel=preload as=style><link href=/js/app.5ef0d454.js rel=preload as=script><link href=/js/chunk-vendors.77daf991.js rel=preload as=script><link href=/css/app.ea9d802a.css rel=stylesheet></head><body><noscript><strong>We're sorry but portal doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/js/chunk-vendors.77daf991.js></script><script src=/js/app.5ef0d454.js></script></body></html>
这是一个典型的 SPA(单页 Web 应用)页面,其 JS 文件名带有编码字符, chunk, vendors 等关键字,这是经过 webpack 打包压缩后的源代码,目前主流起那段开发框架 Vue.js , React.js 等的输出结果都是类似这样的
接下来,我们再看一下其 JS 代码是什么样子的。 在开发者工具中打开 Sources 选项卡下的 Page 选项卡, 然后打开 js 文件夹, 在这里我们能看到 JS 的源代码
我们随便复制一些,看看什么样子的
(window['webpackJsonp'] = window['webpackJsonp'] || [])['push']([['chunk-2f73b8f3'], {
'02f4': function(_0x22a2a8, _0xab6f51, _0x4245f8) {
var _0x249496 = _0x4245f8('4588')
, _0x123c24 = _0x4245f8('be13');
_0x22a2a8['exports'] = function(_0x486f5a) {
return function(_0x411fbe, _0x52029e) {
var _0x51e1ce, _0xe9c0f4, _0x4b2912 = String(_0x123c24(_0x411fbe)), _0x4d2326 = _0x249496(_0x52029e), _0x110e8e = _0x4b2912['length'];
return _0x4d2326 < 0x0 || _0x4d2326 >= _0x110e8e ? _0x486f5a ? '' : void 0x0 : (_0x51e1ce = _0x4b2912['charCodeAt'](_0x4d2326),
_0x51e1ce < 0xd800 || _0x51e1ce > 0xdbff || _0x4d2326 + 0x1 === _0x110e8e || (_0xe9c0f4 = _0x4b2912['charCodeAt'](_0x4d2326 + 0x1)) < 0xdc00 || _0xe9c0f4 > 0xdfff ? _0x486f5a ? _0x4b2912['charAt'](_0x4d2326) : _0x51e1ce : _0x486f5a ? _0x4b2912['slice'](_0x4d2326, _0x4d2326 + 0x2) : _0xe9c0f4 - 0xdc00 + (_0x51e1ce - 0xd800 << 0xa) + 0x10000);
}
;
}
可以看到一些变量是十六进制字符串,而且代码被压缩了
没错,我们就是要从这里找出 token 和 id 的构造逻辑
寻找列表页 Ajax 入口
这里简单介绍两种寻找入口的方法
全局搜索标志字符串
设置 Ajax 断点
全局搜索标志字符串
一些关键的字符串通常会被作为寻找 JS 混淆入口的依据,我们可以通过全局搜索的方式来查找,然后根据搜索到的结果答题观察入口是否为我们想找的入口
重新打开 Ajax 接口, 看一下请求的 Ajax 接口
这里 Ajax 接口的 URL 为
https://spa6.scrape.center/api/movie/?limit=10&offset=0&token=ZjI3YjI1NDRlYjM2NjFkNWNjM2M0MGIzYjZkY2UwMmJhNTgxYWM3ZSwxNzIzNDU2NDE1
,可以看到带有 limit . offset , token 三个参数,关键就是找 token , 我们就全局搜索是否存在 token 点击开发者右上角的“三个小竖点” 然后点击 Search
这样我们就进入了全局搜索模式,搜索 token,可以看到搜索到了几个结果
观察一下,下面两个结果可能是我们想要的,点击第一个进入看看,此时定位到一个 JS 文件
如果是一行,就点击左下角的 { } 进行格式化,然后我们定位到 token ,可以看到这里有 limit , offset , token 。然后观察其他逻辑,基本上能够确定这就是构造 Ajax 请求的地方,如果不是的话,可以继续搜索其他文件观察
设置 Ajax 断点
由于这里的字符串 token 并没有被混淆, 所以上面的方法是奏效的。因为这种字符串非常容易称为寻找入口的依据,所以这样的字符串也会被混淆成类似 Unicode , Base64 , RC4 等的编码形式,这样我们就不能轻松的搜索到了
另外,我们也可以通过 XHR 断点,方便的找到发起 Ajax 请求的一些入口位置
我们可以在 Sources 选项卡右侧 XHR/fetch Breakpoints 处添加一个断点,首先点击 + 号, 此时就会让我们输入匹配的 URL 内容,由于 Ajax 接口的形式是 /api/movie/?limit=10..... 这样的格式,所以截取一段填进去就好了,这里填的就是 /api/movie
然后重新刷新页面,就进入了断点模式
如果代码又变成一行,我们还是点击 { } 格式化代码,找到断点位置,这里可以看到有一个 send 字符,我们可以初步猜测它相当于发送 Ajax 请求的一瞬间
这里我们来回溯查找相关逻辑,点击 Call Stack , 这里记录了 JS 方法的逐层调用过程
当前指向的是一个名为 anonymous (也就是匿名)的调用,在他下面显示了调用 anonymous 的方法名,叫作: _0x29474e 的方法,然后 _0x29474e 下面的方法又是调用 _0x29474e 的,一次类推。我们可以继续找下去,观察类似 token 这样的信息,就能找到对应的位置了。
最后我们找到了 onFetchData , 这个方法实现了 token的构造逻辑,这样就成功找到了 token 的参数构造位置了
到此为止,我们就通过两个方法找到入口了,其实还有其他寻找入口的方法,比如 Hook 关键函数等
寻找列表页加密逻辑
我们已经找到 token 的位置了, 可以观察这个 token 对应的变量,它叫作 _0x263439 所以关键就要看这个变量哪里来的
怎么找呢? 加断点就好了
看一下变量在哪里生成的,然后我们在对应的行添加断点, 我们先取消刚才打的 XHR 断点
这时我们就设置了一个断点,由于只有一个断点,刷新网页之后,我们会发现网页停在新断点上
这时我们就能观察到正在运行的一些变量了,比如把鼠标放在各个变量上,可以看到变量的值和类型,把鼠标放在 _0x2fa7bd 上,会有一个浮窗提示
另外,还可以在右侧的 Watch 面板中添加想要查看的变量,这行代码的内容如下
, _0x263439 = Object(_0x2fa7bd['a'])(this['$store']['state']['url']['index']);
我们比较感兴趣的可能是 _0x2fa7bd 和 this 里面的 $store 属性。 展开 Watch 面板, 然后点击 + 号, 把想看到的变量添加到 Watch 面板中
可以发现 _0x2fa7bd 是一个对象, 它具有属性 a , 其值是一个方法。
this['$store']['state']['url']['index'] 的值其实就是 /api/movie , 即 Ajax 请求 URL 的 Path 。 _0x263439 就是调用前者的方法传入 /api/movie 得到的
下一步就是去寻找这个方法。我们可以把 Watch 面板的 _0x2fa7bd 展开,这里会显示 FunctionLocation , 就是这个函数代码的位置
点击进入,这时我们就进入一个新的名字为 _0x456254 的方法里,在这个方法里,应该就有 token 的生成逻辑了。添加断点,然后点击右上方的 Resume scrpt execution 按钮
这时我们会发现单步执行到
for (var _0x5da681 = Math['round'](new Date()['getTime']
位置,接下来我们不断进行单步调试,观察一下里面的执行逻辑和每一步调试的结果有什么变化,在每步的执行过程中,我们可以发现一些运行值被打到代码右侧并高亮表示,这里教程说的是在 Watch 下面会有每步的结果,不过我的并没有任何结果,但是在 Scope 下面倒是有一些数据的结果
Watch 下面并没有任何数据
最后,我们总结出这个 token 的构造逻辑
传入 /api/movie 会构造一个初始化列表,将变量命名为 _0x31a891
获取当前时间戳,命名为 _0x5da681,调用 push 方法将其添加到 _0x31a891 变量代表的列表中
将 _0x31a891 变量用 , 拼接,然后进行 SHAI 编码,命名为 _0xf7c3c7
将 _0xf7c3c7 (SHAI 编码结果) 和 _0x5da681(时间戳)用逗号拼接,命名为 _0x3c8435
将 _0x3c8435 进行 Base64 编码,命名为 _0x104b5b 得到最后的 token
经过反复观察,可以得出以上逻辑,其中变量可以实时查看,同时也可以自己输入到控制台验证
现在加密逻辑分析出来了,基本思路是
将 /api/movie 放到一个列表中
在列表中加入当前时间戳
将列表内容用逗号拼接
将拼接结果进行 SHAI 编码
将编码结果和时间戳再次拼接
将拼接后的结果进行 Base64 编码
使用 Python 实现列表页爬取
要用 Python 实现这个逻辑,我们需要借助两个库, 一个是 hashlib ,它提供了 sha1 方法,另一个是 base64 它提供了 b64encode 方法对结果进行 Base64 编码
import hashlib import time import base64 from typing import List, Any import requests INDEX_URL = 'https://spa6.scrape.center/api/movie/?limit={limit}&offset={offset}&token={token}' LIMIT = 10 OFFSET = 0 def get_token(args: List[Any]): timestamp = str(int(time.time())) args.append(timestamp) sign = hashlib.sha1(','.join(args).encode('utf-8')).hexdigest() return base64.b64encode(','.join([sign, timestamp]).encode('utf-8')).decode('utf-8') args = ['/api/movie'] token = get_token(args=args) index_url = INDEX_URL.format(limit=LIMIT, offset=OFFSET, token=token) response = requests.get(index_url) print('response', response.json())
部分输出结果
response {'count': 103, 'results': [{'id': 1, 'name': '霸王别姬', 'alias': 'Farewell My Concubine', 'cover': 'https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg@464w_644h_1e_1c', 'categories': ['剧情', '爱情'], 'published_at': '1993-07-26', 'minute': 171, 'score': 9.5, 'regions': ['中国内地', '中国香港']}, {'id': 2, 'name': '这个杀手不太冷', 'alias': 'Léon', 'cover': 'https://p0.meituan.net/movie/27b76fe6cf3903f3d74963f70786001e1438406.jpg@464w_644h_1e_1c', 'categories': ['动画', '歌舞', '冒险'], 'published_at': '1995-07-15', 'minute': 89, 'score': 9.0, 'regions': ['美国']}]}
寻找详情页 id 入口
我们观察前面的输出结果
这里我们看到,有个 id 是 1 , 另外还有一些其他字段, 如电影名称,封面,类别等,这里面一定有某个信息是用来唯一区分电影的
但是当我们点击第一部电影的 信息时,可以看到它跳转了 URL 为
https://spa6.scrape.center/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx
的页面,可以看到这里的 URL 里面有一个加密 id 为 ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx
它和电影信息有什么关系呢?
如果仔细观察,其实可以找出规律来,但是这总归是观察出来的,如果遇到一些观察不出来规律的,那就很麻烦了。因此还是要靠技巧去找到它真正的加密位置。这时候该怎么办?
分析一下,这个加密 id 怎么生成的。
点击详情页的时候,我们就可以看到它访问的 URL 里面就带上了 ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx 这个加密 id 了,。而且不同详情页的加密 id 是不同的,这说明这个加密 id 的构造依赖于列表页 Ajax 的返回结果。因此,可以确定这个加密 id 的生成发生在 Ajax 请求完成后或点击详情页的一瞬间, 为了进一步确定发生在何时,我们查看页面源码,可以看到在没点击前,详情页的 href 里面就已经带有加密 id 了
由此可以肯定,这个加密在 Ajax 请求完成之后生成的,而且肯定也是由 JS 生成的。
怎么去找 Ajax 完成之后的事件呢? 是否应该去找 Ajax 完成之后的事件呢?
可以试试。 在 Sources 面板的右侧, 有一个 Event Listener Breakpoints , 这里有一个 XHR 的监听,包括发起时,成功后,发生错误时的一些监听, 这里我们勾选上 readystatechange 事件,代表 Ajax 得到响应时的事件,其他断点都可以删除了,然后刷新一下页面
可以看到此时就停在 Ajax 得到响应时的位置了。我们怎么知道这个 id 是怎么加密的呢?可以通过断点一步步调试下去,但这个过程非常繁琐,因为这里可能会逐渐用到页面 UI 渲染的一些底层实现,甚至可能找着找着都不知道找到哪里去了
怎么办呢?这里我们就可以使用 Hook ,这个加密 id 是一个 base64 编码的字符串,那么生成过程中想必调用了 JS 的 Base64 编码的方法。这个方法叫做 btoa 。当然, Base64 也有其他实现方法,比如利用 crypto-js 库实现,可能底层调用的就不是 btoa 方法了
现在我们其实并不确定是不是通过调用 btoa 方法实现的 Base64 编码, 那就先试试
要实现 Hook , 关键在于将原来的方法改写, 这里我们其实就是 Hook btoa 这个方法了, btoa 这个方法属于 window 对选哪个,这里直接改写 window 对象的 btoa 方法即可
(function (){ 'use strict' function hook(object, attr){ var func = object[attr] object[attr] = function (){ console.log('hooked', object, attr, arguments) var ret = func.apply(object, arguments) debugger console.log('result', ret) return ret } } hook(window, 'btoa') })()
这里我们定义了一个 hook 方法,给其传入 object 和 attr 参数, 意思就是 Hook object 对象的 attr 参数,例如,如果我们想 Hook alert 方法,那就把 object 设置为 window , 把 attr 设置为 alert 。这里我们想要 Hook Base64 的编码方法, 所以只需要 Hook window 对象的 btoa 方法就可以了
hook 方法的第一句 var func = object[attr] , 相当于把它赋值为一个变量,我们调用 func 方法就可以实现和原来相同的功能。然后我们改写这个方法的定义,将其改成一个新方法。在新的方法中,通过 func.apply 方法有重新调用了原来的方法。这样我们可以保证前后方法的执行效果不受影响的前提下,在 func 方法执行的前后加入自己的代码,如使用 console.log 将信息输出到控制台,通过 debugger 进入断点等。在这个过程中,我们先临时保存 func 方法, 然后定义一个新方法来接管程序控制权,在其中自定义我们想要的实现,同时新方法重新调回 func 方法,保证前后结果不受影响。因此,我们达到了在不影响原有方法效果的前提下,可以实现在方法的前后实现自定义的功能,就是 Hook 的完整实现过程
最后,我们调用 hook 方法,传入 window 对象和 btoa 字符串即可
怎么注入这个代码呢?
控制台注入
重写 JS
Tampermonkey 注入
控制台注入
对于我们这个场景,控制台注入其实就够了,我们先来介绍这个方法,就是直接在控制台输入这行代码并运行即可
首先我们将页面恢复到最初的状态
然后打开控制台输入前面的那段代码 回车
执行完这段代码之后,就相当于我们已经把 window 的 btoa 方法改写了,然后在控制台输入
btoa('germey')
回车
可以看到它进入了我们自定义的 debugger 的位置停下来了,我们把断点向下执行,然后点击 Resume script execution 按钮, 就可以看到控制台输出了一些对应的结果
我们通过 Hook 的方式改写了 btoa 方法, 使其每次在调用的时候都能停到一个断点,同时还能输出对应的结果
接下来,怎么用 hook 找到对应的加密 id 入口?
由于此时我们是控制台直接输入的 Hook 代码,所以页面刷新就无效了。但我们这个网站是 SPA 页面,点击详情页的时候是不会刷新整个页面的,因此这段代码依然生效。如果不是 SPA 页面,即每次访问都需要刷新页面网站,那么这种注入方式就不生效了
我们想要 Hook 列表页 Ajax 加载完成后的逻辑, 对应的就是加密 id 的 Base64 编码过程, 怎样在不刷新页面的情况下,复现这个操作呢? 很简单,点击下一页就好了
这时候,点击第二页的按钮,可以看到它确实再次停到了 Hook 方法的 debugger 处。 由于列表页的 Ajax 和 加密 id 都带有 Base64 编码操作,所以都能 Hook 到。接着,官产对应的 Arguments 或当前网站的行为, 或者观察栈信息,我们就能大体知道现在走到哪个位置了, 从而进一步通过栈的调用信息找到调用 Base64 编码的位置
根据调用栈的信息,我们可以观察这些变量是在哪一层发生变化。比如对于最后一层,我们可以很明显看到它执行了 Base64 编码,编码前的结果是:
ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb1
编码后的结果
ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx
控制台输出的结果
那么核心问题来了,编码前的
ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb1
是怎么来的? 我们展开栈信息,一层层看这个字符串的变化情况,如果不变,就看下一层,如果改变了,就停下来细看。最后在第五层找到了它的变化过程
var _0x11a046 = 'ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb'
是一个写死的字符串,然后和 _0x177944 拼接形成了最后的字符串,那么 _0x177944 是怎么来的,继续向下看,
可以看到 Ajax 返回结果的单个电影信息的 id
因此,这个逻辑就清楚了,就是 固定字符串 ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb 加上电影 id ,然后进行 Base64 编码即可
在控制台注入的不好之处在于,页面刷新就失效了,而代码必须在页面加载完才能注入,所以并不能在一开始生效。
重写 JS
借助 Chrome 浏览器的 Overrides 功能,我们可以实现某些 JS 文件的重写和保存。 Overrides 会在本地生成一个 JS 文件副本, 以后每次刷新,都会使用副本内容
这里我们需要切换到 Sources 面板中的 Overrides 选项卡,然后选择一个文件夹,比如这里我自定义了一个 ChromeOverrides 文件夹
然后随便选择一个 JS 脚本,在后面贴上这段注入的脚本,保存文件,如果页面崩溃,刷新一下页面就好了
同时我们还注意到,目前直接进入到了断点模式,并且成功 Hook 到了 btoa 方法。
其实 Overrides 的功能很有用,有了它,我们可以持久化保存任意修改的 JS 代码,想在哪里修改都可以,甚至可以直接修改 JS 的执行逻辑
Tampermonkey 注入
如果不想使用 Overrides 的方式注入,我们也可以使用 Tampeermonkey 插件来注入
开始之前,先关闭所有断点和 刚才的 Overrides 功能,以防干扰
Tampermonkey 的安装和简单使用
写文章-CSDN创作中心
我们可以将脚本内容改写成下面这样
// ==UserScript==
// @name HookBase64
// @namespace https://scrape.center/
// @version 0.1
// @description Hook Base64 encode function
// @author Germey
// @match https://spa6.scrape.center/
// @grant none
// @run-at document-start
// ==/UserScript==(function() {
'use strict';
function hook(object, attr){
var func = object[attr]
console.log('func', func)
object[attr] = function(){
console.log('hooked', object, attr)
var ret = func.apply(object, arguments)
debugger
return ret
}
}
hook(window, 'btoa')
})()
这时候启动脚本,重新刷新页面,可以发现成功 Hook btoa 方法
寻找详情页 Ajax 的 token
现在我们已经找到详情页的加密 id 了,但是还差一步,其 Ajax 请求也有一个 token
因为也是 Ajax 请求,我们可以通过前面提到的同样的方法对该 token 的生成逻辑进行分析,最终可以发现其实这个 token 和详情页 token 的构造逻辑是一样的
使用 Python 实现详情页的爬取
现在,我们已经成功把详情页的加密 id 和 Ajax 请求的 token 找出来了,下一步就是使用 Python 完成爬取,这里我们只实现第一页的爬取
import hashlib import time import base64 from typing import List, Any import requests INDEX_URL = 'https://spa6.scrape.center/api/movie/?limit={limit}&offset={offset}&token={token}' DETAIL_URL = 'https://spa6.scrape.center/api/movie/{id}?token={token}' LIMIT = 10 OFFSET = 0 SECRET = 'ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb' def get_token(args: List[Any]): timestamp = str(int(time.time())) args.append(timestamp) sign = hashlib.sha1(','.join(args).encode('utf-8')).hexdigest() return base64.b64encode(','.join([sign, timestamp]).encode('utf-8')).decode('utf-8') args = ['/api/movie'] token = get_token(args=args) index_url = INDEX_URL.format(limit=LIMIT, offset=OFFSET, token=token) response = requests.get(index_url) print('response', response.json()) result = response.json() for item in result['results']: id = item['id'] encrypt_id = base64.b64encode((SECRET + str(id)).encode('utf-8')).decode('utf-8') args = [f'/api/movie/{encrypt_id}'] token = get_token(args=args) detail_url = DETAIL_URL.format(id=encrypt_id, token=token) response = requests.get(detail_url) print('response', response.json())