如何在 Postman 里识别验证码并轮询获取 token

solve captcha in postman - How to Solve CAPTCHA in Postman and Poll for the Token

你不用写一行应用代码,就能在 Postman 里识别验证码。这个 API 兼容 2captcha,而且跑在你自己的机器上,所以只要两个请求:把任务 POST 到 /in.php 并保存 ID,然后轮询 /res.php 直到答案返回。一段简短的 post-response 脚本能把第二个请求变成循环,一个集合变量在两者之间传递 ID。本文给出确切的字段、两段脚本,以及那个让循环不会永远跑下去的设置。

你需要什么

  • Postman 桌面应用。它直接和 127.0.0.1 通信,不需要任何 agent 参与。网页版在下文另有说明,它还需要多一个部件
  • CapSkip 正在运行并且可以访问。Local 模式监听 127.0.0.1 的 8080 端口,适合同一台机器上的客户端;Server 模式监听你的内网或公网 IP,这样笔记本、同事或托管 runner 都能访问到它。两种模式都配置在 连接设置
  • 一个 sitekey,以及要识别的页面 URL

别的都不需要。不用 SDK,不用依赖,也不用账号。key 校验默认是关闭的,所以任何非空字符串都能当作 key 的值;而什么都不发,返回的会是 ERROR_WRONG_USER_KEY,不是识别结果。

先设置三个集合变量

把它们放在集合上,而不是放在环境里。集合变量会随导出一起走,所以别人导入之后整套东西照样能用。

变量初始值原因
baseUrlhttp://127.0.0.1:8080识别工具搬到服务器上时,只需要改这一处
apiKeycapskip任何非空字符串。校验默认是关闭的
captchaId提交脚本写入它,轮询请求读取它

请求 1:提交任务

一个 POST 请求,发往 {{baseUrl}}/in.php,请求体用表单。选择 x-www-form-urlencoded,而不是原始 JSON:这个 API 读取的是表单字段。

key{{apiKey}}
methoduserrecaptcha
googlekeyYOUR_SITEKEY
pageurlhttps://example.com/page-with-recaptcha
json1

json=1 这个字段在 Postman 里比在终端里更重要。没有它,响应就是纯字符串 OK,后面跟一个竖线和 ID,你还得自己动手切分。加上它,你拿到的是一个对象,格式化显示也能正常工作。

把这段加到 Scripts 标签页的 Post-response 下面。旧版 Postman 把同一个标签页叫作 Tests。

// Post-response script on the submit request.
const body = pm.response.json();

pm.test('task accepted', function () {
    pm.expect(body.status).to.eql(1);
});

// Hand the ID to the polling request.
pm.collectionVariables.set('captchaId', body.request);

对每一种验证码类型,响应的结构都是一样的。任务被接受时 status 为 1,而你真正关心的值始终在 request 字段里。

{"status": 1, "request": "2122988149"}

注意参数名。reCAPTCHA 用 googlekey,Turnstile 用 sitekey,发错这个参数是收到 ERROR_GOOGLEKEY 响应最常见的原因。 Turnstile 识别工具 页面列出了挑战页面所需的完整字段,这类页面还需要从页面上抓取 cData 和 chlPageData 的值。

同一个请求,换成其他八种类型

只有提交请求会变。五个 method 值就覆盖了 CapSkip 支持的每一种类型,各种变体只是同一个表单体上的额外字段,而不是另外的端点。复制一份 Request 1,套用最后一列,请求的其余部分保持原样。

类型把 method 设为然后改动请求体
图片验证码,文件上传method = post把请求体切换成 form-data,并把图片作为 file 附上
图片验证码,base64method = base64去掉 googlekey 和 pageurl,用 body 发送编码后的图片
reCAPTCHA v2 复选框method = userrecaptcha无需改动。上面演示的就是这个请求
reCAPTCHA v2 Invisiblemethod = userrecaptcha增加 invisible,值为 1
reCAPTCHA Enterprisemethod = userrecaptcha增加 enterprise,值为 1
reCAPTCHA v3method = userrecaptcha增加 version 并设为 v3,再加一个 action
Turnstile 小组件method = turnstile把 googlekey 改名为 sitekey
Turnstile 挑战页面method = turnstile把 googlekey 改名为 sitekey,然后加上 data 和 pagedata
极验 v3method = geetest用 gt 和 challenge 代替 googlekey 发送

其中有两种在 Postman 里要格外注意。上传图片是唯一不能用 x-www-form-urlencoded 的,因为文件需要 multipart 请求体,所以要把那一个请求切换成 form-data。极验则有时间限制:它的挑战值大约一分钟就会过期,所以要在发送之前马上去取,而不是复用之前保存的那个。

轮询请求永远不变。这正是把这一切做成一个集合的理由:一个 Request 2 就能服务列表里的每一种类型,因为答案回来的形状始终一样。

请求 2:轮询直到答案返回

一个 POST 请求,发往 {{baseUrl}}/res.php,同样使用表单请求体。

key{{apiKey}}
actionget
id{{captchaId}}
json1

任务还在运行时,这里会返回 CAPCHA_NOT_READY,拼写就是这样,少的那个字母也照原样。它是状态,不是错误,唯一正确的应对就是再问一次。

// Post-response script on the polling request.
const body = pm.response.json();
const tries = Number(pm.collectionVariables.get('tries') || 0);

if (body.request === 'CAPCHA_NOT_READY' && tries < 20) {
    // Run this same request again.
    pm.collectionVariables.set('tries', tries + 1);
    pm.execution.setNextRequest(pm.info.requestId);
} else {
    pm.collectionVariables.set('captchaToken', body.request);
    pm.collectionVariables.set('tries', 0);
    pm.execution.setNextRequest(null);
}

那段脚本里有三点值得拎出来说,因为每一点都够你搭进去一个下午。

循环只在 Collection Runner 里跑。 Postman 自己的文档写得很明确:发送单个请求时 setNextRequest 不起作用,所以在轮询请求上点 Send 只会发一次,脚本看起来像是死了。请运行整个集合、Postman CLI 或 Newman。

计数器不是可选项。 没有上限,一个永远解不出来的任务就会一直循环,直到你手动停掉这次运行。二十次尝试、每次间隔五秒就是一百多秒的等待,足以从容覆盖 reCAPTCHA v2 任务所需的 15 到 20 秒。

用 ID 来引用请求。 传入 pm.info.requestId 会让循环指向当前正在运行的这个请求,这样以后重命名也不会悄悄把链路弄断。

给 runner 设个延迟,否则你会把端点打爆

上面这段脚本会以 runner 能跑到的最快速度循环,而去轮询一个才跑了两秒的任务纯属白费力气。Collection Runner 的运行配置里有一个 Delay 字段,单位是毫秒,会在每个请求之前生效。把它设成 5000。

第一次轮询之前该等多久,取决于你提交的是什么:

类型在此之前不会就绪
图片验证码1 秒
reCAPTCHA v215 到 20 秒
reCAPTCHA v310 到 15 秒
极验 v3约 5 秒

在 Newman 里,同一个设置是一个命令行参数,所以在应用里跑得通的集合,放到 CI 里不用改也能跑。

# npm install -g newman
newman run captcha.postman_collection.json --delay-request 5000

改读纯文本响应

去掉 json=1,响应体就会以文本形式返回,偶尔这正是你想要的。两个小助手就能覆盖它。

// Plain text mode: OK|2122988149
const id = pm.response.text().split('|')[1];

// Turnstile also returns the user agent, as a header.
const ua = pm.response.headers.get('X-Turnstile-User-Agent');

那个请求头不是摆设。Cloudflare 会把 Turnstile 的 token 和生成它的浏览器指纹绑定,所以提交 token 时必须带上同一个 user agent,否则 token 本身完全有效,站点却照样拒绝。

还有一条规则专门坑 Postman 用户,因为这个应用实在太容易让人点两次 Send: 结果只能读取一次。同一个 ID 的第二次读取会返回空,看起来和识别失败一模一样。第一次读到时就把 token 存进变量。

agent 的问题,以及把 Postman 指向服务器

如果你用的是 Postman 网页版而不是桌面应用,请求会经过一个 agent,而选哪个 agent 决定了 127.0.0.1 到底能不能访问到。Cloud Agent 跑在 Postman 自己的基础设施里,够不到私有网络上的任何东西。Desktop Agent 跑在你的机器上,就够得到。

你怎么运行 Postman它能访问本地的识别工具吗?
桌面应用可以,不需要 agent
网页版加 Desktop Agent可以,agent 会经由你的机器转发
网页版加 Cloud Agent不行,它看不到私有网络

Server 模式会改变这笔账。把 CapSkip 跑在一台监听内网或公网 IP 的机器上,把 baseUrl 变量改成指向它,集合里的每个请求就都跟着走了。别的什么都不用变,因为动的只是地址。

# The collection variable is the only edit.
baseUrl = http://YOUR_SERVER_IP:8080

建议使用静态公网 IP,具体步骤见 连接设置。Server 模式仍然是你自己的硬件,仍然不计量:它改变的是识别工具监听的位置,而不是它归谁所有。

常见错误及其含义

你所看到的原因修复
ERROR_WRONG_USER_KEYkey 字段收到时是空的,因为 {{apiKey}} 没有解析出来把 apiKey 定义在集合上,而不是放在一个你必须记得去选中的环境里
ERROR_WRONG_METHODmethod 或 action 的值拼错了这两个都是表单字段,不是 HTTP 方法:提交请求带一个 method 字段,轮询请求要把 action 字段设成 get
ERROR_GOOGLEKEY把 Turnstile 的 sitekey 发给了 userrecaptcha让 method 和字段名对上
ERROR_PAGEURL页面 URL 没有协议头,或者被截断了带上 https,并且用表单请求体而不是查询字符串
响应体是空的那个 ID 已经被读过一次了第一次读到就把 token 存下来;要新的就重新提交
无法发送请求,连接被拒绝那个地址上没有任何东西在监听启动识别工具,或者检查是不是选中了 Desktop Agent
脚本跑了,但没有循环你点的是 Send,而不是运行整个集合setNextRequest 只在集合运行时有效

API 可能返回的每个错误码的确切文案,都列在 API 文档.

常见问题

我能用一个请求把整件事做完吗?

技术上可以,在 pre-request 脚本里用 pm.sendRequest,但很少值得这么做。那个沙箱是为短脚本设计的,阻塞式轮询会让请求在等待期间看起来像卡死了,没有任何反馈。两个请求加上 runner 能让你看到每一次尝试,而这正是你选择待在 Postman 里而不是写代码的理由。

这套东西能通过 Newman 在 CI 里跑吗?

可以。setNextRequest 在 Newman 和 Postman CLI 里的表现和在应用里完全一样,所以导出的集合不用改就能跑。唯一要解决的是可达性:托管 runner 有自己的回环地址,所以识别工具需要以 Server 模式跑在 runner 能路由到的地址上。

对我能排多少个任务有速率限制吗?

没有。想提交多少任务就提交多少,每个 ID 各自轮询。活儿是在你自己的硬件上干的,而不是在别人的共享队列里,所以上限是你的机器处理得多快,而不是某个配额或余额。

什么时候该从 Postman 毕业

Postman 适合用来验证这个 API 能用、探索一种新的验证码类型,以及给同事一份他们导入就能跑的东西。它不适合生产环境,主要是因为轮询:runner 每一次都要把固定的延迟等满,而 官方 SDK 从 250 毫秒开始轮询并逐步退避,所以识别得快就返回得快。它们还会把错误字符串变成带类型的异常。

在 shell 脚本里做同样的两次调用,请参见 cURL 完整教程。无论哪种方式,调用都只发往你自己的机器,而这正是让一个 无限量验证码识别工具 值得被集合指向的原因:你可以在把字段调对的过程中把集合跑上二十遍,除了自己的 CPU 之外不花任何成本。