From 85477fe46ebeccbda9e1350bf2d66f002dc868ed Mon Sep 17 00:00:00 2001 From: CrescentLeaf Date: Mon, 6 Oct 2025 17:13:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=88=B7=E6=96=B0?= =?UTF-8?q?=E4=BB=A4=E7=89=8C=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 服务端: 添加对应的接口, 对原有令牌系统稍有修改, 添加了令牌类型 * 客户端: 自动刷新访问令牌, 登录时顺带获取刷新令牌 --- client/api/Client.ts | 22 +++++++++++++-- client/ui/dialog/LoginDialog.tsx | 1 + server/api/Token.ts | 3 +++ server/api/TokenManager.ts | 25 ++++++----------- server/api/TokenType.ts | 3 +++ server/api/UserApi.ts | 46 +++++++++++++++++++++++++++++++- 6 files changed, 80 insertions(+), 20 deletions(-) create mode 100644 server/api/TokenType.ts diff --git a/client/api/Client.ts b/client/api/Client.ts index e37f995..71bdfcc 100644 --- a/client/api/Client.ts +++ b/client/api/Client.ts @@ -51,15 +51,33 @@ class Client { }) } return new Promise((resolve) => { - this.socket!.timeout(timeout).emit("The_White_Silk", method, args, (err: Error, res: ApiCallbackMessage) => { + this.socket!.timeout(timeout).emit("The_White_Silk", method, args, async (err: Error, res: ApiCallbackMessage) => { if (err) return resolve({ code: -1, msg: err.message.indexOf("timed out") != -1 ? "請求超時" : err.message, }) - resolve(res) + if (res.code == 401) { + const token = await this.refreshAccessToken() + if (token) { + data.access_token = token + data.apply() + resolve(await this.invoke(method, { + ...args, + token + }, timeout)) + } else + resolve(res) + } else + resolve(res) }) }) } + static async refreshAccessToken() { + const re = await this.invoke("User.refreshAccessToken", { + refresh_token: data.refresh_token + }) + return re.data?.access_token + } static async auth(token: string, timeout: number = 5000) { const re = await this.invoke("User.auth", { access_token: token diff --git a/client/ui/dialog/LoginDialog.tsx b/client/ui/dialog/LoginDialog.tsx index 2a06475..dd1ea4d 100644 --- a/client/ui/dialog/LoginDialog.tsx +++ b/client/ui/dialog/LoginDialog.tsx @@ -35,6 +35,7 @@ export default function LoginDialog({ if (checkApiSuccessOrSncakbar(re, "登录失败")) return data.access_token = re.data!.access_token as string + data.refresh_token = re.data!.refresh_token as string data.apply() location.reload() }) diff --git a/server/api/Token.ts b/server/api/Token.ts index e843468..bfa9393 100644 --- a/server/api/Token.ts +++ b/server/api/Token.ts @@ -1,7 +1,10 @@ +import TokenType from "./TokenType.ts" + export default interface Token { author: string auth: string made_time: number expired_time: number device_id: string + type: TokenType } \ No newline at end of file diff --git a/server/api/TokenManager.ts b/server/api/TokenManager.ts index 4785022..4ea089d 100644 --- a/server/api/TokenManager.ts +++ b/server/api/TokenManager.ts @@ -3,6 +3,7 @@ import config from "../config.ts" import User from "../data/User.ts" import crypto from 'node:crypto' import Token from "./Token.ts" +import TokenType from "./TokenType.ts" function normalizeKey(key: string, keyLength = 32) { const hash = crypto.createHash('sha256') @@ -31,38 +32,28 @@ export default class TokenManager { } } - static make(user: User, time_: number | null | undefined, device_id: string) { + static make(user: User, time_: number | null | undefined, device_id: string, type: TokenType = "access_token") { const time = (time_ || Date.now()) return this.encode({ author: user.bean.id, auth: this.makeAuth(user), made_time: time, - expired_time: time + (1 * 1000 * 60 * 60 * 24), - device_id: device_id + expired_time: time + (type == 'access_token' ? (1000 * 60 * 60 * 2) : (40 * 1000 * 60 * 60 * 24)), + device_id: device_id, + type }) } - /** - * 獲取新令牌 - * 注意: 只驗證用戶, 不驗證令牌有效性! - */ - static makeNewer(user: User, token: string) { - if (this.check(user, token)) - return this.make(user, Date.now() + (1 * 1000 * 60 * 60 * 24), this.decode(token).device_id) - } - static check(user: User, token: string) { - const tk = this.decode(token) - - return this.makeAuth(user) == tk.auth - } /** * 嚴格檢驗令牌: 時間, 用戶, (設備 ID) */ - static checkToken(token: Token, deviceId?: string) { + static checkToken(token: Token, deviceId?: string, type: TokenType = 'access_token') { if (token.expired_time < Date.now()) return false if (!token.author || !User.findById(token.author)) return false if (deviceId != null) if (token.device_id != deviceId) return false + if (token.type != type) + return false return true } } diff --git a/server/api/TokenType.ts b/server/api/TokenType.ts new file mode 100644 index 0000000..fd3cd3b --- /dev/null +++ b/server/api/TokenType.ts @@ -0,0 +1,3 @@ +type TokenType = "access_token" | "refresh_token" + +export default TokenType diff --git a/server/api/UserApi.ts b/server/api/UserApi.ts index 2a775b0..42da2c7 100644 --- a/server/api/UserApi.ts +++ b/server/api/UserApi.ts @@ -57,6 +57,49 @@ export default class UserApi extends BaseApi { throw e } }) + // 刷新访问令牌 + this.registerEvent("User.refreshAccessToken", (args, clientInfo) => { + if (this.checkArgsMissing(args, ['refresh_token'])) return { + msg: "参数缺失", + code: 400, + } + const { deviceId } = clientInfo + try { + const refresh_token = TokenManager.decode(args.refresh_token as string) + + if (refresh_token.expired_time < Date.now()) return { + msg: "登录令牌失效", + code: 401, + } + if (!refresh_token.author || !User.findById(refresh_token.author)) return { + msg: "账号不存在", + code: 401, + } + if (refresh_token.device_id != deviceId) return { + msg: "验证失败", + code: 401, + } + + const user = User.findById(refresh_token.author) as User + + return { + msg: "成功", + code: 200, + data: { + access_token: TokenManager.make(user, null, deviceId) + } + } + } catch (e) { + const err = e as Error + if (err.message.indexOf("JSON") != -1) + return { + msg: "无效的用户令牌", + code: 401, + } + else + throw e + } + }) // 登錄 this.registerEvent("User.login", (args, { deviceId }) => { if (this.checkArgsMissing(args, ['account', 'password'])) return { @@ -78,7 +121,8 @@ export default class UserApi extends BaseApi { msg: "成功", code: 200, data: { - access_token: TokenManager.make(user, null, deviceId) + refresh_token: TokenManager.make(user, null, deviceId, 'refresh_token'), + access_token: TokenManager.make(user, null, deviceId), }, }