2026/10/31 19:25:56

某B视频网站模拟登录复盘:前端逆向、RSA加密与签名会话

某B视频网站模拟登录复盘:前端逆向、RSA加密与签名会话 1. 先把“模拟登录”这件事拆开看写这篇东西的起因很简单某天想做一个个人向的数据统计小工具需要拿到自己账号下的观看记录结果卡在登录这一步。点开登录页F12 一开发现密码框里输入的是明文网络面板里飞出去的却是一串长度固定的十六进制字符串——那一刻我就知道这事儿没那么简单。这篇文章就是围绕某B视频网站的模拟登录尝试做的一次完整复盘从链路测绘、参数拆解、加密复现到会话保持和风控边界尽量把中间踩过的坑和想明白的道理都摊开讲。需要先把定位说清楚这不是一篇“教你批量登录”的文章而是一次学习性质的前端逆向与协议复现练习。适合的人群大概是这几类——写过一点 Python 请求脚本、能看懂浏览器开发者工具的面板、对前端加密和参数签名有好奇心、但一直没搞明白“为什么我照着接口抄一遍就是不行”的同学。如果你只是想拿现成的登录态做点个人用途那更省事的做法是直接手动登录后导出 Cookie本文中间也会讲怎么把这个流程做得稳。我更愿意把模拟登录当成一次“翻译”工作浏览器里跑的是一段被人为处理过的 JavaScript最终产出一组服务器认可的字段我要做的是用另一种语言把这组字段一模一样地生产出来。难点不在于写代码而在于搞清楚这组字段里每一个字节是怎么来的、顺序为什么是这样、哪些字段是常量、哪些是随机的、哪些是由其他字段推导出来的。把这些搞明白代码只是自然结果搞不明白代码写得再花哨也登不进去。1.1 登录链路里到底有几个环节很多人对登录的理解停留在“POST 一个账号密码就完事”实际上现代站点的登录链路通常是分步的而且每一步之间有状态依赖。我把这次测绘出来的链路按顺序拆成五段后面所有分析都建立在这个骨架上。第一段是初始化。打开登录页或首页的时候服务端会下发几个 Cookie其中包含一个设备标识类的字段——长这样的一串字符看起来像随机生成的但它会在后续的加密和签名环节被当作输入。这一步很容易被忽略很多同学抓包直接跳到登录请求结果签名怎么算都差一截根子就在这。第二段是取公钥或取盐值。前端在真正提交密码之前会先打一个接口拿回一个带时效性的值。这个接口返回的东西可能是一段 Base64 的公钥也可能是一个随机串加过期时间戳。它存在的意义是把“本次登录”这件事绑定在一个短窗口内防止重放。第三段是本地加密。拿到公钥之后前端用 RSA 之类的方式把明文密码加密或者把密码和一个固定串做哈希。这一步完全在浏览器里发生所以换了环境就必须自己实现一遍等价的算法。第四段是提交登录。真正的 POST 请求里字段通常不止一两个加密后的密码、用户名、设备标识、时间戳签名、验证码组件产生的票据、是否记住登录等等。这里面签名字段是最容易翻车的因为它要求把其他字段按特定规则拼接后再做一次哈希。第五段是结果判定与会话建立。返回体里一般会带状态码和一个跳转地址成功时服务端会通过 Set-Cookie 下发新的会话凭据。这里有个坑光看返回体的状态码不够有些情况下返回码是成功但实际会话并没有建立必须再打一次用户信息接口才算确认。1.2 为什么一上来就照着接口抄通常不管用我见过太多人包括前几年的自己的做法是这样的抓包里看到登录请求复制 URL 和请求体粘到脚本里把账号密码换掉跑一下返回一个“参数错误”或者“签名校验失败”。然后就开始怀疑人生反复对比自己发的请求和浏览器的请求有什么区别肉眼看着一模一样可就是不行。问题出在三个地方。第一是时效性参数时间戳、随机数、公钥对应的一次性标识这些东西在你复制的那一刻就已经过期了服务端拿到一个五分钟前的签名直接拒绝。第二是隐式依赖请求体里某个字段看着像是固定的实际上它是由上一步接口返回的值算出来的你不走那一步它就对不上。第三是请求头与顺序有些校验会把 User-Agent、Referer、Origin 一起纳入签名范围甚至对字段的拼接顺序敏感换个顺序哈希值就完全不同。所以正确的做法不是“抄”而是“重建”。重建的意思是我要知道每一个字段的计算路径然后用代码把这套路径重走一遍。这条路走通了换账号、换时间、换设备参数都不会崩走不通改动任何一个细节都得重新抓包效率极低。1.3 我的工具组合与取舍工具这块我没有追求花哨浏览器开发者工具 一个本地 HTTP 库 一个能执行 JavaScript 的运行时基本够用。浏览器负责抓包和断点调试本地库负责复现请求JS 运行时用于在还没完全看懂算法的时候先“借用”原函数跑通流程等算法吃透之后再改写成原生实现。具体来说请求用 Python 的 requests 就够了会话保持、Cookie 自动管理都很省事加密部分如果确认是 RSA用现成的密码学库实现如果算法藏在混淆得很厉害的代码里先用 Node 加一个执行桥接把原函数跑起来作为过渡方案。这个“先跑通、再吃透”的思路我觉得挺重要——很多人在算法看不明白的时候就卡死了其实完全可以先用黑盒方式验证整条链路是否成立再回头研究每一行的含义。至于那些“一键登录”类的工具我的建议是能不用就不用。原因不是我清高而是这类工具把链路封装得太死出问题的时候你完全不知道错在哪一层排查成本反而更高。而且登录之后的各种状态维护、异常处理终究要自己写绕不开。2. 抓包与链路测绘把黑盒拆成白盒抓包这件事很多人以为点开网络面板就完事了其实真正花时间的是筛选和还原。一个登录页加载下来请求少说几十个多则上百个广告、埋点、静态资源混杂在一起靠肉眼一个个翻效率非常低。我这次的经验是先建立一份“请求地图”再按图索骥。2.1 抓包环境的搭建步骤第一步是把浏览器的网络面板清空勾上保留日志然后从打开登录页开始到登录成功跳转为止完整走一遍。不要中途刷新否则会把前面的请求清掉。第二步是做过滤。我一般会打开两个过滤维度一个是按请求类型筛先只看 XHR 和 Fetch把图片脚本全屏蔽掉另一个是按关键字筛把登录相关的关键词、公钥相关的关键词、以及看起来像接口路径前缀的字符串都加到过滤框里。这一步的作用是把三四十个请求压缩到五六个肉眼就能扫完。第三步是给请求打标签。我习惯在纸上或者笔记里画一条时间线把“页面加载 → 取公钥 → 提交登录 → 跳转 → 拉用户信息”这五个节点标出来然后把对应的请求路径写在节点旁边。这个动作看起来很原始但后面排查的时候你能立刻定位到该看哪一条。第四步是记录请求头的原貌。不要只看 URL 和 body把 User-Agent、Referer、Origin、Content-Type 以及自定义的头字段都复制下来。有几个站点的签名会把 User-Agent 也算进去这时候少一个字符都会失败。注意抓包过程中不要开启任何会改变请求内容的中间层工具否则你抓到的请求和你脚本发的请求其实不是一回事后面会陷入无穷无尽的对比。2.2 关键请求的识别特征判断哪个请求是“取公钥”我一般看三个特征请求方式多为 GET路径里带类似 key、salt、conf、pre 这样的语义片段返回体很短且包含一段看起来像 Base64 的长字符串或者一个带过期时间的对象。这个请求通常发生在密码框获得焦点或者点击登录按钮的瞬间时机上很靠后。判断哪个是“登录提交”特征更明显POST 方法、body 里有用户名和密码相关字段、响应的状态码和业务码是重点、成功时带 Set-Cookie 和跳转地址。这里有个细节有些站点会把登录请求拆成“预检”和“正式”两步第一次请求返回一个挑战值第二次带上挑战值才真正提交遇到这种情况必须两步都复现。判断哪个是“会话确认”看的是登录成功后浏览器里立刻发起的那几个请求通常是拉用户信息、拉配置、拉消息数这些请求都带着新的 Cookie。如果你想验证登录是否真的成功直接看这一批请求有没有正常返回就行比看登录接口本身的返回码更可靠。2.3 参数清单整理与字段含义推断抓包之后我会做一张表把登录请求里的每一个字段列出来标注它的类型、来源、是否变化。这张表是后面写代码的依据也是排查问题时最快的对照物。下面是我这次整理的示意版本字段名做了改写思路通用字段名示意类型来源是否每次变化备注username字符串用户输入否部分站点支持手机号/邮箱password十六进制串RSA 加密结果是长度固定与公钥长度相关keep布尔用户选择否影响会话有效期ts整数当前毫秒时间戳是参与签名需与签名同源device_id字符串初始化下发的 Cookie否会话级换环境会变sign三十二位十六进制多字段拼接后哈希是最容易出错的一环challenge字符串取盐接口返回是带过期时间validate字符串验证码组件票据视情况无验证码时为空做这张表的时候有个技巧对每个字段做一次扰动实验。比如把 ts 改成一个明显错误的值看服务端返回什么错误码把 sign 删掉看是报“缺少参数”还是“签名校验失败”。这些错误码的差异就是你理解服务端校验顺序的最好线索。3. 密码加密与签名参数的复现这一节是整件事的核心。前面所有准备工作都是为了在这一步能准确地“生产”出两个字段加密后的密码和请求签名。这两个字段只要有一个字节不对整条链路就断。3.1 定位加密入口的三种手法第一种是关键字搜索。在开发者工具的源码面板里全局搜索 encrypt、password、rsa、publickey、setPublicKey 这类词通常会命中一两个文件。命中的位置往往就是加密函数的调用点往上翻两层就能看到完整的参数组装逻辑。这个方法的命中率取决于代码的可读性如果打包压缩得很彻底搜出来的结果会有一堆同名变量需要靠上下文判断。第二种是事件断点。在源码面板里找到登录按钮绑定的点击事件或者直接在元素面板上右键选择“打破点于属性修改”然后在页面上点一下登录代码就会停在处理函数的第一行。之后单步跟进一路跟到加密函数这是最直观的办法缺点是如果代码有异步或者被拆分到多个模块跟的过程会比较绕。第三种是XHR 断点。在网络面板里对登录请求下断点请求发出前会暂停这时候看调用栈能直接看到是哪一段代码构造了这个请求体。这招在排查“参数到底在哪一步被改写”的时候特别好用因为你能顺着调用栈一路上溯看到每个字段的赋值过程。我这次三种都用上了主要靠第二种和第三种交叉验证。实际经验是先用 XHR 断点定位请求构造点再用调用栈往上找加密函数最后用全局搜索确认这个函数有没有被别的路径复用。三者对得上基本就不会看错。3.2 从混淆代码里捞关键函数打包后的代码可读性通常很差变量名全是单字母函数被拆成一层套一层。这时候不要试图理解整个文件只盯住与加密相关的那个片段把它单独抽出来看。我的做法是在断点停住之后把加密函数的源码复制到一个临时文件里手动格式化然后一行一行标注输入输出。比如一个典型的 RSA 加密调用形式上大概是“用某个库的公开方法传入明文和公钥输出十六进制”。混淆只能改名字改不了结构和调用关系你把参数从哪来、结果往哪去标清楚逻辑就浮出来了。还有一种情况是加密逻辑被塞进 WebAssembly 或者被拆成多个小模块动态加载。这种我一般不去硬啃直接用执行桥接的方式把原函数跑起来调用方式和浏览器保持一致结果自然也一致。这不是偷懒是权衡把时间花在链路和参数上比花在还原一段高度混淆的机器码上更有价值。3.3 本地复现加密的两种方案方案 A 是原生实现。确认算法是 RSA 之后用 Python 的密码学库直接把公钥喂进去加密明文输出格式按抓包看到的来十六进制还是 Base64有没有补零大小写敏不敏感。这一步的坑主要在填充模式和输出格式上常见的是 PKCS#1 v1.5 填充、输出十六进制但不同站点的选择不一样必须按抓包结果对齐。from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 import binascii def encrypt_password(plain: str, pub_key_pem: str) - str: # 公钥来自取盐接口形如 -----BEGIN PUBLIC KEY----- 开头 key RSA.import_key(pub_key_pem) cipher PKCS1_v1_5.new(key) # 注意部分站点要求先做一次哈希或加固定后缀需按抓包结果对齐 payload plain.encode(utf-8) encrypted cipher.encrypt(payload) return binascii.hexlify(encrypted).decode(ascii)方案 B 是执行桥接。用 Node 跑一个极小的服务把浏览器里那个加密函数原样搬过去Python 通过子进程或者本地接口调用它。好处是行为绝对一致坏处是多了一层依赖部署和调试都麻烦一点。我的建议是把方案 B 当作过渡先用它确认整条链路能通再逐步用方案 A 替换掉最终只留原生实现。// bridge.js 示意把浏览器里的加密函数抽出来单独跑 const crypto require(crypto); function encryptPassword(plain, publicKeyPem) { const buf Buffer.from(plain, utf8); const encrypted crypto.publicEncrypt( { key: publicKeyPem, padding: crypto.constants.RSA_PKCS1_PADDING }, buf ); return encrypted.toString(hex); } // 从标准输入读参数输出结果供上层脚本调用 process.stdin.on(data, (chunk) { const { plain, key } JSON.parse(chunk.toString()); process.stdout.write(encryptPassword(plain, key)); });两种方案我都跑过结论是先用 B 打通再用 A 落地。因为你在还没完全确定填充模式和输出格式的时候原生实现要试很多次而桥接方案一次就对能快速排除“加密这一层没问题”的假设把精力集中到其他环节。3.4 签名串的拼接规则签名是整个登录流程里最容易被低估的部分。它的本质是把若干个字段按约定规则拼成一个字符串再做一次哈希常见是 MD5 或 SHA-256把结果作为 sign 字段一起提交。服务端收到后用同样的规则重算一遍比对是否一致。拼接规则通常包含三个要素参与字段的范围、排序方式、是否附加固定密钥。范围一般是“所有业务字段除去 sign 本身和空值字段”排序一般是按字段名的字典序升序固定密钥可能是硬编码在代码里的一个常量串也可能来自取盐接口。import hashlib def build_sign(params: dict, app_secret: str) - str: # 1. 过滤空值与 sign 本身 filtered {k: v for k, v in params.items() if k ! sign and v not in (, None)} # 2. 按字段名字典序升序排列 items sorted(filtered.items(), keylambda kv: kv[0]) # 3. 拼成 kvkv 形式末尾追加固定密钥 raw .join(f{k}{v} for k, v in items) app_secret # 4. 哈希后转小写十六进制 return hashlib.md5(raw.encode(utf-8)).hexdigest()这里有几个必须验证的细节任何一个错了都会导致签名不匹配。第一空值字段到底参不参与有些站点把空字符串也拼进去有些不拼得靠实验确定。第二布尔值怎么表示是 true/false 还是 1/0或者干脆转换成字符串。第三哈希结果的大小写服务端比较时是否忽略大小写。第四固定密钥的位置是拼在末尾、拼在开头还是作为某个字段的值。验证的方法很朴素拿浏览器实际发出的 sign 值用你写的函数算一遍看能不能对上。对不上就逐个变量调整直到一致为止。这个过程可能要试十几次但只要对上了一次后面就稳了。提示时间戳字段要特别注意“同源”。签名用的 ts 和请求体里提交的 ts 必须是同一个值不能一个用秒一个用毫秒也不能在两次调用之间重新取时间。我因为这个问题排查了将近一个小时。4. 会话管理Cookie、设备标识与状态校验能发出一个“格式正确”的登录请求只是走完了一半。真正让脚本可用的是会话管理——登录成功之后拿到的那组凭据怎么存、怎么复用、怎么判断还有效。4.1 登录前后 Cookie 的差异我习惯在登录前后各导出一次 Cookie做成对比表。登录前的那批主要是设备标识和匿名会话登录后会新增或替换几个关键字段其中一个是身份凭据通常带有效期另一个是刷新凭据有效期更长。剩下的就是些统计和偏好类的小字段对业务请求没影响。对比表做完就能确定“最小可用 Cookie 集”。这个集合的意义很大后续所有请求只需要带上这几个字段不用把整个 Cookie 罐子端来端去既干净又不容易因为某个无关字段过期而整体失效。Cookie 类别登录前登录后是否必须携带设备标识有有可能刷新是匿名会话有有否身份凭据无有是刷新凭据无有按需统计偏好类有有否4.2 会话持久化与复用持久化这块我踩过一个坑一开始直接把 Cookie 存成 JSON 文件下次启动加载回来直接用结果过了一天就失效了。后来才想明白凭据是有有效期的长则几天短则几小时而且部分站点会在每次请求后滚动更新你存的是旧值用着用着就对不上了。更稳的做法是每次启动都做一次有效性校验拿存下来的凭据请求一个轻量接口比如拉当前用户基本信息返回正常就继续用返回鉴权失败就重新走登录流程。这样既避免了频繁登录也不会在凭据失效后一脸茫然地对着错误日志排查。至于要不要维护刷新逻辑我的看法是看使用频率。如果只是偶尔跑一次数据统计重新登录的成本很低没必要做刷新如果要长期挂着运行那就得把刷新凭据纳入管理并且在刷新失败时优雅降级到重新登录。4.3 登录成功的判定信号判定成功不能只看登录接口的返回码。我这次遇到的情况是登录接口返回了成功状态跳转地址也给了但因为设备标识和会话绑定的关系后续请求依然被判定为未登录。所以我把判定改成三步登录接口返回成功 → 跳转请求能正常返回 → 用户信息接口能拿到数据。三步都过才算真的登录成功。这三步的意义还在于定位问题。如果第一步失败问题在参数或加密如果第二步失败问题在 Cookie 传递或重定向处理如果第三步失败问题在会话绑定。哪一步断就往哪个方向查比盲目打印日志高效得多。def check_login(session) - bool: # 第一步确认登录接口本身返回成功此处为示意路径 r1 session.post(https://example.com/api/login, datapayload, timeout10) if r1.json().get(code) ! 0: return False # 第二步访问一个需要登录态的轻量接口 r2 session.get(https://example.com/api/nav, timeout10) if r2.status_code ! 200: return False # 第三步确认业务数据能取到而不是返回空壳 data r2.json().get(data) or {} return bool(data.get(user_id))5. 风控边界与工程化取舍写到这一节我想稍微收一收讲讲边界。技术上能跑通不代表应该无限制地跑尤其是登录这种强身份相关的环节站点侧的风控投入通常是最高的硬碰硬的成本也很高。5.1 常见风控信号的识别从请求侧能观察到的信号大概有这么几类。频率类同一设备标识在短时间内多次登录、同一 IP 段内出现大量不同账号的登录、请求间隔过于均匀说明是定时器驱动的而不是人为操作。一致性类Cookie、User-Agent、请求头顺序与实际声称的环境不匹配或者签名里的时间戳与请求到达时间的偏差过大。行为类缺少正常浏览过程中的前置请求直接从登录跳到业务接口这种“蹦极式”的访问路径很容易被标记。这些信号本身不代表一定被拦但一旦叠加出现触发挑战的概率就很高。我在实验过程中遇到过一次连续几次参数错误之后登录接口开始返回需要额外验证的响应这说明失败的尝试也是被计数的。5.2 请求节奏与参数一致性节奏这块没什么玄学就是别快、别均匀、别重复。我自己的实验脚本里加了一个简单的随机间隔范围大概在几秒到十几秒之间虽然不能完全模拟人工但至少不会出现“每 1000 毫秒精准打一发”这种明显的机器特征。参数一致性是更容易被忽视的点。同一轮会话里设备标识要固定User-Agent 要固定语言、时区这些头字段也要固定。我见过有人每次请求都重新生成一遍设备标识结果就是每个请求都被当成新环境永远建立不起有效会话。5.3 我主动放弃自动化的几个环节必须说清楚验证码环节我直接放弃了没有做任何形式的自动识别尝试。原因有两个一是这个方向本身超出学习范畴二是即使勉强做出来稳定性也极差维护成本远高于收益。遇到需要验证码的场景我的处理是人工介入一次拿到会话凭据后走持久化复用这也是一开始提到的“更省事方案”。另外我也没去做设备指纹的伪造没有去尝试多账号轮换。这两件事都会把脚本从“个人学习练习”推向另一个性质不在我这次的目标范围内。这次实验的终点很明确用自己的账号完整走通一次登录流程理解每个参数是怎么来的。达到这个目标就停手。注意把自己的实验频率控制在几次以内失败的尝试也要算在计数里。参数错误时不要反复重试先停下来核对比连打二十次有用得多。6. 排查实录与常见问题速查最后一节讲排查。这个问题我最有发言权因为整个实验过程中光“参数错误”和“签名校验失败”这两个提示我就各见了十几次。6.1 问题速查表下面这张表是我这次整理出来的按出现频率排序基本覆盖了模拟登录最常见的失败原因。现象大概率原因排查动作返回参数错误但字段都齐签名参与字段范围不对用浏览器实际 sign 反推逐个增删字段试返回签名校验失败时间戳与其他字段不同源确认 ts 只取一次全程复用返回密码格式错误加密输出格式不匹配检查是十六进制还是 Base64大小写是否敏感登录接口成功但业务接口未登录Cookie 未正确传递或设备标识不一致打印会话内全部 Cookie对比登录前后差异取公钥接口返回过期请求间隔过长或挑战值复用缩短两步之间的间隔每次登录重新取提示需要额外验证失败次数累积触发挑战停止重试改为人工登录后复用会话偶发成功偶发失败随机数生成或参数顺序不稳定固定随机源检查拼接顺序是否确定6.2 一套固定的排查顺序我发现很多人排查是随机的今天怀疑加密明天怀疑签名效率很低。我后来固定了一套顺序从外往里查一步一步排除。第一步查网络层脚本发的请求和浏览器发的请求用抓包工具各抓一次逐字对比 URL、方法、头字段、body。这一步能排掉八成“看起来一样其实不一样”的问题比如少了一个头、多了一个空格、编码方式不同。第二步查参数来源把每个字段的值打印出来和浏览器里的值逐个对照。重点是动态字段时间戳、随机数、挑战值、设备标识看它们是否来自正确的前置接口。这一步能排掉隐式依赖的问题。第三步查加密结果用同样的明文和公钥分别跑浏览器里的函数和本地的实现比对输出。注意必须用同一组输入否则比了也没意义。这一步能排掉算法实现的问题。第四步查签名用浏览器实际发出的 sign 作为基准反推拼接串。常用的办法是故意改动一个字段看 sign 是否随之变化从而判断它是否参与签名。第五步查会话登录成功之后立刻打印全部 Cookie再打一个需要鉴权的接口看是否被识别。这一步能排掉会话绑定的问题。这套顺序的好处是每一步都有明确的判断依据不会陷入“我觉得是这里”的玄学猜测。我后面几次调试基本都在前三步就定位到了问题。6.3 几条实测心得第一条先跑通再优化。我一开始想直接用原生实现结果在填充模式上反复试了很久浪费了大半天。后来改成先用桥接方案跑通整条链路确认后面几步都没问题再回头慢慢替换效率高得多。第二条保留每一次失败的请求体。我会把每次失败的请求参数存下来标注上返回的错误码形成一个小样本集。等积累到十几次之后规律就出来了比如“只要 ts 是偶数就失败”这种匪夷所思的规律靠单次调试根本发现不了。第三条别信自己的记忆。参数顺序、字段名、大小写这些细节我一开始全靠脑子记结果每次改代码都要重新抓包确认。后来老老实实写成配置文件加注释改起来一目了然。这种“笨办法”反而最省时间。第四条遇到卡住就换视角。有一次签名怎么算都对不上我把拼接串打印出来盯着看了半小时没看出问题后来把浏览器里的拼接串也打印出来两行并排一放发现多了一个尾部符号。这种问题靠推理是找不到的只能靠对照。这个内容后续还可以往两个方向延伸一是把整条链路封装成一个可复用的小工具把加密、签名、会话管理拆成独立模块换站点时只改配置二是往协议分析的方向走研究不同站点的参数设计思路有什么差异哪些是行业通行做法哪些是各家自己的巧思。我自己更倾向前者毕竟工具做出来能天天用后者更适合当闲时的读物。最后再分享一个小习惯每次实验结束把关键的请求和参数整理成一份笔记存起来隔几个月再看能省掉大量重新摸索的时间。