news 2026/10/31 5:22:26

Python模拟登录实战:前端RSA加密、Session维持与风控排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python模拟登录实战:前端RSA加密、Session维持与风控排查

做数据备份这件事,最烦的从来不是写代码,而是登录。前阵子想把某B视频网站上自己账号的观看记录拉一份到本地,做个简单的年度统计,结果整个流程写下来,真正花时间的部分全耗在了模拟登录上:密码在前端就被加密了,参数里混着好几个看不见的签名,cookie 还得按特定顺序喂进去。这篇就把这一趟踩过的路完整复盘一遍——从前端加密怎么定位、RSA 参数怎么拼、Session 怎么维持,到风控为什么会找上门、报错该怎么查。适合已经会写 requests、但还没正经处理过登录态的同学,也适合纯粹好奇"点一下登录按钮背后到底发生了什么"的读者。说明一下前提:所有操作仅针对我自己的账号、用于本地个人数据整理,不做批量、不做越界、不碰别人的数据,这条底线比技术本身重要得多。

1. 先想明白:我们要"模拟"的到底是什么

1.1 点一下登录按钮,浏览器背地里干了六件事

很多人对登录的直觉印象是"提交账号密码,服务端返回成功"。真把网络面板打开,你会发现链路上远不止一次请求。完整的动作序列大概是这样的:浏览器先加载首页文档,服务端在响应头里塞下若干设备标识类 cookie;页面加载过程中,前端脚本读取这些标识并做一次预登录请求,拿到一把公钥和一个一次性盐值;用户填完表单点提交,前端用刚拿到的公钥和盐值把密码加密;加密结果连同账号、时间戳、各类标识一起提交到登录接口;服务端校验通过后下发新的 cookie 或返回一个 token;最后前端再打一个用户信息接口,确认身份并跳转。

这六步里,只有第三步和第五步是"必须模拟"的,但第一步到第四步之间存在严格的先后依赖——你没拿到公钥就没法加密,没拿到设备 cookie 就可能直接被判定为异常来源。所以我把它理解成一条有向链,而不是一串可以随便调换顺序的接口。理解这一点之后,很多"为什么我直接 POST 登录接口返回参数错误"的困惑就自然消解了:不是参数写错了,是你把链条从中间截断了。

1.2 模拟登录与普通接口抓取的本质差别

普通的列表接口抓取,本质上是"公开读":你带个 UA,拼对查询参数,服务端就给你数据,请求之间互不影响,失败重试一次往往就好了。登录接口完全不是这个性质,它是"身份写"——要建立一个能在后续请求中被反复识别的新状态,而且这个状态是服务端主动发放的,你没法凭空构造。

这个差别带来的直接后果有三个。第一,参数不再是静态的,每次登录的公钥、盐值、时间戳、随机数都不一样,任何写死的值都会在第二次运行时失效。第二,失败代价变高,普通接口请求失败大不了重试,登录失败几次之后账号本身可能被临时限制,试错空间很小。第三,可观测性变差,登录失败往往只返回一个模糊的错误码,不告诉你到底是密码错了、参数错了、还是环境不对,排查只能靠对比。

我把这个总结成一句话:抓公开接口是在"对答案",模拟登录是在"对过程"。你得让服务端觉得你这套动作序列和真实浏览器一致,而不只是最终那个 payload 长得像。

1.3 动手前先把边界划清楚

技术上能做,和应不应该做,是两码事。我在动手前给自己定了几个约束,后面每写一段代码都会回头核对一遍:只用自己实名注册的账号;不做多账号轮换;遇到验证码就停下,不引入任何自动识别方案;请求频率控制在人的正常操作量级,不做并发;抓下来的数据只存本地,不上传、不分享、不用于任何形式的再分发。

这几条听起来像是免责声明,实际上是实打实的技术约束。比如"遇到验证码就停"这一条,直接决定了你的代码结构——你必须在流程里设计一个人工介入的断点,而不是设计一个自动重试循环。再比如"频率控制在人的量级",决定了你不能写for i in range(100)这种批量登录的循环。想清楚这些,后面写代码时就不会被"反正先跑通再说"的冲动带偏。

注意:不同平台对自动化访问的规则写在用户协议和 robots 文件里,条款差异很大。动手前花十分钟读一遍相关章节,比事后被封号划算得多。规则明确禁止的部分,不要用技术手段去试探边界。

2. 侦察阶段:把登录流程拆成可观测的请求

2.1 抓包环境怎么搭才干净

工欲善其事。我用的是浏览器自带的开发者工具,几个开关必须提前设好,否则你会被缓存和跳转坑得很惨。Network 面板里把 "Preserve log"(保留日志)勾上,因为登录成功后页面通常会整页跳转,不勾的话之前的请求记录全被清空,你就只能看到跳转后的结果,看不到加密过程。再勾上 "Disable cache",避免加载到旧的脚本。最后用一个无痕窗口操作,保证初始环境没有任何历史 cookie 干扰,这样你看到的每一条 Set-Cookie 都是这次产生的,因果关系清清楚楚。

过滤条件也很关键。默认全量展示时,一个首页加载下来可能有上百条请求,图片、字体、统计脚本全混在一起。我一般先按 "Fetch/XHR" 过滤,把静态资源排掉,再按关键词搜接口名。如果实在找不到,就反过来从脚本入手:切到 Sources 面板,全局搜索关键词,用Ctrl+Shift+F做全项目搜索,这招在定位加密逻辑时几乎是必用的。

实操心得:准备两个浏览器会话,一个用来正常登录观察流程,一个保持未登录状态用于反复对比。同一个请求,登录前后携带的 cookie 和 header 差异,往往就是最直接的线索。

2.2 登录链路上的关键请求清单

把整条链路的请求一个个记下来,是我认为最值得投入时间的一步。下面这张表是我这次实际记录的结果,接口名做了脱敏,只保留结构和作用,你在自己的场景里替换成对应的即可。

序号请求作用关键字段是否必须模拟
1加载首页文档响应头下发的设备标识 cookie必须,且要放在最前
2加载前端脚本无(但要记录脚本地址,后续要找加密函数)不需要请求,但必须阅读
3预登录,取公钥与盐值返回公钥串、一次性盐值、有效期必须,且要在登录前实时获取
4提交登录表单账号、加密后的密码、时间戳、各类标识、签名必须,核心
5登录后校验身份携带新 cookie,返回用户基本信息建议,用于确认登录态真的建立成功
6跳转页面的附属请求各种统计、推荐接口完全不需要

这张表的价值在于:它把"我要写多少个函数"这件事定量化了。序号 1、3、4、5 各对应一个函数,加起来不到一百五十行代码,功能边界非常清晰。很多人写模拟登录写成一坨,就是因为没有先做这一步拆解,把所有逻辑堆在一个函数里,出错了根本不知道是哪一环断的。

2.3 参数分三类:写死的、算出来的、藏起来的

把登录请求的 payload 完整复制出来,逐字段过一遍,我给每个字段打了标签。第一种是静态值,比如标识登录类型的act=login、标识来源页面的goto参数,这类值在多次登录之间完全不变,直接写死在配置里就行。第二种是半动态值,比如毫秒时间戳、随机字符串、从预登录接口拿到的盐值,它们每次都不一样,但生成规则是明确的,自己算就行。

第三种最难缠,我叫它藏起来的值。它通常出现在参数名很奇怪的位置,值是一长串十六进制或者 base64,在页面上完全看不出来是怎么来的,只存在于某段被压缩混淆的脚本里。这类参数要么是设备指纹的哈希,要么是多个字段拼接后的签名。判断方法很简单:把登录请求复制成 cURL,然后改动其中一个字段的值重新发一次,如果返回参数错误,那它就是被校验的签名;如果返回正常,那它大概率只是个可有可无的埋点字段。

我用这个方法砍掉了将近一半的字段。原始 payload 有二十多个键,最后真正参与校验、必须正确生成的只有八个左右。剩下的要么是统计埋点,要么是服务端根本没读。这个筛选过程省下的调试时间,比后面所有优化加起来都多。

3. 硬骨头:前端加密的定位与还原

3.1 在混淆的脚本里找到加密入口

登录请求的密码字段是一长串 base64,长度还随公钥变化,基本可以断定是 RSA 之类的非对称加密。问题是算法好猜,入口难找。压缩混淆后的脚本动辄几万行,变量名全是单个字母,靠肉眼翻是不现实的。

我的定位路径是这样的:先在 Network 面板里右键那条登录请求,选 "Copy as cURL",确认密码字段的特征;然后回到 Sources 面板做全局搜索,依次尝试encrypt、publicKey、setPublicKey、JSEncrypt、rsa这些关键词。如果搜不到,就换思路——在 Network 面板里给登录请求打一个 XHR 断点,然后正常操作一次登录,代码会停在发起请求的那一行,此时看右侧的调用栈(Call Stack),一层层往上翻,通常两三层就能翻到加密函数所在的位置。

这个方法的关键在于"让代码自己告诉你答案",而不是你去猜它在哪。断点停下的那一刻,调用栈上每一层都是一个明确的函数名和文件位置,比搜索关键词准确得多。我这次就是靠断点定位的,从发请求的那行往上翻了两层,就看到了一个把密码和盐值拼接后送进加密函数的地方,前后不到五分钟。

3.2 RSA 加密的四个实操细节

找到加密函数之后,剩下的事就是把它翻译成 Python。理论上很简单——公钥加密、base64 编码,两行代码。但实际操作里有四个细节,每一个都能让你卡上半小时。

第一个是填充模式。前端库里用的基本都是 PKCS#1 v1.5 填充,Python 侧对应Crypto.Cipher.PKCS1_v1_5。如果你用成了 OAEP,加密能成功,但服务端一定解密失败,返回的还是那句含糊的"用户名或密码错误",非常误导人。第二个是公钥格式。从接口拿到的一般是不带 PEM 头尾的裸 base64 串,需要自己补上-----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----,再按 64 字符换行,RSA.import_key才认。第三个是明文长度限制。RSA 单次能加密的字节数受密钥长度限制,1024 位密钥最多 117 字节明文,超了会直接抛异常。所以很多站点会在密码前面拼一个盐值或哈希,而不是直接加密原始密码,你翻译的时候要原样保留拼接顺序。第四个是输出编码。前端经常用 URL 安全的 base64,也就是把+换成-、/换成_,并去掉末尾的=。你如果原样输出标准 base64,服务端会判定格式非法。

import base64 from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 def rsa_encrypt(plain: str, salt: str, pub_key_b64: str) -> str: """按前端逻辑:盐值 + 明文 拼接后做 RSA 加密,输出 URL 安全 base64""" message = (salt + plain).encode("utf-8") # 补全 PEM 头尾,每 64 字符换行 body = "\n".join(pub_key_b64[i:i + 64] for i in range(0, len(pub_key_b64), 64)) pem = f"-----BEGIN PUBLIC KEY-----\n{body}\n-----END PUBLIC KEY-----" key = RSA.import_key(pem) cipher = PKCS1_v1_5.new(key) encrypted = cipher.encrypt(message) return base64.b64encode(encrypted).decode().replace("+", "-").replace("/", "_").rstrip("=")

这段代码我反复调了三轮才对上。第一轮忘了补 PEM 头,报ValueError: RSA key format is not supported;第二轮补了头但没做换行,还是同样的错;第三轮填充对了、编码对了,但拼接顺序搞反了,变成"明文 + 盐值",服务端照样不认。所以别嫌麻烦,每改一处就单独打一次日志,把加密前后的结果打印出来,和浏览器里抓到的密文对着看——虽然每次密文都不同(PKCS#1 v1.5 填充带随机字节),但长度必须完全一致,长度对了说明密钥长度和填充模式都对了。

3.3 参数顺序与签名:错一位就全错

有些站点不止加密密码,还会对整个参数集合做一次签名。签名逻辑通常是把若干字段按某种顺序拼成一个字符串,再和某个固定串一起做哈希。这里的坑在于"顺序",而且这个顺序往往和 payload 里的字母序、出现顺序都不一样,是前端代码里手写的。

我的做法是:在断点停下时,把参与拼接的那几个变量名和拼接表达式完整抄下来,一字不差地翻译。不要试图"优化"或者"按字母序排一下",因为服务端校验的是同一份逻辑,你只要有一个字段漏了、顺序换了、分隔符变了,签名就永远对不上,而报错信息通常不会告诉你是签名错了。

还有一个容易忽略的点是时间戳精度。有的接口要毫秒,有的要秒,还有的要求是字符串带三位小数。这个从浏览器抓到的值上直接就能看出来——数一下位数,13 位是毫秒,10 位是秒,别自己猜。另外时间戳和服务器时间偏差过大也会被拒,一般容忍窗口在几分钟内,所以别用写死的固定时间戳来"图省事",那只能成功一次。

4. 登录态的维持:cookie、token 与设备标识

4.1 Session 对象能省掉一半的麻烦

第一版代码我是裸用requests.post写的,登录成功后手动从响应头里抠Set-Cookie,再拼成字符串塞进下一次请求的 header。这套写法能用,但极其脆弱:多个Set-Cookie头的时候容易漏,Expires属性带逗号会截断字符串,跨子域名的 cookie 还得手动处理 domain 匹配。后来换成requests.Session,这些问题一次性消失。

Session对象内部维护一个 cookie jar,会自动解析响应头里的每一条Set-Cookie,按 domain 和 path 规则存储,后续请求自动带上匹配的那些。你唯一要做的是保证整个流程都复用同一个 session 实例,不要中途新建。这一点看起来是废话,但我确实踩过——在某个辅助函数里图省事写了requests.get(...)而不是session.get(...),结果那次请求没带上设备 cookie,服务端把它当成了一次全新的匿名访问,后面整条链路的状态就乱了。

import requests session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (...) Chrome/120.0.0.0 Safari/537.36", "Accept-Language": "zh-CN,zh;q=0.9", }) # 第一步永远是先拿设备标识,再谈登录 session.get("https://www.example-video.test/", timeout=10) # 之后所有请求都走 session,不要混用 requests.xxx key_resp = session.get("https://passport.example-video.test/api/getkey", timeout=10)

提示:User-Agent在整个流程里必须保持一致。先用移动端 UA 拿公钥,再用桌面端 UA 提交登录,是很典型的异常特征。这个细节在日志里完全看不出来,只能靠自查。

4.2 为什么必须先"逛一圈"再登录

这一条是我花了最久才想明白的。一开始我以为登录就是"拿公钥 → 加密 → 提交",三步搞定。实际上直接这么干,十次里有八次返回的是风控类的模糊错误。原因在于:服务端在首页响应的Set-Cookie里下发了一个设备标识,这个标识在登录接口里是要参与校验的,它证明"这个客户端至少正常加载过页面"。你跳过这一步,等于凭空冒出来一个 POST 请求,特征极其明显。

所以正确的顺序是:先 GET 首页(拿设备 cookie),再 GET 预登录接口(拿公钥和盐值),最后 POST 登录。三步都走同一个 session。这个顺序不能颠倒,也不能合并——我试过把预登录和登录合成一个函数连续调用,中间不加任何间隔,结果失败了;加个半秒的停顿就正常了。这说明服务端对"拿到公钥后立刻提交"这种机器特征是有感知的,人对人操作是要点时间的。

4.3 登录成功后怎么确认真的成功了

这里有个很常见的误判:看到登录接口返回了 200 就以为成功了。实际上很多站点的登录接口无论成功失败都返回 200,真正的结果藏在响应体里的一个状态码字段,或者藏在有没有下发关键的登录 cookie 上。

我的验证方式是双重的。先检查响应体里的状态字段,确认是成功值;然后再打一个需要登录态才能访问的接口,比如"获取我的基本信息",如果这个接口能正常返回用户数据,才算真的登录成功。这一步不能省,因为有些情况下服务端会先返回成功、再在后续请求里做二次校验,如果你不验证,会在写业务逻辑的时候才突然发现拿不到数据,那时候就更难定位了。

登录成功后,把session.cookies序列化存到本地,下次运行直接加载,可以省掉重复登录。但要注意存的时候把domain、path、expires一起存下来,只存 name 和 value 的话,加载回去可能不生效。

5. 风控与验证码:该收手时就收手

5.1 哪些行为会被判定为异常

风控系统不看你的代码写得好不好,它看的是行为特征。我把这次遇到和排查到的信号整理成一张表,左边是信号,中间是它在你日志里的表现,右边是我实际采取的应对。

风控信号典型表现我的应对方式
缺少设备标识登录接口返回模糊错误,无具体原因严格按首页 → 预登录 → 登录的顺序走
UA 与请求头不一致有时成功有时失败,复现困难所有请求统一用同一套 header,集中配置
缺少来源页返回参数错误或直接拒绝补上 Referer 和 Origin,值与实际页面一致
请求间隔过短连续多次失败后开始出现验证码关键步骤之间加 0.5 到 1 秒停顿
时间戳偏差过大提示签名或参数失效用本机当前时间实时生成,不留缓存
短时间重复登录账号被临时限制,提示稍后再试单次运行只登一次,失败不自动重试

这张表里我最想强调的是最后一条。"失败不自动重试"这个设计,一开始我很不适应,觉得不够健壮。但实际经验是:登录失败的原因九成是参数或环境问题,重试一百次结果都一样,反而会把失败次数刷上去,触发账号级的临时限制。正确的做法是失败就退出,把响应体完整打印出来,人工分析。等你把原因修好了,一次就能过。

5.2 验证码出现后的处理思路

验证码出现,本质上是一个信号:服务端认为这次访问的可信度不够,要求额外证明。这时候继续用代码硬刚,方向就错了。我在代码里设置的策略是——检测到响应里出现验证码相关字段,立刻终止流程,打印提示,退出。

具体做法是在登录响应解析时判断几个特征:响应体里有没有验证码类型字段、有没有图片或滑块相关的资源地址、状态码是不是特定的那个值。命中任何一个,就raise一个自定义异常,由上层捕获后提示"需要人工处理,请手动登录后再导出 cookie"。

这个设计把"自动化"的边界划得很清楚:能自动的部分自动,不能自动的部分交给人。我见过太多人在这上面钻牛角尖,花大量时间去研究怎么自动过验证,最后既不稳定也不合适。相比之下,手动登录一次、把 cookie 导出成本地文件、后续复用,这个方案稳定得多,几分钟就能搞定,而且完全在规则允许的范围内。

5.3 账号与凭据的安全习惯

写这类脚本,最容易犯的低级错误是把账号密码硬编码在源码里,然后顺手推到公开仓库。我现在的习惯是:账号密码一律从环境变量读,代码里只留变量名;本地调试用一个.env文件,这个文件第一行就写进.gitignore;日志里打印参数时,密码字段统一替换成固定长度的星号,避免调试日志泄露。

import os from dotenv import load_dotenv load_dotenv() # 从 .env 读取,该文件不纳入版本管理 USERNAME = os.environ["SITE_USERNAME"] PASSWORD = os.environ["SITE_PASSWORD"] def mask(value: str, keep: int = 2) -> str: """日志脱敏:只保留首尾少量字符""" if len(value) <= keep * 2: return "*" * len(value) return value[:keep] + "*" * (len(value) - keep * 2) + value[-keep:]

另外,cookie 文件本质上等同于登录凭据,拿到它就能以你的身份访问。所以这个文件的权限要收紧,别放在共享目录里,也别随手发给别人"帮我看看为什么跑不通"。真要给别人看日志,先把 cookie 值和 token 值替换掉。

6. 完整实操:从零跑通一个最小闭环

6.1 目录结构与依赖清单

整个项目我控制在四个文件以内,刻意不做过度设计。因为这个脚本的生命周期通常很短,接口一变就得改,结构越复杂维护成本越高。

project/ ├── .env # 账号密码,不入库 ├── config.py # 常量:域名、请求头、接口路径 ├── login.py # 核心:预登录、加密、提交、校验 └── cookies.json # 登录成功后持久化的会话

依赖就三个:requests处理 HTTP 和会话,pycryptodome提供 RSA 实现,python-dotenv读取环境变量。装的时候注意包名是pycryptodome而不是pycrypto,后者已经很久没维护了,在新版本 Python 上编译经常失败。

pip install requests pycryptodome python-dotenv

config.py里我只放真正不变的东西:域名、接口路径、固定的请求头。凡是会变的(公钥、盐值、时间戳)一律不放在这里,避免"配置里存了个过期值"这种隐蔽问题。

6.2 分步实现与关键代码

第一步,建立会话并获取设备标识。这一步的返回值我不关心内容,只关心session.cookies里多了什么。

def bootstrap(session, cfg): resp = session.get(cfg["HOME_URL"], timeout=10) resp.raise_for_status() # 打印一下拿到了哪些 cookie 名,方便核对 print("初始 cookie:", [c.name for c in session.cookies]) return resp

第二步,预登录拿公钥和盐值。这里要特别注意返回结构,很多站点的公钥字段嵌套两层,还有的是在data下面包了一层。

def fetch_key(session, cfg): resp = session.get(cfg["KEY_URL"], timeout=10) data = resp.json() pub = data["data"]["public_key"] salt = data["data"]["salt"] print(f"公钥长度: {len(pub)}, 盐值长度: {len(salt)}") return pub, salt

第三步,加密密码。直接复用 3.2 节那个函数。这里有个细节:盐值要不要参与拼接、拼在前面还是后面,完全取决于前端的实现,我这次是拼在前面。判断依据就是断点时看到的那个字符串拼接表达式,别自己发挥。

第四步,组装参数并提交。参数顺序在字典里其实不重要(服务端按 key 取),但如果涉及签名,拼接顺序就极其重要,必须严格照抄。

def do_login(session, cfg, username, password, pub, salt): import time, hashlib enc_pwd = rsa_encrypt(password, salt, pub) ts = str(int(time.time() * 1000)) payload = { "act": "login", "username": username, "password": enc_pwd, "ts": ts, "source": "main", } # 若前端有签名,在这里按原顺序拼串后再哈希 # sign_str = f"{payload['act']}|{payload['username']}|{ts}" # payload["sign"] = hashlib.md5(sign_str.encode()).hexdigest() headers = { "Referer": cfg["HOME_URL"], "Origin": cfg["ORIGIN"], "Content-Type": "application/x-www-form-urlencoded", } resp = session.post(cfg["LOGIN_URL"], data=payload, headers=headers, timeout=10) return resp.json()

第五步,校验登录态并持久化。这一步别省,前面说过原因。

def verify_and_save(session, cfg): resp = session.get(cfg["PROFILE_URL"], timeout=10) data = resp.json() if data.get("code") != 0: raise RuntimeError(f"登录态校验失败: {data}") with open("cookies.json", "w", encoding="utf-8") as f: json.dump(requests.utils.dict_from_cookiejar(session.cookies), f, ensure_ascii=False, indent=2) print("登录成功,cookie 已保存")

这五步串起来大概一百二十行,逻辑非常直白。我把每一步都做成独立函数、独立打印日志,好处是失败时一眼就能看出断在哪一环——是没拿到设备标识,还是公钥解析错了,还是提交后被拒。这种"分段可观测"的设计,在调试阶段的价值远超所谓代码优雅。

6.3 运行验证与结果检查

跑起来之后,我会依次核对四件事。第一,看第一步打印的初始 cookie 列表,至少要有一个名字看起来像设备标识的项,如果列表是空的,说明首页请求没走通或者被重定向了,后面必然失败。第二,看公钥长度,正常是几百个字符,如果只有几十个,多半是取错了字段层级。第三,看登录响应的状态码字段,成功值和失败值分别是什么,提前记下来。第四,看校验接口有没有返回真实的用户信息。

这四步里任何一步不符合预期,我都会停下来单独调试,而不是硬着头皮往下跑。经验是:登录流程的错误会累积传递,前一步的小问题到后面会变成完全不相干的报错,让你往错误的方向排查。分段验证虽然啰嗦,但是最省时间的做法。

7. 排查实录:卡住我最久的几个问题

7.1 报错速查表

下面这些是我这次实际遇到过、或者排查过程中验证过的典型现象,整理成表方便对照。

现象或报错最可能的原因处理方式
公钥导入报格式错误缺少 PEM 头尾,或没有按 64 字符换行补全头尾并按行切分后重新导入
加密后长度和浏览器不一致填充模式用错,用了 OAEP改用 PKCS1 v1.5
加密抛长度超限异常明文超过密钥长度减 11 字节确认前端是否也做了截断或前置哈希
提交后提示密码错误拼接顺序反了,或编码不是 URL 安全 base64对照前端拼接表达式,逐字符核对
提示参数缺失漏了埋点之外的必填项用逐字段删减法确定哪些是真正必填
偶发成功、多数失败请求头不一致或缺少设备 cookie统一 header,严格按顺序走会话
提示操作频繁短时间重复登录停止自动重试,等待后再人工操作一次
登录接口返回成功但拿不到数据只是第一步通过,会话未真正建立用需登录的接口做二次验证

这张表里"偶发成功、多数失败"是最难查的一类,因为它不给明确报错。我的排查方法是:把成功那一次的完整请求(连同所有 header 和 cookie)导出,和失败那一次逐项对比,差异项就是嫌疑点。这个笨办法几乎百试百灵。

7.2 几个反直觉的细节

第一个细节:密文每次都不一样是正常的。PKCS#1 v1.5 填充会引入随机字节,同样的明文加密两次结果不同。我一开始以为这是 bug,反复检查了半天。正确的验证方式是比对长度,而不是比对内容。

第二个细节:cookie 有顺序。某些场景下同名不同 path 的 cookie 会同时存在,Session一般能处理对,但如果你手动拼 header,就可能带错了那个。这也是我不再手动拼 cookie 的原因之一。

第三个细节:参数里看着像乱码的字段,未必是签名。我一度花了两个小时去逆向一个看着很像哈希的字段,最后发现逐字段删减法一试,删掉它照样登录成功——它只是个统计埋点。所以先做减法,再做逆向,顺序别搞反。

实操心得:准备一份"对照清单",把每次成功的完整请求参数、header、cookie 快照存下来。等到某天突然跑不通了,第一件事不是改代码,而是拿新请求和旧快照逐项 diff。接口改版时,diff 出来的那一两处差异,就是全部答案。

第四个细节:间隔时间不是越短越好,也不是越长越好。太短触发风控,太长可能公钥过期。我实测下来,关键步骤之间留半秒到一秒,既接近真人操作节奏,也不会让预登录拿到的公钥失效。

这一套走下来,我最大的感受是:模拟登录的技术难度其实没有想象中高,真正消耗精力的是那些"看不见的约定"——请求的顺序、header 的一致性、参数的拼接顺序、编码的细节。这些东西没有任何文档会写,只能靠抓包、断点、对比一点点还原出来。而一旦你把这条链路完整走通一次,后面再遇到类似的登录场景,基本上半个下午就能复刻出七八成。至于那些需要绕过验证码、伪造设备指纹的路子,我的态度很明确:不碰。把边界守住了,这套东西用起来才踏实。

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

MATLAB流场可视化后处理:插值、箭头图、流线与涡识别

把一份算完的流场数据丢进 MATLAB&#xff0c;绝大多数人的第一版图都是这样的&#xff1a;满屏箭头糊成一团黑&#xff0c;流线断在半路&#xff0c;云图的色标跟旁边那张对不上&#xff0c;导出来的 PNG 一放大全是锯齿。这事儿跟水平没关系&#xff0c;纯粹是后处理这一步的…

作者头像 李华
网站建设 2026/10/31 5:20:48

高通Atheros WiFi芯片型号参数与选型指南

拆过几十块二手网卡、翻过上百页数据手册之后&#xff0c;我越来越觉得高通Atheros这条无线芯片产品线欠一份"说人话"的参数清单。它的型号跨度从早期的AR9280一路排到后来的QCN9074、WCN6855&#xff0c;中间横跨了802.11n、802.11ac、802.11ax三个世代&#xff0c;…

作者头像 李华
网站建设 2026/10/31 5:20:18

AI Agent Skills全解析:如何用SKILL.md让大模型稳定执行复杂任务

最近几个月&#xff0c;“skills”这个词在我身边的出现频率突然高了起来。如果你也在折腾AI Agent相关的项目&#xff0c;大概率已经碰过它——不是指“人要学会多少种技能”&#xff0c;而是一种把Agent能力封装成标准化模块的做法。简单说&#xff0c;skills就是给大模型写一…

作者头像 李华
网站建设 2026/10/31 5:20:14

系统化搭建个人技能体系:从技能地图到刻意练习的实操指南

直接开门见山&#xff1a;这个标题往小了说是一个人最值钱的资产&#xff0c;往大了说是职业赛道的底层操作系统。我见过太多人把"skills"理解成"会的东西"&#xff0c;却很少把它当成一套可以被设计、验证、迭代的产品。换句话说&#xff0c;技能不是你有…

作者头像 李华
网站建设 2026/10/31 5:19:41

电脑端替代手机扫描:批量图片转PDF与OCR四类方案

手机里那款几乎人人装过的扫描应用&#xff0c;确实把「拍照—裁边—转PDF」这条链路压成了一次点击&#xff0c;但一旦你把场景搬到电脑上&#xff0c;事情就变了味道&#xff1a;桌面上摊着四十多张合同照片&#xff0c;客户在微信那头催一个合并好的PDF&#xff0c;你打开电…

作者头像 李华
网站建设 2026/10/31 5:19:40

从技能树到刻意练习:一套可落地的个人能力管理体系

你有没有过这种感觉&#xff1a;东西学了不少&#xff0c;课囤了一大堆&#xff0c;真到用的时候却什么也拿不出来。或者反过来&#xff0c;每天都很忙&#xff0c;但这个月到底学到了什么、提升了什么&#xff0c;完全说不清楚。我前几年就是这个状态&#xff0c;后来花了不少…

作者头像 李华