多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

问卷星逆向实战:参数复现与会话模拟两种路线全解析

问卷星逆向实战:参数复现与会话模拟两种路线全解析 “问卷星逆向”这个话题常年挂在自动化测试、数据采集、业务流程验证这几类需求下面。你可能是想把自己搭的问卷系统跟问卷星上的公开问卷做数据打通也可能是想给一套答题系统做接口自动化回归还可能是需要一个受控的数据采集程序去处理已授权的公开问卷数据。不管动机是哪一种撞上的实际问题都一样问卷星的请求不是把答案填进去就能提交里面藏着一串加密参数和完整的会话状态直接裸写 requests 过去大概率被拒。这篇文章就把这两种主流应对思路讲透参数复现与会话模拟。参数复现是硬核路线需要把前端 JS 里的签名逻辑抠出来重新实现会话模拟是稳妥路线把整个操作过程演得像真人在用浏览器。两个方案各有各的坑我把我踩过的细节、定位方法、抓包技巧一并写出来给后来的人省点时间。1. 问卷星逆向到底在逆什么先分清楚两件事很多刚接触的人会把“逆向”想得很玄其实就是两个问题一是请求里的加密参数怎么生成的二是服务端怎么判断“这是一个正常用户在访问”。前者靠的是对前端代码的分析和复现后者靠的是对会话状态与行为特征的模拟。先拿最简单的提交动作举例。你在问卷星上答完题点提交浏览器会发出一个 POST 请求。打开 DevTools 看这个请求的 body 不止有答案内容还带着时间戳、随机串、签名值甚至还有用户在页面上的操作痕迹。这些附加字段并不是平台闲得没事它们的作用是告诉服务端这次提交是页面自己发出来的不是有人在本地拼了一个报文乱发。所以“逆向”这件事做来做什么核心就一句话让构造出来的请求拥有和浏览器请求相同的合法性。1.1 两条路线的本质区别参数复现走的是“静态拆解”。你去看前端 JS 里某个加密函数是怎么写的把加密规则搬到本地代码里自己生成合法参数。这样做出来的请求可以不依赖浏览器环境直接用 Python、Node、Java 发速度和并发都很好控制。缺点也很明显前端代码一变你复现的那套逻辑就过时了排查起来相当费劲。会话模拟走的是“动态仿真”。你不去拆加密逻辑而是把浏览器或者浏览器内核跑起来用自动化工具去执行真实的页面操作提交动作由页面自己的 JS 完成所有参数自然就是合法的。这相当于请了一个“数字替身”帮你操作问卷服务端看到的几乎就是一条正常的用户请求链。简单类比一下参数复现像你研究了一家餐厅的招牌菜配方自己在家里复刻出来会话模拟像你雇了一个人去店里点同一道菜。前者需要你研究厨艺后者需要你找一个靠谱的“演员”。1.2 两条路线各自的适用面参数复现更适合高频、重复、需要快速拿到结果的场景比如你做接口压测、做问卷数据回填、做多个问卷模板间的数据同步。因为不需要拉起浏览器资源占用少可以并发跑很多个任务。会话模拟更适合那些对行为完整性要求高、页面里夹杂了不少动态逻辑的场景比如问卷里有时长检测、有随机题目排序、有点击轨迹采集这时候你单纯构造参数经常会漏掉一两个隐形字段。这里要提前说一句我不建议用这些技术去做任何骚扰、刷单、薅羊毛之类的事。文章里讨论的技术手段本质是 Web 自动化与前端安全研究领域的基础能力用在自己的测试项目、自己的问卷、或者已经取得授权的系统上才是合理的使用方式。2. 思路一参数复现——从 JS 代码里“抠”出签名逻辑很多人一听到“逆向”就觉得很难其实参数复现的路径是非常固定的抓包 → 定位 → 复现 → 验证。只要把这四步走完基本上一个能用的接口就出来了。难点在于后续维护因为平台只要换一次算法你就要重新走一遍。2.1 第一步抓包找到可疑参数先用浏览器正常打开一份问卷手动填一遍、提交一遍同时在 DevTools 的 Network 面板里盯着。你会在提交的那一条请求里看到类似这样的参数组paperid123456 answer{1:A,2:B} t1734567890123 rn2f8c9d1a7b... sign6f4b8a1e...这里t是时间戳rn是随机串sign是签名值。真正核心的是sign它一般是对某些参数做了哈希或者加密之后得到的。很多问卷类系统的第一版逻辑都不复杂常见的是把时间戳、随机串和一个写死在 JS 里的盐值拼起来做一次 MD5 或 SHA1。这里要提一句实际问卷星的算法肯定比我上面这个示例复杂但逆向的流程模板是不变的。你真正要做的就是把页面里生成sign的那段 JS 找出来。怎么定位在 DevTools 的 Network 面板里右键那条提交请求选择“Copy as cURL”然后再打开一个你熟悉的抓包工具或 IDE把 cURL 里的请求行、header、body 一条条拆开。我更推荐直接在 DevTools 的 Initiator 标签看调用栈它会显示是哪个 JS 文件、哪一行代码发起的请求。点进去基本就是加密逻辑所在的位置。2.2 第二步用 Hook 手段定位加密函数拿到 JS 文件以后别急着从头读到尾。前端开发一般会压缩混淆直接读很累。我常用的方法是搜索关键字。在 Sources 面板里按 CtrlShiftF 全局搜索关键词可以是md5、sha1、sign、encrypt、CryptoJS、t 这些。运气好你能直接定位到生成sign的函数运气不好整个字符串被压缩成Z(ne.x)这种叫人头大的写法这时候就要靠断点调试。Hooking 是一个特别实用的技巧。比如页面上调用了md5我可以在 Console 里提前注入一段覆盖代码const originalMd5 window.md5; window.md5 function (...args) { console.trace(); console.log(md5 called with:, args); return originalMd5.apply(this, args); };这样当你再次触发提交时控制台就会输出md5被调用的调用堆栈和参数。看到参数列表再对照请求 body 里实际传上来的值就能推算哪些内容参与了签名。如果页面用的是模块化封装的加密库window.md5访问不到也可以退一步在 DevTools 的 Event Listener Breakpoints 里给“提交”事件打端点观察提交前一刻的调用栈一样能顺藤摸瓜找到加密函数。2.3 第三步用 Python 完成参数复现假设我已经通过逆向确认了签名规则sign md5(rn t _salt_)。那么 Python 这边代码就简单了import time import hashlib import requests def md5(value: str) - str: return hashlib.md5(value.encode(utf-8)).hexdigest() def make_sign(t: str, rn: str) - str: return md5(f{rn}{t}_salt_) def fetch_rn_from_page(session, survey_url): resp session.get(survey_url) # 问卷页面加载时会返回一个 rn 字段写法因版本而异 # 这里用正则做个示例实际可能藏在 JSON 里 import re m re.search(rrn:([a-zA-Z0-9]), resp.text) return m.group(1) def submit_answer(survey_url: str, answers: dict): session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: survey_url, }) rn fetch_rn_from_page(session, survey_url) t str(int(time.time() * 1000)) sign make_sign(t, rn) payload { paperid: 123456, answer: answers, t: t, rn: rn, sign: sign, } resp session.post(https://survey-api.example.com/submit, jsonpayload) print(resp.status_code, resp.text)代码写出来不难真正的问题通常在细节上。比如时间戳用的是秒还是毫秒拼进哈希的字符串顺序是rn t还是t rn答案 JSON 的 key 是字符串还是数组。这就是为什么我强烈建议“先手动提交一次再照抄一遍参数”来对拍。2.4 参数复现的适用场景与隐性成本参数复现的开销集中在“逆向”阶段你花两三个小时剥洋葱一样把签名逻辑捋清楚之后就能无限次、高强度地调用。它适合自动化测试和需要大量并发的场景。但它的隐性成本是“版本敏感”。前端 JS 是会定期更新的今天能用的规则过两个星期可能就失效了。我的经验是每次代码跑不通先去对比页面 JS 文件的版本号或最近一次改动时间再走一遍 2.1 到 2.2 的定位流程往往会发现只是盐值换了或者签名算法升级了。3. 思路二会话模拟——把“活人”的操作节奏搬进程序会话模拟的核心词是“会话”。一次正常的问卷提交不是单独一个请求而是一串请求串起来的结果打开页面 → 拉取问卷配置 → 获取题目列表 → 提交答案 → 获取结果。这些请求之间通过 Cookie、Session、Token 维系关联。如果你只是把最后一个提交请求卷走服务端一看就会拒。会话模拟的思路是把这一整串动作按浏览器的路径重新走一遍让服务端看到一个完整的“用户会话”。3.1 一个完整的会话闭环长什么样以一份最简单的问卷为例访问问卷首页拿到页面整体配置其中包含一个标识当前答题会话的 ID。拉取题目内容通常是 JSON 格式包含题目类型、选项、是否必答、随机顺序等。用户在页面上作答前端会周期性保存草稿也会把这些交互事件传给服务端。点击提交发送答案列表、开始答题时间、结束答题时间等内容服务端返回成功。用代码模拟时前三步基本都可以通过requests.Session()完成。只要保持同一个 Session 实例Cookie 会被自动维护你只需要关心 headers 和请求顺序。这里容易被忽略的是“时间长度”。真人答一份问卷再快也会花几十秒。你程序里面 0.5 秒走完全流程在服务端风控眼里非常可疑。所以每次请求之间加上随机延时是一个低成本却很有用的反制措施。3.2 用 Playwright 模拟真实浏览器操作如果你面对的问卷页面里有大量 JS 动态渲染、有滑块验证、有随机题序纯 requests 模拟会非常痛苦。这时候直接上浏览器自动化更划算我常用 Playwright。from playwright.sync_api import sync_playwright def run_survey(survey_url: str, answers: dict): with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context browser.new_context( user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, localezh-CN, ) page context.new_page() page.goto(survey_url, wait_untilnetworkidle) # 模拟真人作答逐题点击、填写并随机等待 page.wait_for_timeout(5000) # 做题逻辑需要根据页面结构写这里只做示意 page.click(text选项A) page.wait_for_timeout(1500) page.click(button:has-text(提交)) page.wait_for_timeout(3000) browser.close()看到没Playwright 的好处就是你可以像一个真实用户一样操作定位器、点击、输入、等待。缺点是要拉起一个浏览器进程内存和 CPU 开销都更大不适合超高并发。3.3 会话模拟里的“指纹伪装”光有 Cookie 和延时还不够现在很多平台会看请求头指纹。你在 requests 里不设置 headers默认的python-requests一眼就能被识别。所以我一般会固定这些字段User-Agent一个真实浏览器的完整 UA Accepttext/html,application/xhtmlxml... Accept-Languagezh-CN,zh;q0.9 Referer当前问卷页面的 URL Origin问卷站点的域名顺序其实也重要但 Python 的 requests 不支持自动保持 header 顺序如果你遇到严格校验顺序的系统就要退回到 httpx 或直接在底层 socket 层面处理。不过我实测下来问卷星这类系统更多还是看 Cookie、签名和请求时序对 header 顺序没有那么敏感。除了请求头还有“行为指纹”。真人操作鼠标是会有微小抖动的滚动页面是阶段性的而自动化脚本经常是“瞬移鼠标、秒点按钮”。这就是为什么我建议在 Playwright 方案里加随机延时和人机交互动作而不是直接page.click()一口气点完。3.4 会话模拟的优缺点会话模拟最大的优点是维护成本低。平台改前端 JS你的脚本只需要适配页面 DOM 结构的变化不用重新分析一遍加密逻辑。行为更像真人误报率也低。缺点是慢而且对环境依赖大。你要是跑大批量任务得提前考虑并发部署的问题可能一台机器也就跑十几个浏览器实例。另外一旦页面弹出验证码你的自动化脚本就必须接打码平台或者人工介入这会增加额外成本。4. 两种思路的对比与选型到底该走哪条路这个表我基本每次做方案评审都会列出来方便快速判断维度参数复现会话模拟技术门槛高需要逆向 JS 能力中等需要自动化脚本能力抗页面变更能力弱签名算法一变就失效强DOM 变化好适配请求速度与并发快可并发极高慢受浏览器资源限制行为真实性一般容易被风控识别高行为更像人维护成本集中在分析新算法集中在适配页面结构典型场景接口自动化、批量数据回填端到端测试、复杂交互问卷我的选型经验比较直接如果是给自己内部系统做接口回归或者需要在短时间内处理大量数据选参数复现如果目标是答完一份结构复杂、带交互验证的问卷并且不希望频繁调整签名逻辑就选会话模拟。还有第三种思路是两者结合用参数复现生成合法提交负载但把生成过程放到浏览器环境里执行或者反过来用会话模拟浏览页面但最后的提交动作自己构造。这种方案能应对一些极端情况一般我最后才考虑因为它把两边的维护成本都摊上了。5. 实际操作中的常见问题与排查技巧这一节是我真正想写给后来人的。很多问题不是算法推导不出来而是细节没对齐。5.1 签名算出来了提交还是失败这是参数复现最常见的问题。我在 2.3 的坑里提过原因通常是时间戳单位不对前端用的是毫秒你用了秒拼签名字符串时顺序不对或者中间少了某个拼接片段提交的数据类型不对前端传的是 JSON 字符串你传了 Python dict漏掉了参与签名的另一个隐藏字段比如答案内容本身也参与了哈希。我的排查方法是先手动提交一次把提交请求的原始 body 完整保存下来再跑一遍自己的代码把两份 body 逐字符对比。用 diff 工具看哪里有差异九成的 bug 一眼就能看出来。5.2 会话被踢、接口返回 403如果你正常浏览器能提交但代码里提交就 403多数不是签名问题而是风控把这次请求判定为“异常会话”。重点查三件事请求头是否完整Referer 和 Origin 是否跟浏览器里一致两次请求间隔是否过短提交前有没有正常拉取过问卷内容IP 地址是不是已经太频繁地发请求触发了服务端限流。尤其是第三点我这几年深有体会。哪怕参数完全正确只要你在同一个 IP 上高频跑任务照样会被临时风控。合理的做法是控制频率或者准备多个出口 IP 分摊压力。5.3 问卷弹出了验证码/智能盾遇到验证码要么躲要么接。躲的方法是控制行为节奏每次会话开始前先访问一下问卷首页再停留几秒模拟真人阅读接的方法是接打码平台或人工过验证。如果用的是 Playwright遇到滑块验证码有些可以用拖拽轨迹模拟去碰运气但成功率不稳我一般还是预留人工处理通道。5.4 调试时最值得留意的几个细节我调试这类自动化脚本时有一个习惯全程保留日志。每发一个请求就把 URL、参数、响应状态和耗时打出来这样某个环节出问题能快速定位是前置请求失败还是提交逻辑出错。另外我在逆向 JS 时配合抓包工具做“回放”验证也就是先抓一个真实请求再放修正后的参数去重放这个请求会大幅缩短验证周期。还有一条很实用的小技巧在 DevTools 里给XMLHttpRequest原型做 Hook可以把所有请求和 payload 自动记下来省得一个一个翻 Network。(function () { const origOpen XMLHttpRequest.prototype.open; const origSend XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open function (method, url, ...rest) { console.log(XHR open:, method, url); return origOpen.call(this, method, url, ...rest); }; XMLHttpRequest.prototype.send function (body) { console.log(XHR send:, body); return origSend.call(this, body); }; })();这段代码在页面加载前注入就能看到所有 XHR 调用细节用来配合定位加密参数非常管用。6. 合规边界与个人建议技术本身是中性的但用法有边界。参数复现和会话模拟这两类技术我个人的使用场景都比较保守给自己内部的问卷系统做接口回归测试帮同事把一份 CSV 数据回填到自建的问卷模板里或者给一个已经取得授权的问卷做数据备份和统计。这些场景里逆向和自动化都是合理的技术工具。我不建议把这些技术用在别人未经授权的问卷上。不要去刷阅读量、不要去抢礼品型问卷、不要批量采集他人填写的隐私数据。这些都是会给自己惹麻烦的操作而且很容易让平台升级风控策略最终大家的自动化脚本都会失效。最后分享一点我做这些事情的真实体会刚开始做问卷星逆向的时候我也总想着一口气把所有加密逻辑全部逆向完美后来发现这样做既慢又没必要。正确的做法是先花二十分钟抓包看请求体找出真正影响提交成功的核心参数集用最小化复现把流程跑通再逐步补齐边边角角的字段。验证通过以后剩下时间都花在稳定性优化上比如随机延时、日志埋点、失败重试逻辑。真正常用的地方往往不是最复杂的部分而是最简单的那一跳。
返回列表