外观
快速开始
本页以最短路径带你跑通 Notanote 第三方接入全流程:从申请凭证到拉取玩家存档,一共四个步骤。读完本页后,你将得到一个可运行的 Node.js 示例,并了解每一步在整体流程中的位置。
整体流程概览
Notanote 第三方接入采用 OAuth 风格的网页授权流程,共四步:
- 换取临时会话 — 你的服务端用应用凭证换取一个 5 分钟有效的
auth-session - 引导玩家授权 — 将玩家浏览器重定向到 Notanote 授权页
- 接收回调 / 获取 Token — 玩家确认授权后,通过回调地址接收
name与access_token,或从页面展示的授权码中解析得到 - 拉取存档 — 用
access_token调用存档接口,获取玩家存档文件
两种回调模式
Notanote 支持两种回调方式:
- URL 回调模式:
callback填你的回调地址,授权后浏览器自动跳转,携带access_token - 展示模式(
callback="show"):授权后直接在页面上展示授权码(格式为{name}.{token}),用户复制后发给第三方。适用于 QQ 机器人等无法托管回调地址的场景
URL 回调模式:
mermaid
sequenceDiagram
participant App as 你的服务端
participant Nano as Notanote API
participant User as 玩家浏览器
App->>Nano: ① POST /webauth/get-session(换取 auth-session)
Nano-->>App: 返回 auth-session(5 分钟有效)
App->>User: ② 302 重定向到 Notanote 授权页
User->>Nano: ③ 玩家登录并确认授权
Nano-->>User: ④ 返回 redirect_url
User->>App: ⑤ 浏览器跳转 callback?name=&access_token=&state=
App->>App: ⑥ 校验 state(防 CSRF)
App->>Nano: ⑦ 用 access_token 调用第三方接口
Nano-->>App: ⑧ 返回存档下载链接展示模式(callback="show"):
mermaid
sequenceDiagram
participant App as 你的服务端
participant Nano as Notanote API
participant User as 玩家浏览器
participant 3rd as 第三方接入者
App->>Nano: ① POST /webauth/get-session(callback="show")
Nano-->>App: 返回 auth-session(5 分钟有效)
App->>User: ② 302 重定向到 Notanote 授权页
User->>Nano: ③ 玩家登录并确认授权
Nano-->>User: ④ 页面直接展示授权码(name.token)
User->>3rd: ⑤ 玩家复制授权码,发送给第三方接入者
3rd->>3rd: ⑥ 按第一个 "." 拆分,得到 name 与 access_token
3rd->>Nano: ⑦ 用 name + access_token 调用第三方接口前置条件
在开始接入前,请先准备好以下凭证与环境。
申请应用凭证
联系 contact@notanote.cn 申请第三方应用接入,审核通过后你将收到:
| 凭证 | 类型 | 说明 |
|---|---|---|
TID | number | 第三方应用 ID,全局唯一 |
SecretKey | string | 应用密钥,仅限服务端使用,禁止泄露到客户端 |
SecretKey 保管
SecretKey 等同于你的应用密码。泄露后攻击者可冒充你的应用发起任意请求,包括但不限于发起授权、拉取玩家存档。请勿写入前端代码、客户端配置或版本控制系统。
接口域名
https://api.notanote.cn仅支持 HTTPS,明文 HTTP 请求会被拒绝。所有接口统一使用 POST 方法,请求体与响应体均为 JSON。
接入清单
接入前请确认以下事项:
- [ ] 已获得
TID和SecretKey - [ ] 已准备好可公网访问的回调地址(如
https://your-app.com/oauth/callback) - [ ] 服务端可发起 HTTPS 出站请求
- [ ] 玩家端浏览器可访问 Notanote 授权页域名
第一步:换取临时会话
你的服务端调用 POST /webauth/get-session,用 TID + SecretKey 换取一个 5 分钟有效的 auth-session。此步骤同时会传入 state(用于 CSRF 防护)和 callback(授权完成后的回跳地址)。
请求
接口地址:POST https://api.notanote.cn/webauth/get-session
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
tid | number | 是 | 你的第三方应用 ID |
secretkey | string | 是 | 你的第三方应用密钥 |
scope | string | 是 | 授权范围,目前仅支持 access_token |
callback | string | 是 | 授权完成后的回跳地址(合法 HTTPS URL),或填入 "show" 启用展示模式 |
state | string | 是 | 你生成的随机字符串,用于回调时防 CSRF,建议至少 16 字符 |
字段管理
此处为网页授权部分,为适合观感,此接口字段为全小写,请注意。
请求示例:
json
{
"tid": 1001,
"secretkey": "your-secret-key",
"scope": "access_token",
"callback": "https://your-app.com/oauth/callback",
"state": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
}展示模式请求示例(callback 设为 "show"):
json
{
"tid": 1001,
"secretkey": "your-secret-key",
"scope": "access_token",
"callback": "show",
"state": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
}响应
成功响应示例:
json
{
"Status": true,
"Response": {
"Identifier": "nano.api.v2.webauth.get-session.succeed",
"Data": {
"auth-session": "AbC123...XYz789",
"expire_in": 300
}
}
}| 字段 | 类型 | 说明 |
|---|---|---|
auth-session | string | 临时会话令牌,用于第二步授权页 URL |
expire_in | number | 有效期(秒),固定为 300(5 分钟) |
有效期管理
auth-session 仅在 5 分钟内有效。换取后应立即引导玩家进入授权页,避免会话过期导致授权失败。过期后需要重新调用本接口获取新的 auth-session。
失败情况
| Identifier | 说明 | 处理建议 |
|---|---|---|
nano.api.v2.webauth.get-session.failed.TIDVerifyFailed | tid 或 secretkey 错误 | 检查凭证是否正确、应用是否被禁用 |
nano.api.v2.webauth.get-session.failed.serverError | 服务端临时故障 | 稍后重试,持续失败请联系运营方 |
拿到 auth-session 后,立即进入第二步。
第二步:引导玩家授权
将玩家浏览器重定向到 Notanote 授权页面,URL 中携带 auth-session。授权页面会展示你的应用名称和授权范围,玩家登录并确认后才会跳回你的回调地址。
授权页 URL
https://nano.notanote.cn/authorize?auth-session={你的 auth-session}引导方式
在你的前端点击「使用 Notanote 登录」按钮时,由服务端返回 302 重定向:
http
HTTP/1.1 302 Found
Location: https://nano.notanote.cn/authorize?auth-session=AbC123...XYz789或者直接构造跳转链接,让浏览器发起跳转:
html
<a href="https://nano.notanote.cn/authorize?auth-session=AbC123...XYz789">
使用 Notanote 登录
</a>玩家侧体验
- 玩家被重定向到 Notanote 授权页
- 授权页展示你的应用名称、作者和授权范围(如「读取存档」)
- 玩家输入 Notanote 账号密码并确认授权
- Notanote 校验身份后生成
access_token - 浏览器自动跳转回你的
callback地址
第三步:接收回调 / 获取 Token
URL 回调模式
玩家确认授权后,浏览器会跳转到你在第一步传入的 callback 地址,URL 携带以下 query 参数:
| 参数 | 类型 | 说明 |
|---|---|---|
name | string | 玩家的 Notanote 用户名 |
access_token | string | 第三方授权令牌,用于调用后续接口 |
state | string | 你在第一步传入的 state,原样回传 |
回调 URL 示例:
https://your-app.com/oauth/callback?name=player1&access_token=xyz789&state=a1b2c3d4e5f6...展示模式(callback="show")
当 callback 设为 "show" 时,玩家确认授权后不会跳转,而是在当前页面直接展示一个授权码。授权码是一个字符串,格式为:
{name}.{token}| 组成部分 | 含义 |
|---|---|
name | 玩家的 Notanote 用户名 |
. | 固定分隔符 |
token | 第三方授权令牌(即 access_token) |
例如玩家 player1 授权成功后,页面展示的授权码形如:
player1.a1b2c3d4e5f6g7h8玩家将授权码原样复制发送给第三方接入者即可,无需分别传递用户名和 Token。接入方收到授权码后,以第一个 . 为界拆分——前半部分作为 Name、后半部分作为 Token,即可调用第四步的存档接口:
js
// 解析展示模式授权码
const dot = code.indexOf(".");
const name = code.slice(0, dot); // 玩家用户名
const token = code.slice(dot + 1); // access_token适用场景
展示模式专为 QQ 机器人等无法提供回调地址的接入场景 设计。玩家在 Notanote 授权页完成授权后,页面直接展示授权码({name}.{token}),玩家复制后发送给第三方接入者(如 QQ 机器人客服),第三方拆分出 name 与 token 即可调用后续接口。
展示模式下的 state:state 在展示模式下仍然需要传入,但回调不在你控制范围内,state 校验由你的服务端在处理后续接口调用时酌情处理。
必须校验 state(URL 回调模式)
你的 callback 收到请求后,必须先校验 state 是否与第一步发出的一致。不一致说明这不是你发起的授权,应当拒绝处理。这是防御 CSRF 攻击的关键手段。
js
// 伪代码
if (req.query.state !== mySavedState) {
// state 不匹配,拒绝本次回调,可能是 CSRF 攻击
return res.status(403).send('state 校验失败');
}
// 校验通过,安全地存储 access_token处理 access_token
access_token 会出现在浏览器地址栏和历史记录中。建议:
- 服务端在校验
state通过后立即持久化access_token - 引导玩家离开回调页面,避免 token 持续暴露在地址栏
- 不要将
access_token写入前端可读的 Cookie 或 LocalStorage
校验通过后,进入第四步。
第四步:拉取玩家存档
拿到玩家的 name 与 access_token 后(展示模式下从授权码 {name}.{token} 拆分获得),调用第三方存档接口即可读取该玩家的存档数据。完整流程为:先调 get 查询存档是否存在,再调 pull 获取临时下载链接,最后从对象存储节点直接下载存档文件。
js
// 伪代码
// 1. 查询存档元信息
const meta = await callApi('/third-party/save/get', {
Name: playerName,
TID: CONFIG.TID,
SecretKey: CONFIG.SECRET_KEY,
Token: accessToken
});
if (!meta.Response.Data.Save) {
// 玩家尚无存档
return;
}
// 2. 拉取预签名下载链接
const pull = await callApi('/third-party/save/pull', {
Name: playerName,
Token: accessToken,
TID: CONFIG.TID,
SecretKey: CONFIG.SECRET_KEY
});
// 3. 从对象存储节点直接下载存档文件
const file = await fetch(pull.Response.Data.Url);TIP
存档文件直接从对象存储节点传输至你的服务端,不经过 Notanote 服务器中转。预签名下载链接有效期仅 60 秒,获取后请立即下载。
完整的接口参数与错误码请参考 第三方存档。
Node.js 完整示例
以下是一个完整的 Express 示例,覆盖授权全流程:换取临时会话 → 引导授权 → 接收回调 → 拉取存档。可直接复制并填入你的凭证后运行。
依赖
bash
npm install express crypto服务端代码
js
const express = require("express");
const crypto = require("crypto");
const app = express();
// ===================== 配置 =====================
const CONFIG = {
TID: 1001, // 你的第三方应用 ID
SECRET_KEY: "your-secret-key", // 应用密钥(严禁泄露到前端)
API_BASE: "https://api.notanote.cn",
CALLBACK: "https://your-app.com/oauth/callback",
};
// ===================== 工具函数 =====================
/**
* 调用 Notanote API(统一 POST JSON)
* @param {string} path - 接口路径,如 "/webauth/get-session"
* @param {object} body - 请求体
* @returns {Promise<object>} 解析后的响应体
*/
async function callApi(path, body) {
const res = await fetch(`${CONFIG.API_BASE}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error(`API 请求失败: HTTP ${res.status}`);
}
const data = await res.json();
if (!data.Status) {
const errCode = data.Response?.Identifier ?? "unknown";
throw new Error(`业务错误: ${errCode}`);
}
return data;
}
// ===================== 路由 =====================
/**
* 第一步 + 第二步:发起授权 — 换取 auth-session 并重定向
*
* 你的前端点击「使用 Notanote 登录」时调用此接口。
*/
app.get("/oauth/login", async (req, res) => {
try {
// 1. 生成防 CSRF 的 state
const state = crypto.randomBytes(32).toString("hex");
// 2. 服务端持有 state,用于回调时校验(生产环境请存 Redis/DB)
// 这里用内存 Map 示意,实际项目务必使用持久化存储。
stateStore.set(state, { createdAt: Date.now() });
// 3. 换取临时会话
const result = await callApi("/webauth/get-session", {
tid: CONFIG.TID,
secretkey: CONFIG.SECRET_KEY,
scope: "access_token",
callback: CONFIG.CALLBACK,
state,
});
const authSession = result.Response.Data["auth-session"];
// 4. 302 重定向到 Notanote 授权页
res.redirect(
`https://nano.notanote.cn/authorize?auth-session=${encodeURIComponent(authSession)}`
);
} catch (err) {
console.error("换取 auth-session 失败:", err.message);
res.status(500).send("授权发起失败,请稍后再试。");
}
});
/**
* 第三步:接收授权回调
*
* 玩家确认授权后,Notanote 将浏览器重定向到此地址。
* URL 形如: /oauth/callback?name=xxx&access_token=xxx&state=xxx
*/
app.get("/oauth/callback", async (req, res) => {
const { name, access_token, state } = req.query;
try {
// 1. 校验 state — 防 CSRF 攻击,这是强制性步骤
if (!state || !stateStore.has(state)) {
res.status(403).send("state 校验失败,请求被拒绝。");
return;
}
// 2. state 一次性使用,用完即删,防止重放
stateStore.delete(state);
// 3. 持久化存储 access_token(生产环境写入数据库)
// 这里仅打印示意
console.log(`用户 ${name} 授权成功,access_token: ${access_token}`);
// 4. 你的业务逻辑:比如生成自己的 JWT、写入用户表等
// ...
// 5. 引导到你的前端页面
res.redirect(`https://your-app.com/welcome?user=${encodeURIComponent(name)}`);
} catch (err) {
console.error("回调处理失败:", err.message);
res.status(500).send("授权回调处理失败。");
}
});
/**
* 第四步:用 access_token 拉取玩家存档
*
* 你的前端/服务端在需要读取存档时调用。
*/
async function fetchPlayerSave(playerName, accessToken) {
// 1. 查询存档元信息
const meta = await callApi("/third-party/save/get", {
Name: playerName,
TID: CONFIG.TID,
SecretKey: CONFIG.SECRET_KEY,
Token: accessToken,
});
if (!meta.Response.Data.Save) {
throw new Error("玩家尚无存档");
}
// 2. 拉取预签名下载链接
const pull = await callApi("/third-party/save/pull", {
Name: playerName,
Token: accessToken,
TID: CONFIG.TID,
SecretKey: CONFIG.SECRET_KEY,
});
// 3. 返回预签名 URL,由调用方自行下载
return pull.Response.Data;
}
// ===================== 内存 state 存储(仅示意) =====================
const stateStore = new Map();
// 可选:定时清理过期的 state(auth-session 只有 5 分钟寿命)
setInterval(() => {
const now = Date.now();
for (const [key, val] of stateStore) {
if (now - val.createdAt > 5 * 60 * 1000) {
stateStore.delete(key);
}
}
}, 60_000);
// ===================== 启动 =====================
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});展示模式简化示例
如果你的应用面向 QQ 机器人等无法托管回调地址的场景,只需将 callback 改为 "show",省去回调路由:
js
const express = require("express");
const crypto = require("crypto");
const app = express();
const CONFIG = {
TID: 1001,
SECRET_KEY: "your-secret-key",
API_BASE: "https://api.notanote.cn",
};
async function callApi(path, body) {
const res = await fetch(`${CONFIG.API_BASE}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await res.json();
if (!data.Status) {
throw new Error(data.Response?.Identifier ?? "unknown");
}
return data;
}
/**
* 发起授权(展示模式)
* callback 设为 "show",授权完成后页面直接展示授权码(name.token),
* 玩家复制授权码发送给第三方接入者,由接入方拆分后调用后续接口。
*/
app.get("/oauth/login-show", async (req, res) => {
try {
const state = crypto.randomBytes(32).toString("hex");
const result = await callApi("/webauth/get-session", {
tid: CONFIG.TID,
secretkey: CONFIG.SECRET_KEY,
scope: "access_token",
callback: "show", // 关键:设为 "show"
state,
});
const authSession = result.Response.Data["auth-session"];
// 重定向到 Notanote 授权页
res.redirect(
`https://nano.notanote.cn/authorize?auth-session=${encodeURIComponent(authSession)}`
);
} catch (err) {
console.error("换取 auth-session 失败:", err.message);
res.status(500).send("授权发起失败,请稍后再试。");
}
});
// 展示模式无需回调路由:玩家把授权码(name.token)发给你后,
// 按第一个 "." 拆分即可得到 name 与 access_token
function parseAuthCode(code) {
const dot = code.indexOf(".");
return {
name: code.slice(0, dot), // 玩家用户名
token: code.slice(dot + 1), // access_token
};
}
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});展示模式注意事项
- 展示模式下授权码直接展示给玩家,玩家只需复制一个授权码(
{name}.{token})发给第三方接入者,无需分别传递用户名和 Token - 接入方收到授权码后按第一个
.拆分出Name与Token,即可直接调用存档接口,无需额外回调校验步骤 - 建议在你的服务端提供授权码绑定接口,让第三方将收到的授权码提交给你解析并存储