Z 通行证接入文档
服务地址:https://auth.zwkwww.top
版本:v1(2026-11)· 适用:网页 / 游戏 / 小程序 / 服务端
Z 通行证是一套「统一账号 + 排行榜」服务:接入方不用自己做注册体系,用户注册一次, 所有接入站点共用同一身份。本文档面向接入方开发者。
一、能力一览
| 能力 | 说明 |
|---|---|
| 邮箱注册 + 验证码 | 6 位验证码,10 分钟有效,只存哈希 |
| 令牌登录 | 登录签发 30 天令牌,请求头 X-LB-Token 传递 |
| 账号资料 | 昵称、打码邮箱、验证状态 |
| 找回密码 | 邮箱重置码,成功后全端下线 |
| 排行榜 | 多游戏分区、6 种时间窗口、HMAC 验签、MOD/开发者模式剔除 |
| 成就 | 平台级成就,随成绩提交返回 |
| 公开游戏注册表 | 列出所有接入的游戏/站点 |
管理后台的两步验证(TOTP)只影响站长登录后台,与接入方 API 无关。
二、接入前准备
1. 把站点加入跨源白名单
API 使用 X-LB-Token 请求头,属于 CORS 自定义头,浏览器端调用必须把站点 Origin 加入白名单
(服务器 /etc/zpass.env 的 ALLOWED_ORIGINS,逗号分隔,含协议与端口):
ALLOWED_ORIGINS=https://zwkwww.top,https://play.zwkwww.top,https://你的站点
改完 systemctl restart zpass。白名单之外的 Origin 不会收到 Access-Control-Allow-Origin,
浏览器会直接拦下请求。
2. 如果要上榜:注册游戏并获取签名密钥
联系站长在管理端登记游戏,得到:
game_id:如null-sectorsign_key:该游戏专属的 HMAC 密钥
GET /api/v1/games 可查看已登记的游戏(不含密钥)。
三、通用约定
请求
POST /api/v1/auth/login
Content-Type: application/json
X-LB-Token: <已登录时的令牌> # 仅需要身份的接口
- 所有请求体为 JSON;字符编码 UTF-8
- 令牌放在
X-LB-Token请求头,不用 Cookie(因此天然免疫 CSRF,也不需要处理同源策略里的 cookie)
响应
统一信封:
{ "ok": true, "data": { ... }, "message": "..." }
{ "ok": false, "error": "人话错误信息", "code": "可选机器码" }
状态码
| 码 | 含义 | 接入方处理建议 |
|---|---|---|
| 200 | 成功 | — |
| 400 | 参数/业务校验失败 | 直接展示 error |
| 401 | 未登录 / 令牌失效 | 清本地令牌,引导重新登录 |
| 403 | 无权限(如成绩被剔除) | 展示 error |
| 429 | 触发限流 | 读 Retry-After,倒计时后再试 |
| 503 | 服务暂不可用(如邮件未配置) | 稍后重试 |
CORS
Access-Control-Allow-Origin:仅回显白名单内的 Origin(不反射任意来源)Access-Control-Allow-Headers: Content-Type, X-LB-TokenAccess-Control-Allow-Methods: GET, POST, OPTIONS- 预检缓存 600 秒
四、账号 API
4.1 注册
POST /api/v1/auth/register
Content-Type: application/json
{ "email": "you@mail.com", "password": "至少8位含两类字符", "nickname": "榜上昵称" }
响应(已注册的邮箱也返回同样内容,防止探测邮箱是否存在;此时不会真的发信):
{ "ok": true, "message": "如果该邮箱可以注册,验证码已发送(10 分钟内有效)。",
"data": { "email": "you@mail.com" } }
- 密码规则:≥8 位,且包含字母/数字/符号中至少两类
- 昵称:≤24 字
- 验证码:6 位数字,10 分钟有效,最多试 5 次
4.2 重发验证码
POST /api/v1/auth/resend
{ "email": "you@mail.com" }
同样不区分「未注册 / 已验证」,统一返回 ok。
4.3 验证邮箱(自动登录)
POST /api/v1/auth/verify
{ "email": "you@mail.com", "code": "123456" }
{ "ok": true, "data": { "email": "y***@mail.com", "nickname": "虚空行者", "token": "长令牌" } }
4.4 登录
POST /api/v1/auth/login
{ "email": "you@mail.com", "password": "..." }
{ "ok": true, "data": { "email": "y***@mail.com", "nickname": "虚空行者", "token": "长令牌" } }
4.5 当前身份(接入方最常用)
GET /api/v1/auth/me
X-LB-Token: <token>
{ "ok": true, "data": {
"id": "1a2b3c4d5e6f7a8b9c0d1e2f", // 账户稳定 ID(邮箱 SHA-256 前 24 位)
"email": "y***@mail.com", // 打码,不含原文
"nickname": "虚空行者",
"verified": true
} }
id可用于接入方自己的数据表关联(写入用户记录、做配额计数等),不会泄露邮箱。
4.6 退出登录(吊销当前令牌)
POST /api/v1/auth/logout
X-LB-Token: <token>
4.7 其它账号接口
| 接口 | 请求体 | 说明 |
|---|---|---|
POST /auth/password |
{old_password, new_password} |
改密,成功后所有令牌失效 |
POST /auth/nickname |
{nickname} |
改昵称,榜单后续显示新昵称 |
POST /auth/delete |
{password} |
注销账号(不可恢复) |
POST /auth/reset |
{email} |
发送密码重置码(不区分邮箱是否存在) |
POST /auth/reset/confirm |
{email, code, password} |
凭重置码设置新密码,成功后全端下线 |
以上均需 X-LB-Token(reset 系列除外——那是未登录场景用的)。
五、排行榜 API
5.1 游戏列表(公开)
GET /api/v1/games
{ "ok": true, "data": {
"games": [ { "id": "null-sector", "name": "NULL SECTOR · 虚空扇区",
"desc": "波次生存射击 · 排行榜", "url": "https://play.zwkwww.top" } ],
"default": "null-sector"
} }
5.2 查询榜单(公开)
GET /api/v1/lb/leaderboard?game=null-sector&period=season&limit=20
X-LB-Token: <可选:带上时会额外返回你自己的名次 mine>
period 取值:all(全部)、season(本赛季)、lastseason(往季)、
today、week、month;limit 1–100,默认 50。
{ "ok": true, "data": {
"period": "season", "game": "null-sector",
"rows": [ { "rank": 1, "nickname": "虚空行者", "email": "y***@mail.com",
"score": 7777, "wave": 12, "version": "3.5.1", "ts": "2026-11-03 20:43:12" } ],
"mine": { "score": 2400, "wave": 6, "rank": 5 } // 仅在带有效令牌时出现
} }
赛季口径:自然季度(1/4/7/10 月 1 日 00:00 起算)。
5.3 提交成绩(需登录 + 签名)
POST /api/v1/lb/score
X-LB-Token: <token>
Content-Type: application/json
{
"game_id": "null-sector",
"score": 4321,
"wave": 4,
"build": "STABLE",
"version": "3.5.1",
"mode": "",
"dev_used": false,
"modded": false,
"ts": 1793035357,
"nonce": "3f9c2a7b8e1d4c60", // 建议 16 字节随机数的 hex
"sign": "<HMAC-SHA256 十六进制>",
"meta": "{\"kills\":37}" // 可选,≤512 字符,参与签名
}
成功响应:
{ "ok": true, "message": "成绩已记录。", "data": {
"rank": 4,
"personal_best": true,
"best": 4321,
"nickname": "虚空行者",
"achievements": [ { "id": "first_blood", "name": "首战告捷", "icon": "◆" } ]
} }
校验规则(任一不满足返回 403):
build必须为STABLE(MOD 构建不可上榜)mode仅允许""或"campaign"dev_used/modded为 true → 直接拒绝score ≥ 0、wave 1–999、score ≤ wave × 100000ts与服务器时间相差 ≤ 600 秒nonce非空、≤64 字符、仅[A-Za-z0-9_-]- 同一账号同一游戏 60 秒冷却
六、签名算法(v2)
负载字符串(用 | 连接,顺序固定):
v2|game_id|score|wave|build|version|mode|dev|mod|ts|nonce|meta
score/wave/ts用十进制字符串dev/mod用1/0meta原样参与(空字符串就留空)
签名 = HMAC-SHA256(key=sign_key, message=负载),输出小写十六进制。
JavaScript(浏览器)
async function signScore(signKey, p) {
const payload = ["v2", p.game_id, p.score, p.wave, p.build, p.version, p.mode,
p.dev ? "1" : "0", p.mod ? "1" : "0", p.ts, p.nonce, p.meta || ""].join("|");
const enc = new TextEncoder();
const key = await crypto.subtle.importKey("raw", enc.encode(signKey),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(payload));
return [...new Uint8Array(sig)].map(b => b.toString(16).padStart(2, "0")).join("");
}
// nonce 一定要用强随机
function nonce() {
const buf = new Uint8Array(16);
crypto.getRandomValues(buf);
return [...buf].map(b => b.toString(16).padStart(2, "0")).join("");
}
Python(服务端)
import hashlib, hmac, time, secrets
def sign_score(key: str, *, game_id: str, score: int, wave: int, build: str,
version: str, mode: str, dev: bool, mod: bool,
ts: int, nonce: str, meta: str = "") -> str:
payload = "|".join(["v2", game_id, str(score), str(wave), build, version, mode,
"1" if dev else "0", "1" if mod else "0",
str(ts), nonce, meta])
return hmac.new(key.encode(), payload.encode(), hashlib.sha256).hexdigest()
ts = int(time.time())
nonce = secrets.token_hex(16)
sig = sign_score(SIGN_KEY, game_id="null-sector", score=4321, wave=4,
build="STABLE", version="3.5.1", mode="", dev=False, mod=False,
ts=ts, nonce=nonce)
签名密钥会随前端代码下发,只能防「随手改分数」,不能防逆向。 真正的强校验应把签名放到自己的服务端:浏览器只提交结果,服务端签名后再转发。
七、推荐接入姿势
7.1 纯前端(游戏类)
用户点击登录 → POST /auth/login 拿 token → localStorage 存起来
↓
每次请求带 X-LB-Token → /auth/me 取昵称/ID → 渲染界面
↓
对局结束 → 本地生成 nonce/ts → HMAC 签名 → POST /lb/score
参考实现:null-sector/null-sector-lb.js(含登录浮层、签名、上报、榜单 UI)。
7.2 服务端接入(更安全)
浏览器把令牌交给你的后端,由后端调用 GET /auth/me 校验(跨域不经过浏览器,
也不暴露令牌给第三方脚本):
import json, urllib.request
def verify_token(token: str) -> dict | None:
req = urllib.request.Request(
"https://auth.zwkwww.top/api/v1/auth/me",
headers={"X-LB-Token": token, "Accept": "application/json"})
try:
with urllib.request.urlopen(req, timeout=5) as resp:
data = json.loads(resp.read())
return data.get("data") if data.get("ok") else None
except Exception:
return None
参考实现:personal-site/app/core/zpass.py(含 60 秒缓存与正负分桶)。
7.3 只做登录、不做排行榜
只需要 /auth/register、/auth/verify、/auth/login、/auth/me、/auth/logout
五个接口,完全不用管签名与 sign_key。
八、限流表(按 IP)
| 接口 | 额度 | 窗口 |
|---|---|---|
POST /auth/register |
5 次 | 1 小时 |
POST /auth/resend |
10 次 | 1 小时 |
POST /auth/verify |
10 次 | 10 分钟 |
POST /auth/login |
10 次 | 5 分钟 |
POST /auth/reset |
10 次 | 1 小时 |
POST /auth/reset/confirm |
10 次 | 10 分钟 |
POST /lb/score |
60 次 | 1 小时 |
超限返回 429 + Retry-After(秒)。服务端用 Redis 做跨进程计数;
另外:验证码 60 秒重发冷却、同一账号成绩提交 60 秒冷却、密码错误 5 次锁定
(接入方后台的登录锁定是本地站点的,与本服务无关)。
九、本地联调
cd zpass
python -m venv .venv && .venv/bin/pip install -r requirements.txt
SECRET_KEY=dev LB_SIGN_KEY=dev-key COOKIE_SECURE=0 python app.py # :8100
接入方把 Base URL 指向 http://127.0.0.1:8100;本地测试可把站点 Origin 加进
ALLOWED_ORIGINS。注册收不到邮件时,用仓库里的 tools/smtp_sink.py 抓验证码:
python tools/smtp_sink.py --port 1025 --out codes.jsonl
# zpass 侧:SMTP_HOST=127.0.0.1 SMTP_PORT=1025 SMTP_SSL=0
十、常见问题
Q:浏览器报 CORS 错误?
A:Origin 没进白名单(注意协议、端口、尾斜杠都要完全一致);或请求带了
X-LB-Token 却只做了简单请求——浏览器会先发 OPTIONS 预检,确认服务端返回 204 且
允许该头。
Q:401 提示「未登录或令牌已过期」? A:令牌 30 天到期、改密/重置密码/注销都会让旧令牌失效;清掉本地令牌重新登录。
Q:403「成绩超出合理性范围」?
A:score > wave × 100000。高分游戏请按实际波次提交,或联系站长调整该游戏的上限。
Q:403「请求数据校验失败」?
A:签名不匹配。核对:负载字段顺序、dev/mod 是 1/0 而非 true/false、
meta 与提交的完全一致(包括空格)、sign_key 是否是该游戏的、输出是否小写 hex。
Q:429 之后多久能重试?
A:读响应头 Retry-After;登录类建议指数退避。
Q:能改成 OAuth / 第三方登录吗? A:当前是自定义令牌接口,不是 OAuth2/OIDC;接入方只需按本文档调用即可。
十一、参考实现
| 文件 | 说明 |
|---|---|
null-sector/null-sector-lb.js |
游戏端完整接入(登录浮层、签名上报、榜单页签、成就) |
personal-site/app/core/zpass.py |
服务端令牌校验(缓存、分桶、失效处理) |
personal-site/app/api/messages.py |
「登录后才能留言」的服务端门禁写法 |
zpass/api.py |
全部接口的服务端实现(可直接对照字段) |
zpass/tests/ |
账号、找回、排行榜的契约测试 |