From 4258a7fd265185ab1176bc969ca0aeaf43cde225 Mon Sep 17 00:00:00 2001 From: zhl Date: Thu, 2 Mar 2023 11:56:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0web=E7=AB=AF=E9=92=B1?= =?UTF-8?q?=E5=8C=85=E7=99=BB=E5=BD=95=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/wallet.bridge.controller.ts | 65 +++++++++++++++ src/controllers/wallet.controller.ts | 5 +- src/modules/Wallet.ts | 3 + src/service/bridge.svr.ts | 92 +++++++++++++++++++++ src/utils/security.util.ts | 10 +++ src/utils/string.util.ts | 52 +++++++++--- 6 files changed, 215 insertions(+), 12 deletions(-) create mode 100644 src/controllers/wallet.bridge.controller.ts create mode 100644 src/service/bridge.svr.ts diff --git a/src/controllers/wallet.bridge.controller.ts b/src/controllers/wallet.bridge.controller.ts new file mode 100644 index 0000000..5ee0754 --- /dev/null +++ b/src/controllers/wallet.bridge.controller.ts @@ -0,0 +1,65 @@ +import BaseController from "common/base.controller"; +import {ZError} from "common/ZError"; +import {role, router} from "decorators/router"; +import {BridgeSvr} from "service/bridge.svr"; + +class WalletBridgeController extends BaseController { + @role('anon') + @router('post /bridge/regist') + async registWebClient(req, res) { + let { clientId } = req.params; + if (!clientId) { + throw new ZError(10, 'params mismatch') + } + const qrId = new BridgeSvr().registClientReq(clientId) + const token = await res.jwtSign({ qrId }) + return { token } + } + + @role('anon') + @router('post /bridge/status') + async bridgeStatus(req, res) { + let { webtoken,clientId } = req.params; + if (!webtoken || !clientId) { + throw new ZError(10, 'params mismatch') + } + const reqData = await req.jwtVerify({ extractToken: () => webtoken}) + if (!reqData || !reqData.qrId) { + throw new ZError(14, 'token error') + } + const { qrId } = reqData + const data = new BridgeSvr().fetchData(qrId) + if (!data) { + throw new ZError(11, 'data not found') + } + if (data.clientId !== clientId) { + throw new ZError(13, 'login info mismatch') + } + if (data.status === 0 && data.expire < Date.now()) { + throw new ZError(12, 'login expired') + } + return data + } + /** + * 1. 验证游戏客户端的token + * 2. 验证扫码获得的web端的token, 获取qrId + * 3. 生成一个有时效的token, 保存至cache, 供web端获取 + */ + @router('post /bridge/upload') + async uploadKeyInfo(req, res) { + let { key, webtoken } = req.params; + let user = req.user + if (!webtoken || !key ) { + throw new ZError(10, 'params mismatch') + } + const reqData = await req.jwtVerify({ extractToken: () => webtoken}) + if (!reqData || !reqData.qrId) { + throw new ZError(14, 'token error') + } + const { qrId } = reqData + const token = await res.jwtSign({ id: user.id }) + let result = new BridgeSvr().updateData(qrId, key, token) + return {result} + } + +} diff --git a/src/controllers/wallet.controller.ts b/src/controllers/wallet.controller.ts index 62af999..511062d 100644 --- a/src/controllers/wallet.controller.ts +++ b/src/controllers/wallet.controller.ts @@ -27,12 +27,15 @@ class WalletController extends BaseController { @router('post /wallet/info') async uploadWalletInfo(req, res) { let user = req.user - let { key } = req.params + let { key, address } = req.params if (!key) { throw new ZError(10, 'no data to save') } let record = await Wallet.insertOrUpdate({ account: user.id }, {}) record.key = key + if (address) { + record.address = address + } await record.save() return {} } diff --git a/src/modules/Wallet.ts b/src/modules/Wallet.ts index 17c837e..fff61f1 100644 --- a/src/modules/Wallet.ts +++ b/src/modules/Wallet.ts @@ -17,6 +17,9 @@ class WalletClass extends BaseModule { @prop() public key: string + @prop() + public address: string + /** * 钱包客户端存储的密码 */ diff --git a/src/service/bridge.svr.ts b/src/service/bridge.svr.ts new file mode 100644 index 0000000..7ee8d28 --- /dev/null +++ b/src/service/bridge.svr.ts @@ -0,0 +1,92 @@ +import { ZError } from 'common/ZError' +import { singleton } from 'decorators/singleton' +import { shortUuid, uuid } from 'utils/security.util' + +export interface IQrData { + clientId: string + qrId: string + expire: number + remove: number + status: number + userId?: string + key?: string + token?: string +} + +const DEFAULT_EXPIRE_TIME = 5 * 60 * 1000 +const DEFAULT_DELETE_TIME = 10 * 60 * 1000 + +@singleton +export class BridgeSvr { + private clientMap: Map = new Map() + private qrMap: Map = new Map() + + public registClientReq(clientId: string): string { + const qrId = shortUuid() + const now = Date.now() + if (this.clientMap.has(clientId)) { + const data = this.clientMap.get(clientId) + if (data.expire > now && data.status == 0) { + data.expire = now + DEFAULT_EXPIRE_TIME + data.remove = now + DEFAULT_DELETE_TIME + return data.qrId + } else { + if (this.qrMap.has(data.qrId)) { + this.qrMap.delete(data.qrId) + } + this.clientMap.delete(clientId) + } + } + let data = { + clientId, + qrId, + expire: now + DEFAULT_EXPIRE_TIME, + remove: now + DEFAULT_DELETE_TIME, + status: 0, + } + this.clientMap.set(clientId, data) + this.qrMap.set(qrId, data) + let self = this + setImmediate(function () { + self.cacheGc() + }) + return qrId + } + + public cacheGc() { + let now = Date.now() + for (let [key, data] of this.qrMap.entries()) { + if (data.remove <= now) { + this.qrMap.delete(key) + } + } + for (let [key, data] of this.clientMap.entries()) { + if (data.remove <= now) { + this.clientMap.delete(key) + } + } + } + + public fetchData(qrId: string) { + const data = this.qrMap.get(qrId) + return data + } + + public updateData(qrId: string, key: string, token: string) { + if (!this.qrMap.has(qrId)) { + throw new ZError(21, 'login data not found') + } + let data = this.qrMap.get(qrId) + const now = Date.now() + if (data.status === 0 && data.expire < now) { + throw new ZError(22, 'login data expired') + } + if (data.status != 0) { + throw new ZError(23, 'already logined') + } + data.status = 1 + data.key = key + data.token = token + return true + } +} diff --git a/src/utils/security.util.ts b/src/utils/security.util.ts index ecbb84b..f33a05d 100644 --- a/src/utils/security.util.ts +++ b/src/utils/security.util.ts @@ -1,4 +1,5 @@ import crypto from 'crypto' +import {compressUuid} from './string.util' const ENCODER = 'base64' const REG_KEY = /^[0-9a-fA-F]{63,64}$/ @@ -22,3 +23,12 @@ export function aesDecrypt(encryptedText: string, password: string, iv: string) let decrypted = decipher.update(encryptedText, ENCODER, 'utf8') return decrypted + decipher.final('utf8') } + +export function uuid() { + return crypto.randomUUID() +} + +export function shortUuid() { + let uid = uuid() + return compressUuid(uid) +} diff --git a/src/utils/string.util.ts b/src/utils/string.util.ts index 62dba1b..a4c288b 100644 --- a/src/utils/string.util.ts +++ b/src/utils/string.util.ts @@ -15,7 +15,7 @@ export function generateKeyValStr(data: {}, ignoreNull = true, splitChar: string return } if (i++ > 0) result += splitChar - result += `${key}${equalChar}${data[key]}` + result += `${key}${equalChar}${data[key]}` } return result } @@ -47,15 +47,15 @@ export function keyValToObject(str: string, splitChar: string = '&', equalChar = export function isTrue(obj) { return ( obj === 'true' || - obj === 'TRUE' || - obj === 'True' || - obj === 'on' || - obj === 'ON' || - obj === true || - obj === 1 || - obj === '1' || - obj === 'YES' || - obj === 'yes' + obj === 'TRUE' || + obj === 'True' || + obj === 'on' || + obj === 'ON' || + obj === true || + obj === 1 || + obj === '1' || + obj === 'YES' || + obj === 'yes' ) } @@ -84,7 +84,7 @@ export function string10to62(number: string | number) { qutient = (qutient - mod) / radix arr.unshift(chars[mod]) } while (qutient) - return arr.join('') + return arr.join('') } /** @@ -104,3 +104,33 @@ export function string62to10(numberCode: string) { } return originNumber } + +const reNormalUUID = /^[0-9a-fA-F-]{36}$/; +const reLongUUID = /^[0-9a-fA-F]{32}$/; +const reShortUUID = /^[0-9a-zA-Z+/]{22,23}$/; +const n = /-/g; + +export function compressUuid(e:string, t: boolean = false) { + if (reNormalUUID.test(e)) { + e = e.replace(n, ''); + } else if (!reLongUUID.test(e)) { + return e; + } + var r = !0 === t ? 2 : 5; + return compressHex(e, r) +} + +const CHARS_BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; +export function compressHex(e: string, r: number) { + var i, n = e.length; + i = void 0 !== r ? r : n % 3; + for (var s = e.slice(0, i), o = []; i < n;) { + var u = parseInt(e[i], 16), + a = parseInt(e[i + 1], 16), + c = parseInt(e[i + 2], 16); + o.push(CHARS_BASE64[u << 2 | a >> 2]); + o.push(CHARS_BASE64[(3 & a) << 4 | c]); + i += 3; + } + return s + o.join('') +}