做爬虫开发的同学,通常都会遇到这样一个场景:同一个接口,在浏览器地址栏里访问没有问题,页面上的数据也能正常展示,可一旦换成 Python 的requests或httpx去请求,结果就变成了 403,或者返回一串看不懂的密文。很多人第一反应是“请求头没写全”,于是补 Cookie、加 User-Agent、带上 Referer,但问题依然存在。
出现这种情况,大概率不是请求头的问题,而是服务器在接口层校验了一组动态生成的签名参数。这组参数往往由网页里的 JavaScript 在请求前动态生成,你无法直接在网络请求中硬编码。于是,就需要把前端这段“黑盒逻辑”还原出来,让它也能在你的爬虫脚本里运行。这个分析过程,就是大家常说的 JS 逆向。
JS 逆向并不是一个神秘的黑客技术,它的本质是“前端代码分析和逻辑复现”。浏览器把 JavaScript 代码下载到本地运行,这些代码本身就是可读或半可读的,我们可以通过调试工具追踪某个请求参数是怎么生成的,再把同样的算法用 Node.js 或 Python 实现一遍。下面我会从概念、工具、核心知识到完整案例,一步步带着你走一遍闭环流程。
1. 什么是 JS 逆向?为什么爬虫开发要学它
1.1 从一个签名接口说起
我们先看一个非常典型的签名接口场景:
GET /api/list?key=user-001×tamp=1719000000000&sign=3fa92b...服务器并不是只靠key来识别请求,它还会校验timestamp和sign:
timestamp用来防止请求被重复使用,或者防止请求过于陈旧;sign是根据某些参数、密钥和时间戳共同计算出来的签名。
在浏览器页面里,这个接口可以正常返回,是因为浏览器先执行了页面里的 JavaScript,通过加密函数算出了正确的sign。而 Python 脚本直接请求时,我们不知道签名算法,所以只能得到一个失败响应。
于是,所谓 JS 逆向,就是回到浏览器中,找到生成sign的 JavaScript 函数,读懂它的算法,然后在自己的程序里复现相同结果。
1.2 JS 逆向的常见工作流程
一次完整的 JS 逆向分析,通常可以拆成四个步骤。
第一步,抓包确认请求参数。在浏览器开发者工具切到 Network 面板,找到目标 XHR/Fetch 请求,观察 URL Query、请求头、请求体中的关键参数,确定什么是动态参数,什么是静态参数。
第二步,定位加密函数。通过关键字搜索或断点调试,在 JavaScript 源码中找到生成动态参数的函数。
第三步,还原完整算法。分析函数用到的原始字符串、密钥、IV、盐值、编码方式,把流程整理成可独立运行的脚本。
第四步,验证与集成。把还原后的签名算法接到 Python 或其他爬虫框架里,校验输出结果是否与浏览器一致。
虽然真实网站里还有压缩、混淆、动态执行、环境检测等更复杂的情况,但底层思路都一样。只要养成“先定位、再分析、后复现”的习惯,很多问题都能迎刃而解。
1.3 合法边界与适用范围
这里必须补充一个重要前提:JS 逆向和技术学习都应该在合法授权范围内进行。
建议的使用场景包括:
- 分析自己公司或自己开发的网站接口;
- 在获得目标站点授权的前提下,做自动化测试或数据采集;
- 本地自建测试服务,用于学习浏览器调试和前端加密原理;
- 阅读前端开源项目,理解加密方案的代码结构。
不建议对未授权的站点做绕过访问控制、批量抓取敏感信息、破解登录验证等操作。这类行为可能违反网站服务条款,严重的还会触犯法律。本篇文章的所有示例,都建议在本地模拟环境中操作。
2. 环境准备与工具选择
2.1 软件环境
本文的实战会同时使用浏览器、Node.js 和 Python,因此需要提前准备一套基础环境。
| 工具 | 作用 | 版本建议 |
|---|---|---|
| Chrome 或 Edge | 开发者工具、断点调试 | 更新到较新稳定版即可 |
| Node.js | 运行后端演示服务、还原签名算法 | 建议 Node.js 16 及以上 |
| Python 3 | 编写请求验证脚本 | 建议 Python 3.9 及以上 |
| 代码编辑器 | 写项目代码 | VS Code 或 PyCharm 均可 |
版本不一定完全一致,只要满足示例中的功能即可。如果本机没有 Node.js,可以去官网下载 LTS 版本,安装完成后在命令行执行node -v,能看到版本号就说明环境正常。
2.2 浏览器开发者工具
Chrome 和 Edge 的开发者工具是整个 JS 逆向分析的“主战场”。
常用的面板有:
- Elements:查看页面结构,很少直接用在 JS 逆向中;
- Console:测试表达式、查看输出、手动调用函数;
- Sources:查看 JavaScript 源码、打断点、跟踪调用栈;
- Network:查看接口请求、响应、Cookie、Header;
- Application:查看 localStorage、sessionStorage、Cookie。
真实定位加密逻辑时,最常用的组合是 Network + Sources。Network 负责告诉我们“哪里生成了这个参数”,Sources 负责让我们停留在加密函数内部,观察参数是怎么变换的。
2.3 抓包工具选择
普通 Web 页面请求通常不需要额外抓包工具,浏览器 Network 面板已经足够。但如果是 App 内嵌 WebView、小程序或客户端请求,就需要用到 Fiddler、Charles、Whistle 等中间人代理工具。
这类工具的核心原理是:把手机或客户端的网络流量代理到电脑上,通过安装证书解密 HTTPS 请求,从而看到真实接口。安装证书、设置代理属于常规调试手段,但要注意只在自己设备或已授权环境使用,不能非法窃听他人通信。
如果是初学者,建议先从浏览器开始,不用一上来就折腾抓包证书。
3. 核心知识拆解:加密、定位与调试
3.1 先分清“加密”的类型
JS 逆向中常见的算法可以分成三类:摘要算法、对称加密、非对称加密。很多新手一看到sign就想到 MD5,这是可以理解的,但不同类型的参数判断方式并不一样。
第一类是摘要算法,常见的有 MD5、SHA-1、SHA-256。摘要算法是单向的,只能从原文计算摘要,不能从摘要反推原文。它的特点通常是输出固定长度字符串,比如 32 位的十六进制字符串很可能是 MD5,64 位的可能是 SHA-256。摘要通常用来做签名或内容校验。
第二类是对称加密,常见的是 AES、DES。对称加密的特点是加密和解密使用同一个密钥,所以代码还原时要特别关注密钥、偏移量 IV、分组模式和填充方式。如果你看到一个密文长度规律、密钥是由固定字符串生成,大概率就是 AES。
第三类是非对称加密,常见的是 RSA。加密使用公钥,解密使用私钥。在 Web 端最常见的使用方式是:服务器把 RSA 公钥下发给前端,前端用公钥加密密码,后端再用私钥解密。即使攻击者拿到公钥,也无法直接还原原文。
除此之外,还要注意 Base64 并不是加密算法,它只是编码。很多网站会先做一次 Base64,再拼接参数做摘要。
3.2 快速定位加密入口
拿到一个接口后,想快速找到加密函数,可以优先使用关键字搜索法。
在浏览器开发者工具里按 Ctrl+Shift+F 可以全局搜索当前页面加载的所有脚本内容。建议先搜索以下关键字:
| 搜索类别 | 建议关键字 |
|---|---|
| 参数名 | sign、signature、token、params、encrypt、key |
| 算法特征 | md5、sha256、AES、RSA、CryptoJS、crypto |
| 组合规律 | timestamp、nonce、appId、clientId、secret |
| 请求方式 | XMLHttpRequest、fetch、axios、ajax |
比如一个请求参数叫sign,我们就可以搜索sign =、sign:、"sign"、setSign等写法。压缩后的 JavaScript 代码虽然变量名很短,但字符串和函数名通常还会保留,因此搜索字符串是最高效的定位方式。
如果搜索出来的结果很多,可以再结合 Network 面板的 Initiator 查看是哪个 JS 文件发起的请求。
3.3 断点调试与调用栈分析
找到源码位置后,下一步是打断点看执行过程。
这里介绍三种最常用的断点方式。
第一种是普通代码断点。在 Sources 面板中打开 JS 文件,点击行号即可设置断点。刷新页面或触发按钮后,代码会停在断点处,我们就可以在右侧 Scope 面板查看当前变量值。
第二种是 XHR/fetch 断点。在 Sources 面板右侧找到 XHR/fetch breakpoints,点击加号添加包含关键字的 URL,比如/api/list。当页面发起匹配请求时,浏览器会主动停在发请求之前的代码位置,这样可以非常快地找到网络请求的地点。
第三种是事件监听断点。比如点击按钮后才触发请求,可以在 Sources 面板的事件监听器断点中勾选 click 事件,从而在点击回调内部断下。
分析过程中要重点观察 Call Stack 调用栈。调用栈能告诉我们当前函数是被谁调用的,一层层往上跳,就能找到参数最早从哪里进入。
3.4 从“看到了”到“能复现”
很多人卡在最后一步:函数逻辑看到了,但不知道怎么写成 Python 或独立 Node 脚本。
这里有一个很实用的思路:不要急着用 Python 重写,先用 Node.js 把加密逻辑 1:1 复现。因为网站前端是 JavaScript,很多库在 Node 中可以直接使用,复现成本最低。等 Node 脚本能跑通后,再根据算法类型决定是否改成 Python。
比如网站上用了CryptoJS.SHA256(...),那么在 Node 项目中可以安装crypto-js,调用方式几乎一样。如果网站用了加密库特定封装,也要尽量保持字符编码和参数拼接顺序一致,例如到底是key + timestamp还是timestamp + key,这种顺序问题最容易出错。
复现后,用同一组入参分别运行浏览器和 Node 脚本,对比输出是否一致。如果一致,就说明算法分析正确。
4. 实战案例:本地签名接口的完整逆向流程
下面我们用一套本地环境,演示从搭建目标服务、浏览器观察请求,到 Node 还原签名、Python 请求验证的完整流程。
4.1 准备一个带签名校验的本地服务
先创建一个项目目录,例如js-reverse-demo,项目结构如下:
js-reverse-demo/ ├── backend/ │ └── server.js ├── public/ │ ├── index.html │ └── app.js └── crawler/ ├── sign.js └── request.py在backend目录初始化 npm 项目并安装 Express:
cd js-reverse-demo npm init -y npm install express然后编写backend/server.js。这个后端服务会校验三个参数:key、timestamp、sign,其中sign的规则是:
SHA256(key | timestamp | 固定密钥) 的十六进制结果,取前 16 位完整代码如下:
// 文件路径:backend/server.js const express = require('express'); const crypto = require('crypto'); const path = require('path'); const app = express(); const PORT = 3000; const SECRET_KEY = 'csdn-demo-secret-2025'; function createSign(key, timestamp) { const raw = `${key}|${timestamp}|${SECRET_KEY}`; return crypto .createHash('sha256') .update(raw) .digest('hex') .substring(0, 16); } function checkSign(req, res, next) { const key = req.query.key || ''; const timestamp = req.query.timestamp || ''; const sign = req.query.sign || ''; if (!key || !timestamp || !sign) { res.status(403).json({ code: 403, message: '缺少签名参数' }); return; } const expectSign = createSign(key, timestamp); const now = Date.now(); if (Math.abs(now - Number(timestamp)) > 5 * 60 * 1000) { res.status(403).json({ code: 403, message: '签名过期' }); return; } if (sign !== expectSign) { res.status(403).json({ code: 403, message: '签名校验失败' }); return; } next(); } app.get('/api/list', checkSign, (req, res) => { res.json({ code: 0, message: 'success', data: [ { id: 1, title: 'JS逆向示例-01', author: 'CSDN' }, { id: 2, title: 'JS逆向示例-02', author: 'CSDN' } ] }); }); app.use(express.static(path.join(__dirname, '../public'))); app.listen(PORT, () => { console.log(`demo server run at http://localhost:${PORT}`); });这个服务演示的是签名参数校验。SECRET_KEY相当于服务端与前端约定的“盐值”,实际项目中会放在服务端配置里,而不会像演示代码这样直接出现在前端。
启动服务:
node backend/server.js浏览器访问http://localhost:3000,如果直接请求/api/list是不带签名的,会返回 403。
4.2 前端页面中的加密逻辑
为了让页面正常调用接口,我们需要在public/index.html中加载app.js,并在app.js中动态生成签名。
public/index.html代码如下:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>签名接口演示</title> </head> <body> <h2>签名接口演示</h2> <pre id="result">正在加载...</pre> <script src="/app.js"></script> </body> </html>public/app.js代码如下:
// 文件路径:public/app.js async function createSign(key, timestamp) { // 这里模拟线上前端代码中的加密函数 const raw = `${key}|${timestamp}|csdn-demo-secret-2025`; const buffer = await crypto.subtle.digest( 'SHA-256', new TextEncoder().encode(raw) ); const hash = Array.from(new Uint8Array(buffer)) .map((b) => b.toString(16).padStart(2, '0')) .join(''); return hash.substring(0, 16); } async function loadData() { const key = 'demo-user'; const timestamp = String(Date.now()); const sign = await createSign(key, timestamp); const api = `/api/list?key=${key}×tamp=${timestamp}&sign=${sign}`; const response = await fetch(api); const result = await response.json(); document.getElementById('result').innerText = JSON.stringify(result, null, 2); } loadData();刷新页面后,页面会正常显示接口返回的数据。此时,我们就可以把自己当成一个“只拿到了网页源码,但不知道签名算法”的开发者,开始逆向分析。
4.3 从浏览器里定位签名算法
打开 Chrome 开发者工具,切到 Network 面板,刷新页面,可以看到一个/api/list请求。
点击这个请求,在 Payload 或 Headers 面板中能看到:
key=demo-user timestamp=1719000000000 sign=2fa5b19c1c1e2a3d其中timestamp是不断变化的,sign也随着 timestamp 变化。这里的key是固定的,所以重点怀疑对象就是sign。
然后回到 Sources 面板,在左侧文件列表中找到app.js,按 Ctrl+F 搜索sign。很快能看到const sign = await createSign(key, timestamp)这一行。
这里就是关键入口:原来签名由createSign(key, timestamp)函数生成。继续点进这个函数,就能看到它把key、timestamp和固定字符串拼起来,然后使用 Web Crypto API 做 SHA-256 摘要,取前 16 位。
如果遇到的是压缩混淆代码,搜索sign会看到一大段压缩后的 JS,分析起来会更困难。这时候可以给生成签名的那一行打上断点,刷新页面,当代码暂停时,在 Scope 面板中查看参数和函数返回值,也能快速确认算法细节。
4.4 使用 Node.js 还原签名
在云端分析前端逻辑后,我们可以把它翻译成 Node.js 脚本。这里注意:浏览器端使用的是 Web Crypto API,Node.js 端使用内置的crypto模块即可,两者结果一致。
创建crawler/sign.js:
// 文件路径:crawler/sign.js const crypto = require('crypto'); const SECRET_KEY = 'csdn-demo-secret-2025'; function createSign(key, timestamp) { const raw = `${key}|${timestamp}|${SECRET_KEY}`; return crypto .createHash('sha256') .update(raw) .digest('hex') .substring(0, 16); } const key = process.argv[2]; const timestamp = process.argv[3]; if (!key || !timestamp) { console.error('用法: node sign.js <key> <timestamp>'); process.exit(1); } console.log(createSign(key, timestamp));执行下面的命令测试:
node crawler/sign.js demo-user 1719000000000输出结果是一串 16 位十六进制字符串。把这个签名与浏览器 Network 面板里的对比,只要 timestamp 相同,签名就应该完全一致。
4.5 编写 Python 请求脚本验证
最后,我们用 Python 脚本请求本地接口。为了不依赖第三方库,这里使用标准库urllib,通过subprocess调用 Node 脚本生成签名。
创建crawler/request.py:
# 文件路径:crawler/request.py import json import subprocess import time import urllib.parse from urllib.request import Request, urlopen API_URL = "http://localhost:3000/api/list" def get_sign(key: str, timestamp: int) -> str: result = subprocess.run( ["node", "sign.js", key, str(timestamp)], capture_output=True, text=True, check=True, ) return result.stdout.strip() def main(): key = "demo-user" timestamp = int(time.time() * 1000) sign = get_sign(key, timestamp) params = urllib.parse.urlencode({ "key": key, "timestamp": timestamp, "sign": sign, }) url = f"{API_URL}?{params}" req = Request(url, headers={"User-Agent": "Mozilla/5.0"}) with urlopen(req, timeout=5) as resp: data = json.load(resp) print(json.dumps(data, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()在crawler目录下运行:
cd crawler python request.py正常输出如下:
{ "code": 0, "message": "success", "data": [ { "id": 1, "title": "JS逆向示例-01", "author": "CSDN" }, { "id": 2, "title": "JS逆向示例-02", "author": "CSDN" } ] }到这里,一个最简单的签名参数逆向闭环就完成了。从浏览器定位到参数生成函数,再用 Node.js 还原算法,最后集成到 Python 请求中拿到数据。
5. 常见问题与排查思路
JS 逆向学习和实战过程中,大家经常会在某个步骤卡住。下面整理了一些高频问题。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 找不到生成 sign 的 JS 文件 | 源码被压缩合并,或搜索词不对 | 用 Initiator 查看调用栈,换更多关键字搜索 |
| 在接口请求中看不到加密参数 | 参数被放在 Cookie 或 Header 中 | 打开 Network 请求详情,查看所有请求头 |
| Python 请求得到签名过期 | timestamp 是字符串拼接,类型或时区不对 | 用毫秒时间戳,确保与浏览器一致 |
| 浏览器加密结果和 Node 不一致 | 参数拼接顺序不同,或文本编码不同 | 用相同入参逐字比对拼接前的原始字符串 |
| 断点一打就断,但看不到函数 | 断点设在压缩代码错误行 | 在右侧 Call Stack 中向上层函数跳转 |
| Node 中出现 window 未定义 | 前端代码依赖浏览器环境 | 用 jsdom 模拟 window 对象,或把纯算法抽离 |
| Python 脚本运行只显示 Process finished with exit code 0 | 没有调用 main 入口或代码没有打印 | 检查是否写了if __name__ == "__main__",并确保主流程被调用 |
其中“Python 脚本只显示 exit code 0”是一个很典型的新手问题。很多时候代码里只是定义了函数,却没有在底部调用,或者使用了异步请求但没有等待任务完成。最简单的排查方法是在代码入口加一行打印,确认程序是否真的执行到了目标位置。
另外,签名参数分析出来后,运行结果仍然不一致,需要重点检查字符编码。JavaScript 默认使用 UTF-16 的部分场景可能导致 String 长度与 Python 不一致,尤其是包含中文、特殊符号时。建议在调试阶段,把原始拼接字符串打印出来,再用 Python 完全复制一遍,逐字符核对。
6. 工程化与最佳实践
分析出签名算法只是开始,真正把它用于实际项目时,还需要考虑几个工程问题。
6.1 稳定性与可维护性
签名算法经常更新,如果只是把算法硬编码在爬虫脚本里,一升级就得重新分析。更建议的做法是:
- 把签名生成逻辑单独封装成一个模块或独立服务;
- 只暴露输入参数和输出结果,不与其他业务代码耦合;
- 在签名算法变更时,通过日志和报警及时发现问题;
- 使用版本管理工具保存每次还原后的签名模块,方便回滚。
例如,你可以把 Node 签名脚本包装成一个 HTTP 微服务,Python 爬虫请求这个微服务获取签名,签名逻辑更新时不用重新发布爬虫。
6.2 请求频率与资源控制
即使接口合法可访问,大量高频请求也会给目标服务造成压力。实际项目中应控制并发数,建议增加延迟、重试退避机制和请求频率限制。
一个比较稳妥的爬虫策略是:先用小批量测试接口稳定性,再按固定频率逐步增加请求量;遇到 429、403 等响应时,立即降低频率,而不是无限重试。日志中要记录响应状态码、耗时和失败原因,方便事后分析。
如果你的爬虫部署在多台服务器上,还要考虑分布式限流。多节点同时高频请求,很容易触发服务端风控,导致 IP 被封禁。加代理是风险较高的操作,不建议在未授权环境中贸然使用。
6.3 合规与防守视角
前端加密无法做到绝对安全,服务器永远不应该把核心密钥放在浏览器端代码里。如果只是用前端 JS 做“防君子不防小人”的签名,那么整个安全方案还需要结合服务端权限校验、频率限制、账号体系、设备指纹等共同完成。
对 JS 逆向学习者来说,最好的心态是:把它理解成“前端代码可观测性分析与调试技巧”,而不是“绕过安全限制的攻击手段”。所有练习都应该在本地环境、自己开发的系统或获得授权的项目中完成。
6.4 更进一步的学习方向
如果想继续深入,有两个方向值得投入。
第一个方向是混淆对抗。真实网站会使用 JavaScript 混淆工具把函数名、变量名替换为无意义字符,甚至加入控制流平坦化、字符串加密、虚拟机保护等方案。想还原这种代码,需要学习 AST 抽象语法树,可以借助 Babel 或在线 AST 工具分析代码结构。
第二个方向是浏览器环境补充。很多前端签名算法会读取window、navigator、document等浏览器环境变量,一旦脱离浏览器运行就会报错。这种情况下,要么使用 Puppeteer/Playwright 这类无头浏览器来模拟真实浏览器,要么在 Node 里自己补齐环境对象。
这两个方向都属于 JS 逆向的高阶部分,建议先掌握本篇文章的基础定位与还原思路,再逐步深入。
7. 总结与后续学习路线
本篇文章从一个签名接口场景出发,解释了 JS 逆向的核心含义,带你走完了“抓包观察、源码定位、断点调试、Node 还原、Python 验证”的完整流程。如果你能独立跑通本地示例,说明你已经掌握了 JS 逆向分析最基本的方法论。
接下来的学习路线可以按这个顺序推进:
- 第一周:熟悉 Chrome DevTools 的 Network、Sources、Console,练习分析公开网页的简单请求参数;
- 第二周:掌握 MD5、SHA、Base64、AES、RSA 算法的识别与 Node.js 复现;
- 第三周:学习 JavaScript 语法进阶,包括闭包、原型链、异步流程、事件循环;
- 第四周:学习 AST 与 JavaScript 混淆还原,尝试理解压缩代码和反调试逻辑。
每学一个新知识点,都建议自己搭建一个带加密逻辑的本地页面来做实验。你可以改造本文的 Express 服务,加入 AES 加密、动态混淆、环境校验等条件,然后模拟逆向过程。这样练习得越多,后续遇到真实项目时越有把握。
JS 逆向本质上是“基于浏览器可观测性做逻辑分析”的能力。真正的价值不在于突破某个网站的防护,而在于当你面对一段陌生的、混淆的、压缩的前端代码时,能够快速理解它做了什么,并且有能力用工程化方式复现和沉淀。希望这篇文章能帮你把这个基本功打牢。如果有帮助,也欢迎收藏备用,避免在后面的学习和项目中再走弯路。