给 站内游戏模块第三方网页内嵌企业服务端
使用的排行榜接入说明。接口域名:https://bbs.jnqj.net
使用金牛ID 企业账号登录本站。系统以金牛ID OAuth 返回的企业信息为准,并可叠加管理白名单。个人账号、邮箱注册账号无法通过。
管理员审核通过后发放 AppID + AppSecret(Secret 仅显示一次)。请保存在企业服务端,严禁写入前端 JS / 公开仓库。
每次请求携带时间戳 + Nonce + HMAC-SHA256 签名。服务端校验通过且会话用户为企业身份后才允许上报/上榜。
| 接口 | 方法 | 身份 | 说明 |
|---|---|---|---|
/api/game/leaderboard | GET | 公开读 | ?game=all 总分榜;?game=raiden 分游戏榜。榜单只统计企业用户 |
/api/game/session | GET | 否 | 登录态 + CSRF + is_enterprise / account_label |
/api/game/score | POST | 企业 | 提交成绩(进分榜,总分自动汇总) |
/api/game/coin-op | POST | 企业 | 开局投币,返回 fee/balance/round_ref |
/api/game/settle | POST | 企业 | 对局结算(羊/斗地主/极速狂飙) |
/api/game/points | GET | 登录 | 积分余额 |
为防止黑客与同行恶意刷榜/盗用,写接口按顺序执行以下门禁,任一失败立即拒绝并审计:
| 层 | 机制 | 说明 |
|---|---|---|
| 1 | AppID 存在且启用 | 未知 / 禁用 AppID 一律 403 |
| 2 | 时间戳窗口 | X-Timestamp 与服务器时间差 ≤ 300 秒 |
| 3 | Nonce 防重放 | 同一 Nonce 只能使用一次(600 秒内) |
| 4 | HMAC-SHA256 签名 | 用 AppSecret 对请求行 + Body 哈希签名 |
| 5 | IP 白名单(可选) | 企业可限制来源 IP / CIDR |
| 6 | 会话 / CSRF | 站内内嵌走 Cookie + CSRF;服务端走签名 |
| 7 | 企业身份双因子 | 金牛ID OAuth 企业字段 ∪ 管理白名单;且必须有金牛ID openid |
| 8 | 限流 + 审计 | 默认写 30 次/分、App 60 次/分;失败全部落 game_api_audit |
signString =
METHOD + "\n" +
PATH + "\n" +
timestamp + "\n" +
nonce + "\n" +
sha256_hex(body)
X-Signature = hex( HMAC-SHA256( app_secret , signString ) )
请求头:
X-App-Id: JNGA_XXXXXXXXXXXXXXXX
X-Timestamp: 1761300000
X-Nonce: a1b2c3d4e5f6g7h8
X-Signature: 6f1c...(小写 hex)
import hashlib, hmac, time, uuid, requests
APP_ID = "JNGA_..."
APP_SECRET = "..." # 仅服务端保存
BASE = "https://bbs.jnqj.net"
def signed_headers(method, path, body: bytes):
ts = str(int(time.time()))
nonce = uuid.uuid4().hex
body_hash = hashlib.sha256(body).hexdigest()
sign_string = "\n".join([method.upper(), path, ts, nonce, body_hash])
sig = hmac.new(APP_SECRET.encode(), sign_string.encode(), hashlib.sha256).hexdigest()
return {
"X-App-Id": APP_ID,
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Signature": sig,
}
# 上报成绩(需企业用户 Cookie 会话,或站内内嵌走 CSRF)
path = "/api/game/score"
body = b"game=raiden&score=12345&detail=wave%3D12"
h = signed_headers("POST", path, body)
h["Content-Type"] = "application/x-www-form-urlencoded"
# 浏览器内嵌时另附 Cookie + X-CSRF-TOKEN
r = requests.post(BASE + path, data=body, headers=h, cookies={"BBS_JNQJ_SESSION": "..."})
第三方网页内嵌游戏时,玩家必须以金牛ID 企业账号登录本站会话。脚本:
<script src="https://bbs.jnqj.net/game/js/auth.js"></script>
<button id="go">打一局雷电风暴</button>
<ol id="rank"></ol>
<script>
document.getElementById("go").onclick = async () => {
const s = await JNGameAuth.session();
if (!s || !s.logged_in) {
location.href = JNGameAuth.loginUrl("/game/raiden/");
return;
}
if (s.is_enterprise === false) {
alert("排行榜仅支持金牛ID企业用户接入,当前账号:" + (s.account_label || "不允许"));
return;
}
await JNGameAuth.coinOp("raiden");
// …你的游戏逻辑…
await JNGameAuth.submitScore({ game: "raiden", score: 1000, detail: "demo" });
const j = await JNGameAuth.leaderboard("raiden");
document.getElementById("rank").innerHTML = j.data.list.map(x =>
"<li>#" + x.rank + " " + x.nickname + " " + x.score + "</li>").join("");
};
</script>
is_enterprise(bool)、
account_type(email / jnqj_personal / jnqj_enterprise)、
account_label(人类可读)、
access_policy(政策说明)。
| 游戏 | 每局 |
|---|---|
| 极速狂飙 / AI / AI V2 | 250(胜退 250 + 净赚 250) |
| 雷电风暴 | 100 |
| 其余 | 10 |
| game | 说明 |
|---|---|
all | 全游戏总分(每款最佳分相加) |
raiden | 雷电风暴 |
sheep | 羊了个羊 |
doudizhu | 斗地主 |
makaimura / mojiecun | 魔界村 / 摩羯村 |
speed-rush / speed-rush-ai / speed-rush-ai-v2 | 极速狂飙三端 |
neon-racer | 霓虹飞车 |
| HTTP | 含义 |
|---|---|
| 401 | 未登录 / 缺少签名头 / 签名失败 / 时间戳过期 |
| 403 | 非金牛ID企业用户 / AppID 无效或禁用 / IP 不在白名单 |
| 409 | Nonce 重放 |
| 429 | 限流 |
| 422 | 参数不合法 |
user_id、盗用 Cookie、重放签名、刷分行为都会被拒绝并审计,情节严重将封禁 AppID。
# 列出应用
php scripts/game_api_admin.php apps
# 发放令牌(owner 必须是金牛ID企业用户)
php scripts/game_api_admin.php create-app \
--name="某某游戏" --owner=123 --ent="某某科技有限公司" \
--ips=203.0.113.10 --rpm=60
# 白名单强制开通企业身份(双因子之一)
php scripts/game_api_admin.php approve-user --uid=123
# 审计
php scripts/game_api_admin.php audit --limit=50