跳到主要内容
NextProxyNextProxy文档

认证与调用约定

access token 与 refresh token 的机制、Origin 校验要求、统一响应结构和分页约定。

客户 API 的通用规则都在这里:怎么拿令牌、写操作为什么会被 403 挡掉、错误结构长什么样、分页怎么传。具体端点见 端点清单

两种令牌

access tokenrefresh token
前缀at_rt_
长度前缀 + 43 位随机串前缀 + 43 位随机串
怎么传Authorization: Bearer at_...HttpOnly Cookie
默认有效期15 分钟12 小时
勾了"记住我"不变30 天

登录

POST/api/v1/auth/login
shell
curl -X POST 'https://api.example.com/api/v1/auth/login' \
  -H 'Origin: https://console.example.com' \
  -H 'Content-Type: application/json' \
  -c cookies.txt \
  -d '{"email":"you@example.com","password":"YOUR_PASSWORD","remember":true}'

-c cookies.txt 保存 refresh Cookie,后面刷新令牌要用。

开了两步验证的账号

密码验证通过后不会直接给 access token,而是返回一个 5 分钟有效的 MFA ticket,需要再调一次:

POST/api/v1/auth/login/two-factor
shell
curl -X POST 'https://api.example.com/api/v1/auth/login/two-factor' \
  -H 'Origin: https://console.example.com' \
  -H 'Content-Type: application/json' \
  -c cookies.txt \
  -d '{"ticket":"MFA_TICKET","code":"123456"}'

TOTP 的参数是标准的:SHA1、6 位、30 秒周期,验证窗口是当前周期 ±1(也就是允许 30 秒的时钟偏差)。

刷新令牌

access token 只有 15 分钟,长时间运行的程序需要定期刷新:

POST/api/v1/auth/refresh
shell
curl -X POST 'https://api.example.com/api/v1/auth/refresh' \
  -H 'Origin: https://console.example.com' \
  -b cookies.txt -c cookies.txt

-b 带上 refresh Cookie,-c 保存可能被轮转的新 Cookie。

写操作必须带 Origin

匹配是精确字符串比较,不是前缀或域名匹配。https://console.example.comhttps://console.example.com/ 不一样,http://https:// 也不一样。

Cookie 本身是 HttpOnly + SameSite=Lax;生产环境还强制 Secure__Host- 前缀。

统一响应结构

成功:

json
{ "data": { } }

失败:

json
{
  "error": {
    "code": "machine_readable_code",
    "message": "给人看的说明",
    "fields": { "FieldName": "validationTag" }
  }
}

code 是稳定的机器可读标识,程序里应该判断它而不是判断 messagemessage 会随文案调整变化)。fields 只在参数校验失败时出现。

分页

通用约定:

参数默认上限
page1
pageSize20100

响应:

json
{
  "data": {
    "items": [],
    "page": 1,
    "pageSize": 20,
    "totalItems": 137,
    "totalPages": 7
  }
}

API Key 和 access token 的分工

access tokenAPI Key
作用范围全部 /api/v1 客户接口 提取接口
归属网站用户单个代理子账号(一账号一个 Key)
传递方式Authorization: Bearer查询参数 ?apikey=
有效期15 分钟长期,直到被撤销
额外要求写操作需 Origin来源 IP 必须在白名单

两者不能互换。

一个可用的客户端封装

python
import threading
import time

import requests


class NextProxyClient:
    """带自动刷新的客户 API 客户端。

    access token 只有 15 分钟,所以在过期前主动换新,
    而不是等 401 再重试——后者会让每次过期都白费一个请求。
    """

    # 提前这么多秒刷新,留出网络往返的余量
    REFRESH_MARGIN = 120

    def __init__(self, base_url: str, origin: str) -> None:
        self._base = base_url.rstrip("/")
        self._origin = origin
        self._session = requests.Session()
        self._token: str | None = None
        self._expires_at = 0.0
        self._lock = threading.Lock()

    def login(self, email: str, password: str, *, remember: bool = True) -> None:
        payload = {"email": email, "password": password, "remember": remember}
        data = self._post("/api/v1/auth/login", payload)

        if "mfaTicket" in data:
            raise RuntimeError("账号启用了两步验证,请改用 login_with_totp")

        self._store_token(data)

    def login_with_totp(self, email: str, password: str, code: str) -> None:
        data = self._post(
            "/api/v1/auth/login",
            {"email": email, "password": password, "remember": True},
        )
        ticket = data.get("mfaTicket")
        if not ticket:
            self._store_token(data)
            return

        # MFA ticket 只有 5 分钟有效
        data = self._post(
            "/api/v1/auth/login/two-factor",
            {"ticket": ticket, "code": code},
        )
        self._store_token(data)

    def get(self, path: str, **params) -> dict:
        response = self._session.get(
            self._base + path,
            params=params or None,
            headers=self._headers(),
            timeout=30,
        )
        return self._unwrap(response)

    def post(self, path: str, payload: dict) -> dict:
        return self._post(path, payload, authenticated=True)

    def _post(self, path: str, payload: dict, *, authenticated: bool = False) -> dict:
        headers = self._headers() if authenticated else {"Origin": self._origin}
        response = self._session.post(
            self._base + path, json=payload, headers=headers, timeout=30,
        )
        return self._unwrap(response)

    def _headers(self) -> dict[str, str]:
        self._ensure_token()
        return {
            "Authorization": f"Bearer {self._token}",
            # 写操作必须带可信 Origin,否则 403 origin_rejected
            "Origin": self._origin,
        }

    def _ensure_token(self) -> None:
        with self._lock:
            if self._token and time.time() < self._expires_at - self.REFRESH_MARGIN:
                return
            response = self._session.post(
                self._base + "/api/v1/auth/refresh",
                headers={"Origin": self._origin},
                timeout=30,
            )
            self._store_token(self._unwrap(response))

    def _store_token(self, data: dict) -> None:
        self._token = data["accessToken"]
        # 服务端给的是秒数;没给就按默认 15 分钟算
        self._expires_at = time.time() + data.get("expiresIn", 900)

    @staticmethod
    def _unwrap(response: requests.Response) -> dict:
        payload = response.json()
        if response.ok:
            return payload.get("data", payload)

        error = payload.get("error", {})
        raise RuntimeError(
            f"HTTP {response.status_code} {error.get('code', 'unknown')}: "
            f"{error.get('message', response.text[:200])}"
        )


client = NextProxyClient("https://api.example.com", "https://console.example.com")
client.login("you@example.com", "YOUR_PASSWORD")
print(client.get("/api/v1/wallet"))

注销

POST/api/v1/auth/logout
POST/api/v1/auth/logout-all

logout 只失效当前这对令牌,logout-all 失效该用户的全部会话。改密码时会自动撤销旧会话。

常见问题

现象原因
403 origin_rejected写操作没带 Origin,或值不在可信列表里(精确匹配)
401 但 token 看起来是对的access token 过期了(只有 15 分钟)
刷新失败refresh Cookie 没带上(-b cookies.txt)或已过期
登录返回 mfaTicket 而不是 token账号开了两步验证,需要第二步
MFA ticket 失效只有 5 分钟,重新走登录流程
429触发风控频率限制,见 限额与配额

代理端口的错误另行判断

这里的 401 / 403 是 REST 接口鉴权或访问校验。代理 HTTP / CONNECT 的凭据错误使用 407,参数 400、额度 402、账号/策略 403、并发 429、内部 500、连接失败 502、暂不可用 503、连接超时 504;通过 X-NextProxy-Error 读取细分原因。SOCKS5 使用协议协商码。见 错误码对照

这篇解决你的问题了吗?