news 2026/10/10 5:24:12

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
问卷星逆向实战:参数复现与会话模拟两种路线全解析

“问卷星逆向”这个话题,常年挂在自动化测试、数据采集、业务流程验证这几类需求下面。你可能是想把自己搭的问卷系统跟问卷星上的公开问卷做数据打通,也可能是想给一套答题系统做接口自动化回归,还可能是需要一个受控的数据采集程序去处理已授权的公开问卷数据。不管动机是哪一种,撞上的实际问题都一样:问卷星的请求不是把答案填进去就能提交,里面藏着一串加密参数和完整的会话状态,直接裸写 requests 过去,大概率被拒。

这篇文章就把这两种主流应对思路讲透:参数复现与会话模拟。参数复现是硬核路线,需要把前端 JS 里的签名逻辑抠出来重新实现;会话模拟是稳妥路线,把整个操作过程演得像真人在用浏览器。两个方案各有各的坑,我把我踩过的细节、定位方法、抓包技巧一并写出来,给后来的人省点时间。

1. 问卷星逆向到底在逆什么:先分清楚两件事

很多刚接触的人会把“逆向”想得很玄,其实就是两个问题:一是请求里的加密参数怎么生成的,二是服务端怎么判断“这是一个正常用户在访问”。前者靠的是对前端代码的分析和复现,后者靠的是对会话状态与行为特征的模拟。

先拿最简单的提交动作举例。你在问卷星上答完题点提交,浏览器会发出一个 POST 请求。打开 DevTools 看,这个请求的 body 不止有答案内容,还带着时间戳、随机串、签名值,甚至还有用户在页面上的操作痕迹。这些附加字段并不是平台闲得没事,它们的作用是告诉服务端:这次提交是页面自己发出来的,不是有人在本地拼了一个报文乱发。

所以“逆向”这件事做来做什么,核心就一句话:让构造出来的请求拥有和浏览器请求相同的合法性。

1.1 两条路线的本质区别

参数复现走的是“静态拆解”。你去看前端 JS 里某个加密函数是怎么写的,把加密规则搬到本地代码里,自己生成合法参数。这样做出来的请求可以不依赖浏览器环境,直接用 Python、Node、Java 发,速度和并发都很好控制。缺点也很明显:前端代码一变,你复现的那套逻辑就过时了,排查起来相当费劲。

会话模拟走的是“动态仿真”。你不去拆加密逻辑,而是把浏览器或者浏览器内核跑起来,用自动化工具去执行真实的页面操作,提交动作由页面自己的 JS 完成,所有参数自然就是合法的。这相当于请了一个“数字替身”帮你操作问卷,服务端看到的几乎就是一条正常的用户请求链。

简单类比一下:参数复现像你研究了一家餐厅的招牌菜配方,自己在家里复刻出来;会话模拟像你雇了一个人去店里点同一道菜。前者需要你研究厨艺,后者需要你找一个靠谱的“演员”。

1.2 两条路线各自的适用面

参数复现更适合高频、重复、需要快速拿到结果的场景,比如你做接口压测、做问卷数据回填、做多个问卷模板间的数据同步。因为不需要拉起浏览器,资源占用少,可以并发跑很多个任务。

会话模拟更适合那些对行为完整性要求高、页面里夹杂了不少动态逻辑的场景,比如问卷里有时长检测、有随机题目排序、有点击轨迹采集,这时候你单纯构造参数,经常会漏掉一两个隐形字段。

这里要提前说一句:我不建议用这些技术去做任何骚扰、刷单、薅羊毛之类的事。文章里讨论的技术手段,本质是 Web 自动化与前端安全研究领域的基础能力,用在自己的测试项目、自己的问卷、或者已经取得授权的系统上,才是合理的使用方式。

2. 思路一:参数复现——从 JS 代码里“抠”出签名逻辑

很多人一听到“逆向”就觉得很难,其实参数复现的路径是非常固定的:抓包 → 定位 → 复现 → 验证。只要把这四步走完,基本上一个能用的接口就出来了。难点在于后续维护,因为平台只要换一次算法,你就要重新走一遍。

2.1 第一步:抓包找到可疑参数

先用浏览器正常打开一份问卷,手动填一遍、提交一遍,同时在 DevTools 的 Network 面板里盯着。

你会在提交的那一条请求里看到类似这样的参数组:

paperid=123456 answer={"1":"A","2":"B"} t=1734567890123 rn=2f8c9d1a7b... sign=6f4b8a1e...

这里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 面板里按 Ctrl+Shift+F 全局搜索,关键词可以是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(r'"rn":"([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", json=payload) 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 一个完整的会话闭环长什么样

以一份最简单的问卷为例:

  1. 访问问卷首页,拿到页面整体配置,其中包含一个标识当前答题会话的 ID。
  2. 拉取题目内容,通常是 JSON 格式,包含题目类型、选项、是否必答、随机顺序等。
  3. 用户在页面上作答,前端会周期性保存草稿,也会把这些交互事件传给服务端。
  4. 点击提交,发送答案列表、开始答题时间、结束答题时间等内容,服务端返回成功。

用代码模拟时,前三步基本都可以通过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(headless=False) context = browser.new_context( user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36", locale="zh-CN", ) page = context.new_page() page.goto(survey_url, wait_until="networkidle") # 模拟真人作答:逐题点击、填写,并随机等待 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 Accept:text/html,application/xhtml+xml... Accept-Language:zh-CN,zh;q=0.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,多数不是签名问题,而是风控把这次请求判定为“异常会话”。重点查三件事:

  1. 请求头是否完整,Referer 和 Origin 是否跟浏览器里一致;
  2. 两次请求间隔是否过短,提交前有没有正常拉取过问卷内容;
  3. 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 数据回填到自建的问卷模板里,或者给一个已经取得授权的问卷做数据备份和统计。这些场景里,逆向和自动化都是合理的技术工具。

我不建议把这些技术用在别人未经授权的问卷上。不要去刷阅读量、不要去抢礼品型问卷、不要批量采集他人填写的隐私数据。这些都是会给自己惹麻烦的操作,而且很容易让平台升级风控策略,最终大家的自动化脚本都会失效。

最后分享一点我做这些事情的真实体会:刚开始做问卷星逆向的时候,我也总想着一口气把所有加密逻辑全部逆向完美,后来发现这样做既慢又没必要。正确的做法是先花二十分钟抓包看请求体,找出真正影响提交成功的核心参数集,用最小化复现把流程跑通,再逐步补齐边边角角的字段。验证通过以后,剩下时间都花在稳定性优化上,比如随机延时、日志埋点、失败重试逻辑。真正常用的地方,往往不是最复杂的部分,而是最简单的那一跳。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 5:23:52

老游戏低配优化指南:CPU单核与显存管理实战

1. 为什么十几年后还有人折腾这款老游戏每次看到有人问“这游戏都这么多年了,还有必要优化吗”,我都想回一句:你去试试在现在的机器上直接跑原版,看看那个帧数曲线有多酸爽。这款游戏当年是出了名的吃CPU,双核时代它能…

作者头像 李华
网站建设 2026/10/10 5:23:24

MyBatis-Plus selectByMap详解:原理、实战与避坑指南

先说结论:selectByMap就是 MyBatis-Plus 提供的一个“用 Map 当查询条件”的方法。很多刚接触的人一看名字就懵——又是Mapper又是Map的,这俩到底啥关系?其实翻译成大白话就是:你给这个方法一个 Map,它把 Map 的 key 当…

作者头像 李华
网站建设 2026/10/10 5:22:17

如何用feiyangdigital-bot实现智能防刷屏:深入解析AntiFlood机制

如何用feiyangdigital-bot实现智能防刷屏:深入解析AntiFlood机制 在Telegram群组管理中,智能防刷屏是维护良好交流环境的关键功能。feiyangdigital-bot作为一款基于SpringBoot和Telegrambot-Api的多功能群管机器人,其内置的AntiFlood机制为群…

作者头像 李华