feat: 檢驗用戶的 設備 ID

This commit is contained in:
CrescentLeaf
2025-09-21 12:28:44 +08:00
parent 83719f5f44
commit e5dd3ade51
9 changed files with 67 additions and 38 deletions

View File

@@ -21,9 +21,10 @@ const _data_cached = JSON.parse(_dec)
declare global { declare global {
interface Window { interface Window {
data: { data: {
split_sizes: number[]; split_sizes: number[]
apply(): void apply(): void
access_token?: string access_token?: string
device_id: string
} }
} }
} }

View File

@@ -2,19 +2,24 @@ import { io, Socket } from 'socket.io-client'
import { CallMethod, ClientEvent } from './ApiDeclare.ts' import { CallMethod, ClientEvent } from './ApiDeclare.ts'
import ApiCallbackMessage from './ApiCallbackMessage.ts' import ApiCallbackMessage from './ApiCallbackMessage.ts'
import User from "./client_data/User.ts" import User from "./client_data/User.ts"
import data from "../Data.ts"; import data from "../Data.ts"
type UnknownObject = { [key: string]: unknown } type UnknownObject = { [key: string]: unknown }
class Client { class Client {
static myUserProfile?: User static myUserProfile?: User
static socket?: Socket static socket?: Socket
static events: { [key: string]: (data: UnknownObject) => UnknownObject } = {} static events: { [key: string]: (data: UnknownObject) => UnknownObject | undefined } = {}
static connect() { static connect() {
if (data.device_id == null)
data.device_id = crypto.randomUUID()
this.socket?.disconnect() this.socket?.disconnect()
this.socket && delete this.socket this.socket && delete this.socket
this.socket = io({ this.socket = io({
transports: ['websocket'] transports: ['websocket'],
auth: {
device_id: data.device_id
},
}) })
this.socket!.on("The_White_Silk", (name: string, data: UnknownObject, callback: (ret: UnknownObject) => void) => { this.socket!.on("The_White_Silk", (name: string, data: UnknownObject, callback: (ret: UnknownObject) => void) => {
try { try {

View File

@@ -34,10 +34,22 @@ export default class ApiManager {
} }
static initEvents() { static initEvents() {
const io = this.socketIoServer const io = this.socketIoServer
io.on('connection', (socket) => { io.on('connection', (socket) => {
// TODO: fix ip == undefined
// https://github.com/denoland/deno/blob/7938d5d2a448b876479287de61e9e3b8c6109bc8/ext/node/polyfills/net.ts#L1713
const ip = socket.conn.remoteAddress
const deviceId = socket.handshake.auth.device_id as string
socket.on('disconnect', (_reason) => {
console.log(chalk.yellow('[斷]') + ` ${ip} disconnected`)
})
console.log(chalk.green('[連]') + ` ${ip} connected`)
socket.on("The_White_Silk", (name: string, args: { [key: string]: unknown }, callback_: (ret: ApiCallbackMessage) => void) => { socket.on("The_White_Silk", (name: string, args: { [key: string]: unknown }, callback_: (ret: ApiCallbackMessage) => void) => {
function callback(ret: ApiCallbackMessage) { function callback(ret: ApiCallbackMessage) {
console.log(chalk.blue('[發]') + ` ${socket.request.socket.remoteAddress} <- ${ret.code == 200 ? chalk.green(ret.msg) : chalk.red(ret.msg)} [${ret.code}]${ ret.data ? (' <extras: ' + JSON.stringify(ret.data) + '>') : ''}`) console.log(chalk.blue('[發]') + ` ${ip} <- ${ret.code == 200 ? chalk.green(ret.msg) : chalk.red(ret.msg)} [${ret.code}]${ret.data ? (' <extras: ' + JSON.stringify(ret.data) + '>') : ''}`)
return callback_(ret) return callback_(ret)
} }
try { try {
@@ -45,9 +57,11 @@ export default class ApiManager {
msg: "Invalid request.", msg: "Invalid request.",
code: 400 code: 400
}) })
console.log(chalk.red('[收]') + ` ${socket.request.socket.remoteAddress} -> ${chalk.yellow(name)} <args: ${JSON.stringify(args)}>`) console.log(chalk.red('[收]') + ` ${ip} -> ${chalk.yellow(name)} <args: ${JSON.stringify(args)}>`)
return callback(this.event_listeners[name]?.(args) || { return callback(this.event_listeners[name]?.(args, {
deviceId
}) || {
code: 501, code: 501,
msg: "Not implmented", msg: "Not implmented",
}) })
@@ -59,7 +73,7 @@ export default class ApiManager {
code: err instanceof DataWrongError ? 400 : 500, code: err instanceof DataWrongError ? 400 : 500,
msg: "錯誤: " + err.message msg: "錯誤: " + err.message
}) })
} catch(_e) {} } catch (_e) { }
} }
}) })
}) })

View File

@@ -22,14 +22,12 @@ export default abstract class BaseApi {
return true return true
return false return false
} }
checkUserToken(user: User, token: Token) { checkToken(token: Token, deviceId: string) {
if (!this.checkToken(token)) return false
if (token.author != user.bean.id) return false
return true
}
checkToken(token: Token) {
if (token.expired_time < Date.now()) return false if (token.expired_time < Date.now()) return false
if (!User.findById(token.author)) return false if (!User.findById(token.author)) return false
if (deviceId != null)
if (token.device_id != deviceId)
return false
return true return true
} }
registerEvent(name: CallMethod, func: EventCallbackFunction) { registerEvent(name: CallMethod, func: EventCallbackFunction) {

View File

@@ -15,14 +15,14 @@ export default class ChatApi extends BaseApi {
* @param token 令牌 * @param token 令牌
* @param target 目標對話 * @param target 目標對話
*/ */
this.registerEvent("Chat.getInfo", (args) => { this.registerEvent("Chat.getInfo", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target'])) return { if (this.checkArgsMissing(args, ['token', 'target'])) return {
msg: "參數缺失", msg: "參數缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌無效", msg: "令牌無效",
} }
@@ -59,14 +59,14 @@ export default class ChatApi extends BaseApi {
* @param target 目標對話 * @param target 目標對話
* @param * @param
*/ */
this.registerEvent("Chat.sendMessage", (args) => { this.registerEvent("Chat.sendMessage", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target'])) return { if (this.checkArgsMissing(args, ['token', 'target'])) return {
msg: "參數缺失", msg: "參數缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌無效", msg: "令牌無效",
} }
@@ -82,14 +82,14 @@ export default class ChatApi extends BaseApi {
* @param target 目標對話 * @param target 目標對話
* @param page 頁面 * @param page 頁面
*/ */
this.registerEvent("Chat.getMessageHistory", (args) => { this.registerEvent("Chat.getMessageHistory", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'target', 'page'])) return { if (this.checkArgsMissing(args, ['token', 'target', 'page'])) return {
msg: "參數缺失", msg: "參數缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌無效", msg: "令牌無效",
} }

View File

@@ -3,4 +3,5 @@ export default interface Token {
auth: string auth: string
made_time: number made_time: number
expired_time: number expired_time: number
device_id: string
} }

View File

@@ -22,22 +22,29 @@ export default class TokenManager {
).toString('hex') ).toString('hex')
} }
static decode(token: string) { static decode(token: string) {
if (token == null) throw new Error('令牌為空!')
return JSON.parse(crypto.createDecipheriv("aes-256-gcm", normalizeKey(config.aes_key), '01234567890123456').update( return JSON.parse(crypto.createDecipheriv("aes-256-gcm", normalizeKey(config.aes_key), '01234567890123456').update(
Buffer.from(token, 'hex') Buffer.from(token, 'hex')
).toString()) as Token ).toString()) as Token
} }
static make(user: User, time: number = Date.now()) { static make(user: User, time_: number | null | undefined, device_id: string) {
const time = (time_ || Date.now())
return this.encode({ return this.encode({
author: user.bean.id, author: user.bean.id,
auth: this.makeAuth(user), auth: this.makeAuth(user),
made_time: time, made_time: time,
expired_time: time + (1 * 1000 * 60 * 60 * 24), expired_time: time + (1 * 1000 * 60 * 60 * 24),
device_id: device_id
}) })
} }
/**
* 獲取新令牌
* 注意: 只驗證用戶, 不驗證令牌有效性!
*/
static makeNewer(user: User, token: string) { static makeNewer(user: User, token: string) {
if (this.check(user, token)) if (this.check(user, token))
return this.make(user, Date.now() + (1 * 1000 * 60 * 60 * 24)) return this.make(user, Date.now() + (1 * 1000 * 60 * 60 * 24), this.decode(token).device_id)
} }
static check(user: User, token: string) { static check(user: User, token: string) {
const tk = this.decode(token) const tk = this.decode(token)

View File

@@ -11,7 +11,7 @@ export default class UserApi extends BaseApi {
} }
override onInit(): void { override onInit(): void {
// 驗證 // 驗證
this.registerEvent("User.auth", (args) => { this.registerEvent("User.auth", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['access_token'])) return { if (this.checkArgsMissing(args, ['access_token'])) return {
msg: "參數缺失", msg: "參數缺失",
code: 400, code: 400,
@@ -23,11 +23,14 @@ export default class UserApi extends BaseApi {
msg: "登錄令牌失效", msg: "登錄令牌失效",
code: 401, code: 401,
} }
if (!User.findById(access_token.author)) return { if (!User.findById(access_token.author)) return {
msg: "賬號不存在", msg: "賬號不存在",
code: 401, code: 401,
} }
if (access_token.device_id != deviceId) return {
msg: "驗證失敗",
code: 401,
}
return { return {
msg: "成功", msg: "成功",
@@ -45,7 +48,7 @@ export default class UserApi extends BaseApi {
} }
}) })
// 登錄 // 登錄
this.registerEvent("User.login", (args) => { this.registerEvent("User.login", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['account', 'password'])) return { if (this.checkArgsMissing(args, ['account', 'password'])) return {
msg: "參數缺失", msg: "參數缺失",
code: 400, code: 400,
@@ -65,7 +68,7 @@ export default class UserApi extends BaseApi {
msg: "成功", msg: "成功",
code: 200, code: 200,
data: { data: {
access_token: TokenManager.make(user) access_token: TokenManager.make(user, null, deviceId)
}, },
} }
@@ -75,7 +78,7 @@ export default class UserApi extends BaseApi {
} }
}) })
// 注冊 // 注冊
this.registerEvent("User.register", (args) => { this.registerEvent("User.register", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['nickname', 'password'])) return { if (this.checkArgsMissing(args, ['nickname', 'password'])) return {
msg: "參數缺失", msg: "參數缺失",
code: 400, code: 400,
@@ -105,7 +108,7 @@ export default class UserApi extends BaseApi {
* ================================================ * ================================================
*/ */
// 更新頭像 // 更新頭像
this.registerEvent("User.setAvatar", (args) => { this.registerEvent("User.setAvatar", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['avatar', 'token'])) return { if (this.checkArgsMissing(args, ['avatar', 'token'])) return {
msg: "參數缺失", msg: "參數缺失",
code: 400, code: 400,
@@ -115,7 +118,7 @@ export default class UserApi extends BaseApi {
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌無效", msg: "令牌無效",
} }
@@ -130,14 +133,14 @@ export default class UserApi extends BaseApi {
} }
}) })
// 更新資料 // 更新資料
this.registerEvent("User.updateProfile", (args) => { this.registerEvent("User.updateProfile", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token'])) return { if (this.checkArgsMissing(args, ['token'])) return {
msg: "參數缺失", msg: "參數缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌無效", msg: "令牌無效",
} }
@@ -154,14 +157,14 @@ export default class UserApi extends BaseApi {
} }
}) })
// 獲取用戶信息 // 獲取用戶信息
this.registerEvent("User.getMyInfo", (args) => { this.registerEvent("User.getMyInfo", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token'])) return { if (this.checkArgsMissing(args, ['token'])) return {
msg: "參數缺失", msg: "參數缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌無效", msg: "令牌無效",
} }
@@ -180,14 +183,14 @@ export default class UserApi extends BaseApi {
} }
}) })
// 獲取聯絡人列表 // 獲取聯絡人列表
this.registerEvent("User.getMyContacts", (args) => { this.registerEvent("User.getMyContacts", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token'])) return { if (this.checkArgsMissing(args, ['token'])) return {
msg: "參數缺失", msg: "參數缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌無效", msg: "令牌無效",
} }
@@ -212,14 +215,14 @@ export default class UserApi extends BaseApi {
} }
}) })
// 添加聯絡人 // 添加聯絡人
this.registerEvent("User.addContact", (args) => { this.registerEvent("User.addContact", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'contact_chat_id'])) return { if (this.checkArgsMissing(args, ['token', 'contact_chat_id'])) return {
msg: "參數缺失", msg: "參數缺失",
code: 400, code: 400,
} }
const token = TokenManager.decode(args.token as string) const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return { if (!this.checkToken(token, deviceId)) return {
code: 401, code: 401,
msg: "令牌無效", msg: "令牌無效",
} }

View File

@@ -1,5 +1,5 @@
import ApiCallbackMessage from "../api/ApiCallbackMessage.ts" import ApiCallbackMessage from "../api/ApiCallbackMessage.ts"
type EventCallbackFunction = (args: { [key: string]: unknown }) => ApiCallbackMessage type EventCallbackFunction = (args: { [key: string]: unknown }, client: { deviceId: string }) => ApiCallbackMessage
export default EventCallbackFunction export default EventCallbackFunction