
干过问卷类项目的人应该都清楚这类平台真正的技术点不在页面长什么样而在于你拿到的那一堆参数是怎么来的。尤其是问卷星这类成熟产品每次提交背后都有一整套验签、时效和会话状态在兜底你以为只是填个表点个提交实际上浏览器替你做了大量隐形工作。当你需要把这些隐形工作复现出来时通常会走到两条路上参数复现和会话模拟。这两条路各管一段各有优劣也各自藏着不少让人挠头的坑。这篇就围绕这两条思路展开聊聊我在实际测试和调试里摸出来的完整链路、选型依据和翻车教训。1. 问卷提交链路的基本构成先知道题在哪在谈逆向思路之前得先把问题域画清楚。问卷星的问卷提交不是一次简单的 HTTP POST而是由好几层机制叠起来的。如果你连这些机制在哪一环发力都不清楚后面无论做参数复现还是会话模拟都会抓瞎。1.1 从打开页面到点击提交发生了什么一次完整的问卷访问大概可以拆成四个阶段。第一阶段是加载静态资源浏览器拿到 HTML、JavaScript 和样式文件这时候页面上的题目结构、校验规则和部分答题逻辑就已经解析出来了。第二阶段是初始化答题状态页面脚本会向服务端发起一个获取问卷信息的异步请求把问卷模板 ID、版本号、有效期这些信息拉下来同时创建一份属于当前会话的答题上下文。第三阶段是用户交互也就是真正逐题选择、填写或拖拽这期间脚本会在本地维护一份答案快照顺便启动计时、校验必填项。第四阶段才是最终提交页面把答案序列化后带上各类签名参数、时间戳、会话凭证一起 POST 出去。这里容易忽略的是第四阶段的提交往往不是只有一次请求。有的问卷在提交成功之后还会追加一次完成回执请求用于更新统计状态或触发后续动作。如果只盯着最显眼的那条 POST很可能漏掉真正的收尾逻辑。另外一个关键点是这些请求大多不是简单的表单编码格式。问卷星这类系统通常会把答案数据拆成多字段拼接再包一层自定义的编码规则某些新鲜的题型或逻辑跳转还会在运行时动态生成字段名名字本身就没有固定规律只能靠抓包逐一确认。1.2 两套逆向思路的本质区别所谓参数复现指的是不去管浏览器内部的状态管理而是把所有请求所需的参数都当成待还原的输入一步步推导出它们的生成规则然后在一套全新的请求环境中把它们重新造出来。这种做法有点像重新拼装一台机器你不需要保留机器原有的运行痕迹只要把每个齿轮的规格摸清楚就能用新零件装出一台行为一致的机器。会话模拟则完全是另一种思路。它默认你没必要把每个参数的生成规则都研究透只要能让服务端认为当前请求方就是刚才那个浏览器就够了。换句话说参数复现解决的是请求内容对不对的问题会话模拟解决的是请求身份认不认的问题。这两种思路并非互斥。大多数稳定方案其实都是混合使用主体靠参数复现保证请求内容合法再靠会话模拟维持身份连续性。真正选型的时候取决于你对目标系统的理解深度以及你手上掌握了多少可用的上下文信息。2. 思路一参数复现——把请求重新拼一遍参数复现是很多人第一个想到的做法因为它最直接抓包、看参数、找规律、写代码。但真正上手之后你会发现难的不是看参数而是还原参数的生成过程。2.1 参数复现的核心搞清每个字段的生成规则还是把提交请求抓下来拆开分析每一层内容。我习惯把一个完整 POST 请求的参数分成三类。第一类是固定参数包括问卷模板 ID、题目编号、必填标志这些基本不变化的字段。这类参数的还原成本最低直接从页面源码或者抓包结果里复制下来就行。但正因为太固定它们也经常被脚本拿来当校验的锚点稍有不符就会被拦下来。第二类是请求相关的动态参数典型代表是时间戳、随机数、一次性令牌。它们的存在意义就是让每次请求看起来新鲜避免重放攻击。还原这类参数的关键在于弄清楚时间戳的精度、随机数的生成范围和令牌的失效条件。比如有的时间戳用的秒级有的用毫秒级有的是 Unix 时间戳有的是格式化字符串后再做过变换。差一个量级服务端算出来的签名就对不上。第三类也是最费心思的是防伪签名参数。这类参数一般由前面所有参数的子集加上一个密钥做哈希或加密运算得出。常见的做法有 MD5 拼接、HMAC-SHA256、或者自定义的位运算混淆。还原这类参数的难度取决于两点算法能不能识别出来以及密钥藏在哪里。密钥可能是硬编码在前端脚本里也可能是从某个接口动态拉取下来的还可能藏在 Cookie 里再参与运算。拿我调试过的一个典型流程举例页面提交时会带上一个看起来像十六进制字符串的校验码长度固定在 32 位。第一次看到时基本可以直接假设是 MD5但直接搜前端脚本里md5(的函数调用却找不到对应入口。后来把请求里的参数按不同顺序拼接逐个尝试常见的哈希组合最后发现校验码是问卷ID 版本号 答案摘要 盐值按特定顺序做双层 MD5 的结果。盐值是页面加载时从后端返回的每次会话都不同。这个例子说明参数复现的核心其实是一场猜规则的过程你得不断缩小参数参与范围再用已知的算法模型去验证。2.2 实践过程中我最常用的一套参数拆解流程先讲结论参数复现不要一上来就翻前端代码、找加密函数那样太容易被带偏。我自己的习惯是走一条更线性的路径。第一步先抓一条完整的提交链路。从打开问卷开始到最终提交完成把所有请求按时间顺序排好记录每个请求的 URL、请求头、参数体和响应内容。这一步的产出是一张请求清单后面所有分析都建立在它上面。注意一定把预请求和主请求的依赖关系标出来很多签名参数是预请求返回的但被用在了主请求里。第二步做参数消融。把请求里的参数分为必然变化和可能固定两类然后逐个尝试固定掉那些动态参数观察服务端返回是否变化。比如把时间戳写死看提交是否还能成功如果不能说明时间戳参与了签名或时效校验再把随机数写死看看会不会收到重复提交之类的报错。这一步的目的不是马上写出还原代码而是用最小代价找出哪些参数真正影响服务端结果。第三步定位算法宿主。确认哪些参数是动态派生的之后再去前端脚本里搜索这些参数的名字或者相邻的变量名。由于现在很多前端代码都经过压缩混淆直接搜参数名往往不如搜提交按钮绑定的函数调用链来得快。更实用的做法是给前端脚本打上日志断点在浏览器里用断点追踪提交那一刻的调用栈看看签名函数是在哪一个作用域里执行。只要一段代码能通过浏览器调试跑通你就可以把同样的逻辑搬到自己的脚本里。第四步是把你还原出来的参数生成逻辑独立成一组函数用同一组原始输入反复验证。验证条件很简单用相同输入跑三遍签名结果必须一致改一个输入签名必须跟着变。如果出现过结果随机跳动的情况说明算法里还有你没捕获的随机因子需要继续回看断点上下文。2.3 参数复现的几个坑有几个坑我在项目里反复踩到在这里提前记录一下。第一算法不一定是标准实现。现在很多前端会用自己设计的混淆方式把标准哈希算法包一层自定义的前处理。比如先把字符串做一次字节反转再塞进标准 MD5又比如把哈希结果从大端序转成小端序再字符串化。这类包装并不会改变算法的输入输出长度但如果拿标准库去算结果永远对不上。排查手段是拿一段已知输入喂给浏览器里的同名函数再把你本地实现的结果跟它逐字节对比。第二参数名会误导分类。有的参数名字看起来像随机数实际却是签名的一部分有的参数名字看起来是签名值实际只是固定字符串。不要基于名字做判断一切以断点命中的位置为准。我之前遇到过名叫nonce的字段功能却完全是版本号导致我在随机数生成逻辑上浪费了两三个小时。第三请求头的顺义校验。问卷星的请求校验不只看参数体还可能检查Content-Type、User-Agent甚至Referer。用脚本直接发请求的时候这些头很容易因为库的默认值被填错。实际测试里遇到签名正确但返回 403 的情况先检查请求头这比继续折腾参数更有可能解决问题。3. 思路二会话模拟——让服务端觉得还是同一个浏览器参数复现这条路走通之后你会进入一个更尴尬的中间状态请求参数能造出来了但提交后经常出现会话已失效或提交异常的提示。这时候就需要会话模拟出场了。3.1 会话模拟为什么有效Cookie 与状态维持服务端判断你是不是刚才那个访问者的最重要依据其实是 Cookie。Cookie 里通常会里存放会话标识、用户状态标识、以及一些安全令牌。有些问卷系统在你第一次打开页面时就种下了一个代表该浏览器开始答题的 Cookie后面每一次提交都必须携带它如果请求里少这个 Cookie哪怕参数全对也会被视为新访问者进而触发风控。会话模拟的另一个层面是复用请求头里的连续性信息。浏览器在向服务端发起请求时会带上Referer、Origin等字段这些字段表明我是在页面内部发起的请求而不是外部直接调接口。服务端完全可以检查这些字段来判断请求是否来自一个合理的浏览器环境。会话模拟的关键就是把这种连续性模拟出来让服务端感觉这条链路从头到尾没有断过。还有一个容易被忽略的状态点是LocalStorage或SessionStorage。一些平台的防刷逻辑不依赖 Cookie而是前端在本地维护一组状态值提交时把这些值合并进参数体。如果你只是复制了 Cookie没有把本地存储状态一并还原同样会被识别为异常访问。这就是为什么真正的会话模拟在实践里往往要同时管理Cookie 池 本地存储快照 请求头模板三样东西。3.2 模拟会话的落地步骤以现在主流的技术栈来说做会话模拟有三个路线可以选择。路线一是直接用可以持久化 Cookie 的 HTTP 客户端比如 Python 的 requests.Session 或者 Node.js 的 axios 配合 cookie 库。你只需要把首次访问页面时返回的 Set-Cookie 存下来后续每次请求都自动带上。这个路线胜在快速适合验证会话模拟到底能不能跑通的阶段。路线二是用无头浏览器。Playwright 或 Puppeteer 都可以让你以真正的浏览器内核去打开问卷、填写内容、点击提交所有 Cookie、LocalStorage、请求头都由浏览器真实生成和维持。这个路线的优势是原生的指纹特征什么都不用自己造缺点是并发能力差、内存占用大而且容易被服务端识别出自动化操作的特征。路线三是混合模拟用无头浏览器完成一次真实答题把过程中的 Cookie、本地存储和关键参数全部抓到再把这些信息交给轻量级的 HTTP 客户端去批量执行。这样既有真实会话的合法性又有高性能的并发能力。我实际项目里用得最多的就是这条路线。它比纯 HTTP 模拟复杂但稳定性远高于完全自己造状态。3.3 会话模拟的局限性会话模拟不是银弹它有一个天然天花板所有会话都有生命周期。问卷星这类系统会给会话设置时效短的可能只有 15 分钟长的虽然可能有数小时但过期后你手里攒的 Cookie 就成了废纸。如果你要做的是一次拿一批数据这类短平快的任务会话模拟非常合适如果目标是长期维护一批会话就必须处理定时刷新、断线重连这些问题复杂度会陡然上升。另一个局限是会话模拟并不能解决参数验签层面的问题。有些系统在每次提交时要求传入的签名参数跟当前会话状态强绑定如果会话状态被服务端主动更换比如答题中途触发了安全验证你手里旧的会话快照就失效了所有后续请求都会被拒绝。换句话说会话模拟是在参数复现之上再加一层保证并不能替代参数复现。4. 两种思路的交叉对比与实际选型聊完了两边的细节再来做一次直白的对比顺便把选型的判断标准讲清楚。4.1 对比表格对比维度参数复现会话模拟核心问题请求参数怎么生成请求身份怎么维持前置投入需要拆解算法逻辑投入高需要构建会话管理投入中等稳定来源依赖对前端脚本的理解深度依赖会话有效时间和状态完整度抗风控能力强只要参数正确且时效内就稳中会话被标记后基本无力回天实现复杂度高算法还原费时中要处理存储和续期适用场景短平快提交、批量答题需要连续操作的场景、页面跳转多的流程主要风险签名算法识别失败会话失效、被风控识别从表格也能看出这两条思路并不是互相取代的关系。实际项目里最常见的情况是参数复现负责造出合法请求会话模拟负责让请求在合理环境中发出。你可以在 5.3 节看到我在生产环境里用的混合方案。4.2 混合使用先复现参数再借会话兜底我自己的实践经验里最稳的组合方式是参数复现为主、会话模拟兜底。流程是这样的先用无头浏览器打开一次问卷把页面初始化的请求结果拿到手包括加密盐值、会话令牌、Cookie 和本地存储快照。然后退出无头浏览器改用轻量级客户端发起提交请求。在发请求前把从浏览器里拿到的 Cookie 逐条塞进客户端的会话对象同时把请求头里的User-Agent、Referer、Origin全部设置成与浏览器一致。最后在请求体里填入参数复现阶段生成的签名和时间戳。这个方案的好处是你不需要在无头浏览器里逐题点击避免自动化特征被识别同时也不需要自己维护完整的状态机因为所有状态都从真实浏览器里借用过来。坏处是每次会话失效后需要重新拉起无头浏览器去刷新状态这个刷新过程如果太频繁整体效率反而会下降。4.3 一套适用于日常调试的参考代码结构下面这段结构不是完整可跑的工具但足以说明参数复现和会话模拟在一个工程里怎么组织。以 Python 为例# 会话管理模块负责从无头浏览器中抽取会话状态 class SessionHarvester: def acquire(self): # 用 Playwright 打开答卷页, 等待初始化请求完成 # 提取 cookie、localStorage、初始化接口返回的 token # 序列化为 JSON 返回 pass # 参数构造模块负责按规则生成签名、时间戳等动态字段 class PayloadBuilder: def __init__(self, session_state): self.token session_state[token] self.salt session_state[salt] def sign(self, answer_data, timestamp): # 按抓包分析的算法拼接字符串 raw f{self.salt}|{answer_data}|{timestamp} # 返回双层MD5后的结果 return self._double_md5(raw) # 提交执行模块使用带会话的客户端发送请求 class Submitter: def __init__(self, session_state): self.session self._build_session(session_state) def submit(self, params): # 这里确保请求头、cookie 与浏览器采集时一致 resp self.session.post(https://example.qualtrics.com/submit, dataparams) return resp.text这个代码结构把三段职责拆开了会话管理只管状态获取参数构造只管签名生成提交执行只管网络传输。后面不管哪一段出问题都可以单独替换。比如今天发现签名算法变了只需要改PayloadBuilder明天发现会话被识别了只需要改SessionHarvester。5. 边界、注意点与常见翻车场景最后这部分说的不是技术是把前面所有内容放进真实环境之后必然要遇到的合规问题和稳定性问题。这些比任何一段代码都更重要。5.1 逆向不等于破解合规边界必须收敛先说明白一个认知参数复现和会话模拟是前端安全测试里常见的手段但能做和可以做是两回事。如果你是对自己的系统做测试、对客户授权的系统做灰度验证、或者在学习研究层面理解协议设计那这些方法完全正当。如果把它用于绕过签名、伪造数据、干扰正常问卷数据的真实性那就跨过了合规红线。问卷星这类问卷工具收集的数据往往用于市场调研、学术统计、用户反馈数据质量直接影响决策。用脚本大量刷题的行为不仅破坏统计有效性对使用问卷的调研方也造成直接损失。我不鼓励也不支持任何人对在线问卷平台发起未经授权的批量提交。写这篇文章的目的是希望读者理解 Web 请求参数和会话机制的原理知道一个提交按钮背后到底有多少逻辑在支撑而不是提供一件批量刷问卷的工具。如果你确实有自动化提交的需求请先确认三点第一目标系统是否允许脚本提交第二你的数据是否是真实、有效、经过授权的第三提交频率和并发是否会对对方服务器造成压力。这三点全部确认无误之后再把本文的技术手段用在合法的范围内。5.2 常见的反爬与风控机制就算你处在合法场景里也仍然可能遇到系统本身的风控策略。问卷星这类平台在经历过大量恶意提交之后迭代出了不少反自动化手段我这里基于常见实践做一个梳理。首先是频控。同一个 IP 短时间提交超过阈值会直接收到请求过于频繁的提示。对应策略是控制并发、模拟人类操作节奏必要时通过代理轮换出口 IP。但代理轮换也有风险如果代理池质量差IP 段本身被风控标记反而更容易触发验证。其次是行为特征检测。如果你用无头浏览器自动化操作鼠标轨迹、点击间隔、滚动行为都会暴露目标。每道题停留 300 毫秒就切下一题的均匀节奏跟真人答题的随机节奏有本质差别。检测模型只要收集几个特征点就能判断操作方是不是真实用户。要绕开这类检测只能把无头浏览器的操作间隔做得足够不规律但这本身也是个比拼耐心的过程。再次是设备指纹。服务端可以通过 Canvas 绘制、WebGL 信息、时区、语言列表、字体列表来生成一个设备指纹。如果你每次请求的设备指纹都不一致说明你背后有脚本在切换环境反而会被重点观察。这也是为什么混合方案里我建议尽量从无头浏览器里借用真实环境信息而不是自己手动拼凑请求头。5.3 排查与稳定性建议把以上这些内容串起来最后分享几个我在实际调试中遇到的翻车场景和应对经验。场景一签名正确但提交后提示会话失效。这种多数是服务端在某次请求里更新了会话标识但没有在你本地的会话存储里同步更新。解决办法是每次请求之后检查响应里的Set-Cookie只要发现有新值立即更新本地会话池。场景二关键参数在运行时动态生成调试时能断到独立复现时却总差一位。这种情况通常是变量作用域问题。你可以把断点打在参数生成完成的那一行把该参数在浏览器内存里的完整值输出到控制台再和自己本地脚本里生成的值做逐位对比。不要只是看看起来一样要逐字符比对。场景三同一条请求在浏览器里能成功在脚本里会莫名其妙的偶发失败。优先怀疑请求头顺序和大小写问题。HTTP 协议里头的名称不区分大小写但某些风控系统会严格按照原始请求头顺序来做校验。如果你用默认客户端发请求头顺序往往会跟浏览器不一致。解决方法是把浏览器里抓到的完整请求头模板固定下来按照顺序逐条设置。场景四本地存储状态污染。如果你复用了浏览器状态一定要在每次答题开始前清理掉上一轮遗留的答题进度和缓存数据。一次清理不彻底后面所有请求都会带上半成品状态服务端可能把它识别为作弊。我建议在无头浏览器里每完成一轮模拟就强制清一次localStorage、sessionStorage和 Cookie再进入下一轮。归根结底不管是参数复现还是会话模拟核心都在于你对整个请求链路的理解是否足够细。抓包只能告诉你发出去的是什么断点调试才能告诉你这些值怎么来的。两条思路合起来用才是一套能扛住真实环境的完整方案。