认证与调用约定
access token 与 refresh token 的机制、Origin 校验要求、统一响应结构和分页约定。
客户 API 的通用规则都在这里:怎么拿令牌、写操作为什么会被 403 挡掉、错误结构长什么样、分页怎么传。具体端点见 端点清单。
两种令牌
| access token | refresh token | |
|---|---|---|
| 前缀 | at_ | rt_ |
| 长度 | 前缀 + 43 位随机串 | 前缀 + 43 位随机串 |
| 怎么传 | Authorization: Bearer at_... | HttpOnly Cookie |
| 默认有效期 | 15 分钟 | 12 小时 |
| 勾了"记住我" | 不变 | 30 天 |
登录
/api/v1/auth/logincurl -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,需要再调一次:
/api/v1/auth/login/two-factorcurl -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 分钟,长时间运行的程序需要定期刷新:
/api/v1/auth/refreshcurl -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.com 和 https://console.example.com/ 不一样,http:// 和 https:// 也不一样。
Cookie 本身是 HttpOnly + SameSite=Lax;生产环境还强制 Secure 和 __Host- 前缀。
统一响应结构
成功:
{ "data": { } }
失败:
{
"error": {
"code": "machine_readable_code",
"message": "给人看的说明",
"fields": { "FieldName": "validationTag" }
}
}
code 是稳定的机器可读标识,程序里应该判断它而不是判断 message(message 会随文案调整变化)。fields 只在参数校验失败时出现。
分页
通用约定:
| 参数 | 默认 | 上限 |
|---|---|---|
page | 1 | 无 |
pageSize | 20 | 100 |
响应:
{
"data": {
"items": [],
"page": 1,
"pageSize": 20,
"totalItems": 137,
"totalPages": 7
}
}
API Key 和 access token 的分工
| access token | API Key | |
|---|---|---|
| 作用范围 | 全部 /api/v1 客户接口 | 仅 提取接口 |
| 归属 | 网站用户 | 单个代理子账号(一账号一个 Key) |
| 传递方式 | Authorization: Bearer | 查询参数 ?apikey= |
| 有效期 | 15 分钟 | 长期,直到被撤销 |
| 额外要求 | 写操作需 Origin | 来源 IP 必须在白名单 |
两者不能互换。
一个可用的客户端封装
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"))
注销
/api/v1/auth/logout/api/v1/auth/logout-alllogout 只失效当前这对令牌,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 使用协议协商码。见 错误码对照。