Integrations

CaptchaAI API 密钥管理的 Vault 集成

密钥管理最难的不是加密,而是回答两个问题:这把 API 密钥有几份拷贝,最近是谁读走的。把 CaptchaAI 的 API 密钥交给 HashiCorp Vault,答案就是一份密文加一条审计记录:worker 运行时读取,轮换只需更新 Vault 里的值,不改代码也不重启。

.env 与 Vault 的差别

.env 交给 Vault
每台机器一份拷贝 集中加密,一份权威值
靠 IM 传递 认证后 API 读取
无访问记录 每次读取留痕
轮换要逐台重启 更新一次自动生效

一个国内团队的场景

某比价团队 3 台采集机加一组 K8s worker 共用一把密钥,识别 reCAPTCHA v2 和 Cloudflare Turnstile,套餐 ADVANCE($90/月,50 线程)。上次换密钥花了一下午:没人记得凌晨的对账任务里还硬编码了一份。

接入 Vault 后二十分钟收工,读取日志也便于满足网络安全法、数据安全法的审计要求。

动手前的准备

  • Vault 服务端(自建或 HCP Vault)与 CLI 权限
  • CaptchaAI API 密钥
  • Python 3.8+ 或 Node.js 18+

装 SDK 走国内镜像更快:pip install hvac -i https://pypi.tuna.tsinghua.edu.cn/simple

第一步:密钥写进 KV v2

启用 KV v2,密钥写到 secret/captchaai

# Enable the KV secrets engine (if not already enabled)
vault secrets enable -path=secret kv-v2

# Store the CaptchaAI API key
vault kv put secret/captchaai api_key="YOUR_API_KEY"

# Verify
vault kv get secret/captchaai

能读回写入的值就算通过。生产按 prodstaging 拆路径。

第二步:给 worker 只读策略

worker 只读,权限按最小给:

# captcha-worker-policy.hcl
path "secret/data/captchaai" {
  capabilities = ["read"]
}

path "secret/metadata/captchaai" {
  capabilities = ["read"]
}

应用策略:

vault policy write captcha-worker captcha-worker-policy.hcl

两条路径都要写,漏掉 metadata 会让版本查询报 403。

第三步:Python worker 取密钥

启动取一次,之后按周期回源。识别流程不变:in.php 提交,res.php 轮询。

# vault_solver.py
import os
import time
import hvac
import requests

# Connect to Vault
vault_client = hvac.Client(
    url=os.environ.get("VAULT_ADDR", "http://127.0.0.1:8200"),
    token=os.environ.get("VAULT_TOKEN"),
)

def get_api_key():
    """Retrieve CaptchaAI API key from Vault."""
    secret = vault_client.secrets.kv.v2.read_secret_version(
        path="captchaai",
        mount_point="secret",
    )
    return secret["data"]["data"]["api_key"]

class CaptchaSolver:
    """CAPTCHA solver with Vault-managed credentials."""

    def __init__(self):
        self.api_key = get_api_key()
        self.session = requests.Session()
        self._key_fetched_at = time.time()
        self._key_refresh_interval = 3600  # Re-fetch key hourly

    def _refresh_key_if_needed(self):
        """Periodically refresh the key from Vault."""
        if time.time() - self._key_fetched_at > self._key_refresh_interval:
            self.api_key = get_api_key()
            self._key_fetched_at = time.time()

    def solve(self, sitekey, pageurl):
        """Solve reCAPTCHA v2 using Vault-managed key."""
        self._refresh_key_if_needed()

        # Submit
        resp = self.session.get("https://ocr.captchaai.com/in.php", params={
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": pageurl,
            "json": "1",
        })
        result = resp.json()

        if result.get("status") != 1:
            raise Exception(f"Submit failed: {result.get('request')}")

        task_id = result["request"]
        time.sleep(15)

        for _ in range(25):
            poll = self.session.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
                "json": "1",
            })
            poll_result = poll.json()

            if poll_result.get("status") == 1:
                return poll_result["request"]
            if poll_result.get("request") != "CAPCHA_NOT_READY":
                raise Exception(f"Error: {poll_result.get('request')}")

            time.sleep(5)

        raise Exception("Timeout")

# Usage
solver = CaptchaSolver()
token = solver.solve(
    "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
    "https://www.google.com/recaptcha/api2/demo"
)
print(f"Token: {token[:30]}...")

_key_refresh_interval 决定轮换多久生效,示例为 1 小时。CAPCHA_NOT_READY 是正常状态,别当错误抛。

第四步:Node.js 等价写法

不用装 Vault SDK,axios 直调接口即可。响应是 data.data.data.api_key 三层嵌套:

// vault_solver.js
const axios = require('axios');

const VAULT_ADDR = process.env.VAULT_ADDR || 'http://127.0.0.1:8200';
const VAULT_TOKEN = process.env.VAULT_TOKEN;

async function getApiKey() {
  const resp = await axios.get(
    `${VAULT_ADDR}/v1/secret/data/captchaai`,
    { headers: { 'X-Vault-Token': VAULT_TOKEN } }
  );
  return resp.data.data.data.api_key;
}

class CaptchaSolver {
  constructor() {
    this.apiKey = null;
    this.keyFetchedAt = 0;
    this.refreshInterval = 3600000; // 1 hour
  }

  async init() {
    this.apiKey = await getApiKey();
    this.keyFetchedAt = Date.now();
  }

  async refreshKeyIfNeeded() {
    if (Date.now() - this.keyFetchedAt > this.refreshInterval) {
      this.apiKey = await getApiKey();
      this.keyFetchedAt = Date.now();
    }
  }

  async solve(sitekey, pageurl) {
    await this.refreshKeyIfNeeded();

    const submit = await axios.get('https://ocr.captchaai.com/in.php', {
      params: {
        key: this.apiKey, method: 'userrecaptcha',
        googlekey: sitekey, pageurl, json: '1',
      },
    });

    if (submit.data.status !== 1) throw new Error(submit.data.request);
    const taskId = submit.data.request;

    await new Promise(r => setTimeout(r, 15000));

    for (let i = 0; i < 25; i++) {
      const poll = await axios.get('https://ocr.captchaai.com/res.php', {
        params: { key: this.apiKey, action: 'get', id: taskId, json: '1' },
      });

      if (poll.data.status === 1) return poll.data.request;
      if (poll.data.request !== 'CAPCHA_NOT_READY') throw new Error(poll.data.request);
      await new Promise(r => setTimeout(r, 5000));
    }
    throw new Error('Timeout');
  }
}

(async () => {
  const solver = new CaptchaSolver();
  await solver.init();

  const token = await solver.solve(
    '6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-',
    'https://www.google.com/recaptcha/api2/demo'
  );
  console.log(`Token: ${token.slice(0, 30)}...`);
})();

第五步:生产改用 AppRole

认证方式 适用场景 需要配置
Token 开发、CI/CD VAULT_TOKEN
AppRole 生产服务 Role ID + Secret ID
Kubernetes 集群内 worker Service Account JWT

示例里的 VAULT_TOKEN 调试够用,但不会自动过期;生产改用 AppRole,凭证可过期、可撤销,改动只在初始化这几行:

# AppRole authentication — no static token needed
vault_client = hvac.Client(url=os.environ["VAULT_ADDR"])
vault_client.auth.approle.login(
    role_id=os.environ["VAULT_ROLE_ID"],
    secret_id=os.environ["VAULT_SECRET_ID"],
)

# Now read the secret
secret = vault_client.secrets.kv.v2.read_secret_version(path="captchaai")
api_key = secret["data"]["data"]["api_key"]

轮换密钥:四步,零发版

最后一步别抢跑:提前吊销会让没刷新的 worker 报 ERROR_WRONG_USER_KEY

  1. 在 CaptchaAI 控制台生成新密钥
  2. 写回 Vault:vault kv put secret/captchaai api_key="NEW_KEY"
  3. worker 下个刷新周期取到新值
  4. 全部刷新后再吊销旧密钥

报错速查

按现象对号入座:

现象 原因 处理方式
403 Forbidden 策略未给读权限 核对两条路径
VAULT_TOKEN 失效 超过 TTL 改用 AppRole
ERROR_WRONG_USER_KEY 值带空格或换行 去掉首尾空白

常见问题

Vault 连不上,识别任务会停吗?

不会,前提是做了内存缓存。密钥留在进程里,到刷新周期才回源;Vault 不可达时沿用缓存并告警。

审计日志能查到是谁读走了密钥吗?

可以。Vault 的 audit device 会记录每次读取的身份、时间和路径,按服务拆分 AppRole 就能定位到具体 worker。

刷新间隔设多久合适?

示例的 1 小时适合常驻 worker。想让轮换十几分钟内生效就调到 5–10 分钟,间隔越短压力越大。

换密钥会影响计费吗?

不会。CaptchaAI 按并发线程计费,BASIC($15/月,5 线程)到 VIP-3($7,500/月,5,000 线程),每线程识别次数不限。

下一步

先跑通识别,再把密钥搬进 Vault——注册 CaptchaAI 领取 API 密钥

该文章已禁用评论。