Z Z 通行证 · 接入文档
Z-PASSPORT · API v1
auth.zwkwww.top ↗

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. 如果要上榜:注册游戏并获取签名密钥

联系站长在管理端登记游戏,得到:

GET /api/v1/games 可查看已登记的游戏(不含密钥)。


三、通用约定

请求

POST /api/v1/auth/login
Content-Type: application/json
X-LB-Token: <已登录时的令牌>   # 仅需要身份的接口

响应

统一信封:

{ "ok": true,  "data": { ... }, "message": "..." }
{ "ok": false, "error": "人话错误信息", "code": "可选机器码" }

状态码

码 含义 接入方处理建议
200 成功 —
400 参数/业务校验失败 直接展示 error
401 未登录 / 令牌失效 清本地令牌,引导重新登录
403 无权限(如成绩被剔除) 展示 error
429 触发限流 读 Retry-After,倒计时后再试
503 服务暂不可用(如邮件未配置) 稍后重试

CORS


四、账号 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" } }

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):


六、签名算法(v2)

负载字符串(用 | 连接,顺序固定):

v2|game_id|score|wave|build|version|mode|dev|mod|ts|nonce|meta

签名 = 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/ 账号、找回、排行榜的契约测试