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 {
interface Window {
data: {
split_sizes: number[];
split_sizes: number[]
apply(): void
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 ApiCallbackMessage from './ApiCallbackMessage.ts'
import User from "./client_data/User.ts"
import data from "../Data.ts";
import data from "../Data.ts"
type UnknownObject = { [key: string]: unknown }
class Client {
static myUserProfile?: User
static socket?: Socket
static events: { [key: string]: (data: UnknownObject) => UnknownObject } = {}
static events: { [key: string]: (data: UnknownObject) => UnknownObject | undefined } = {}
static connect() {
if (data.device_id == null)
data.device_id = crypto.randomUUID()
this.socket?.disconnect()
this.socket && delete this.socket
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) => {
try {

View File

@@ -34,10 +34,22 @@ export default class ApiManager {
}
static initEvents() {
const io = this.socketIoServer
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) => {
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)
}
try {
@@ -45,9 +57,11 @@ export default class ApiManager {
msg: "Invalid request.",
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,
msg: "Not implmented",
})
@@ -59,7 +73,7 @@ export default class ApiManager {
code: err instanceof DataWrongError ? 400 : 500,
msg: "錯誤: " + err.message
})
} catch(_e) {}
} catch (_e) { }
}
})
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ export default class UserApi extends BaseApi {
}
override onInit(): void {
// 驗證
this.registerEvent("User.auth", (args) => {
this.registerEvent("User.auth", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['access_token'])) return {
msg: "參數缺失",
code: 400,
@@ -23,11 +23,14 @@ export default class UserApi extends BaseApi {
msg: "登錄令牌失效",
code: 401,
}
if (!User.findById(access_token.author)) return {
msg: "賬號不存在",
code: 401,
}
if (access_token.device_id != deviceId) return {
msg: "驗證失敗",
code: 401,
}
return {
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 {
msg: "參數缺失",
code: 400,
@@ -65,7 +68,7 @@ export default class UserApi extends BaseApi {
msg: "成功",
code: 200,
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 {
msg: "參數缺失",
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 {
msg: "參數缺失",
code: 400,
@@ -115,7 +118,7 @@ export default class UserApi extends BaseApi {
code: 400,
}
const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return {
if (!this.checkToken(token, deviceId)) return {
code: 401,
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 {
msg: "參數缺失",
code: 400,
}
const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return {
if (!this.checkToken(token, deviceId)) return {
code: 401,
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 {
msg: "參數缺失",
code: 400,
}
const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return {
if (!this.checkToken(token, deviceId)) return {
code: 401,
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 {
msg: "參數缺失",
code: 400,
}
const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return {
if (!this.checkToken(token, deviceId)) return {
code: 401,
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 {
msg: "參數缺失",
code: 400,
}
const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token)) return {
if (!this.checkToken(token, deviceId)) return {
code: 401,
msg: "令牌無效",
}

View File

@@ -1,5 +1,5 @@
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