feat(client): 实现稳定的设备ID生成与认证优化

- 重构设备ID生成逻辑,采用多重降级策略确保唯一性与稳定性- 移除客户端SQLite缓存依赖,改用localStorage存储token与设备ID
- 优化认证流程,简化token管理与会话恢复逻辑- 增加设备数量限制检查,防止超出配额
- 更新SSE连接逻辑,适配新的认证机制
- 调整Redis连接池配置,提升并发性能与稳定性
- 移除冗余的缓存接口与本地退出逻辑
- 修复设备移除时的状态处理问题,避免重复调用offline接口
- 引入OSHI库用于硬件信息采集(备用方案)- 更新开发环境API地址配置
This commit is contained in:
2025-10-13 16:12:51 +08:00
parent 4c2546733e
commit f614860eee
17 changed files with 391 additions and 324 deletions

View File

@@ -21,7 +21,6 @@ function openAppIfNotOpened() {
mainWindow.show();
mainWindow.focus();
}
// 安全关闭启动画面
if (splashWindow && !splashWindow.isDestroyed()) {
splashWindow.close();
@@ -227,7 +226,7 @@ function startSpringBoot() {
app.quit();
}
}
startSpringBoot();
startSpringBoot();
function stopSpringBoot() {
if (!springProcess) return;
try {

View File

@@ -3,7 +3,7 @@ import {onMounted, ref, computed, defineAsyncComponent, type Component, onUnmoun
import {ElMessage, ElMessageBox} from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
import {authApi} from './api/auth'
import {authApi, TOKEN_KEY} from './api/auth'
import {deviceApi, type DeviceItem, type DeviceQuota} from './api/device'
import {getOrCreateDeviceId} from './utils/deviceId'
const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue'))
@@ -146,21 +146,19 @@ function handleMenuSelect(key: string) {
}
async function handleLoginSuccess(data: { token: string; permissions?: string; expireTime?: string }) {
isAuthenticated.value = true
showAuthDialog.value = false
showRegDialog.value = false
try {
await authApi.saveToken(data.token)
const username = getUsernameFromToken(data.token)
currentUsername.value = username
userPermissions.value = data?.permissions || ''
vipExpireTime.value = data?.expireTime ? new Date(data.expireTime) : null
localStorage.setItem(TOKEN_KEY, data.token)
isAuthenticated.value = true
showAuthDialog.value = false
showRegDialog.value = false
currentUsername.value = getUsernameFromToken(data.token)
userPermissions.value = data.permissions || ''
vipExpireTime.value = data.expireTime ? new Date(data.expireTime) : null
// 获取设备ID并注册设备
const deviceId = await getOrCreateDeviceId()
await deviceApi.register({
username,
username: currentUsername.value,
deviceId,
os: navigator.platform
})
@@ -168,26 +166,13 @@ async function handleLoginSuccess(data: { token: string; permissions?: string; e
} catch (e: any) {
isAuthenticated.value = false
showAuthDialog.value = true
await authApi.deleteTokenCache()
localStorage.removeItem(TOKEN_KEY)
ElMessage.error(e?.message || '设备注册失败')
}
}
async function logout() {
try {
const deviceId = await getClientIdFromToken()
if (deviceId) await deviceApi.offline({ deviceId })
} catch (error) {
console.warn('离线通知失败:', error)
}
try {
const tokenRes: any = await authApi.getToken()
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
if (token) await authApi.logout(token)
} catch {}
await authApi.deleteTokenCache()
function clearLocalAuth() {
localStorage.removeItem(TOKEN_KEY)
isAuthenticated.value = false
currentUsername.value = ''
userPermissions.value = ''
@@ -197,6 +182,16 @@ async function logout() {
SSEManager.disconnect()
}
async function logout() {
try {
const deviceId = getClientIdFromToken()
if (deviceId) await deviceApi.offline({ deviceId })
} catch (error) {
console.warn('离线通知失败:', error)
}
clearLocalAuth()
}
async function handleUserClick() {
if (!isAuthenticated.value) {
showAuthDialog.value = true
@@ -220,47 +215,42 @@ function showRegisterDialog() {
function backToLogin() {
showRegDialog.value = false
showAuthDialog.value = true
}
async function checkAuth() {
try {
await authApi.sessionBootstrap().catch(() => undefined)
const tokenRes: any = await authApi.getToken()
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
if (token) {
const verifyRes: any = await authApi.verifyToken(token)
isAuthenticated.value = true
currentUsername.value = getUsernameFromToken(token) || ''
userPermissions.value = verifyRes?.data?.permissions || verifyRes?.permissions || ''
if (verifyRes?.data?.expireTime || verifyRes?.expireTime) {
vipExpireTime.value = new Date(verifyRes?.data?.expireTime || verifyRes?.expireTime)
const token = localStorage.getItem(TOKEN_KEY)
if (!token) {
if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) {
showAuthDialog.value = true
}
SSEManager.connect()
return
}
} catch {
await authApi.deleteTokenCache()
}
if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) {
showAuthDialog.value = true
const res: any = await authApi.verifyToken(token)
isAuthenticated.value = true
currentUsername.value = getUsernameFromToken(token) || ''
userPermissions.value = res?.data?.permissions || res?.permissions || ''
const expireTime = res?.data?.expireTime || res?.expireTime
if (expireTime) vipExpireTime.value = new Date(expireTime)
SSEManager.connect()
} catch {
localStorage.removeItem(TOKEN_KEY)
if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) {
showAuthDialog.value = true
}
}
}
async function getClientIdFromToken(token?: string) {
function getClientIdFromToken(token?: string) {
try {
let t = token
if (!t) {
const tokenRes: any = await authApi.getToken()
t = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
}
const t = token || localStorage.getItem(TOKEN_KEY)
if (!t) return ''
const payload = JSON.parse(atob(t.split('.')[1]))
return payload.clientId || ''
return JSON.parse(atob(t.split('.')[1])).clientId || ''
} catch {
return ''
}
@@ -268,32 +258,23 @@ async function getClientIdFromToken(token?: string) {
function getUsernameFromToken(token: string) {
try {
const payload = JSON.parse(atob(token.split('.')[1]))
return payload.username || ''
return JSON.parse(atob(token.split('.')[1])).username || ''
} catch {
return ''
}
}
// SSE管理器
const SSEManager = {
connection: null as EventSource | null,
async connect() {
if (this.connection) return
try {
const tokenRes: any = await authApi.getToken()
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
if (!token) {
console.warn('SSE连接失败: 没有有效的 token')
return
}
const token = localStorage.getItem(TOKEN_KEY)
if (!token) return console.warn('SSE连接失败: 没有token')
const clientId = await getClientIdFromToken(token)
if (!clientId) {
console.warn('SSE连接失败: 无法从 token 获取 clientId')
return
}
const clientId = getClientIdFromToken(token)
if (!clientId) return console.warn('SSE连接失败: 无法获取clientId')
let sseUrl = 'http://192.168.1.89:8085/monitor/account/events'
try {
@@ -348,16 +329,15 @@ const SSEManager = {
},
handleError() {
if (!this.connection) return
try { this.connection.close() } catch {}
this.connection = null
console.warn('SSE连接失败,已断开')
this.disconnect()
},
disconnect() {
if (!this.connection) return
try { this.connection.close() } catch {}
this.connection = null
if (this.connection) {
try { this.connection.close() } catch {}
this.connection = null
}
},
}
@@ -409,8 +389,9 @@ async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) {
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
deviceQuota.value.used = Math.max(0, (deviceQuota.value.used || 0) - 1)
const clientId = await getClientIdFromToken()
if (row.deviceId === clientId) await logout()
if (row.deviceId === getClientIdFromToken()) {
clearLocalAuth() // 移除当前设备只清理本地状态不调用offline避免覆盖removed状态
}
ElMessage.success('已移除设备')
} catch (e: any) {

View File

@@ -1,45 +1,21 @@
import { http } from './http'
export const TOKEN_KEY = 'auth_token'
export const authApi = {
login(params: { username: string; password: string }) {
// 直接调用 RuoYi 后端的登录接口
return http.post('/monitor/account/login', params)
},
register(params: { username: string; password: string }) {
// 直接调用 RuoYi 后端的注册接口
return http.post('/monitor/account/register', params)
},
checkUsername(username: string) {
// 直接调用 RuoYi 后端的用户名检查接口
return http.get('/monitor/account/check-username', { username })
},
verifyToken(token: string) {
// 直接调用 RuoYi 后端的验证接口
return http.post('/monitor/account/verify', { token })
},
logout(token: string) {
// 保留客户端的 logout用于清理本地状态
return http.postVoid('/api/logout', { token })
},
// 以下缓存相关接口仍使用客户端服务(用于本地 SQLite 存储)
deleteTokenCache() {
return http.postVoid('/api/cache/delete?key=token')
},
saveToken(token: string) {
return http.postVoid('/api/cache/save', { key: 'token', value: token })
},
getToken() {
return http.get('/api/cache/get?key=token')
},
sessionBootstrap() {
return http.get('/api/session/bootstrap')
}
}

View File

@@ -1,14 +1,7 @@
/**
* 设备ID管理工具
* 从客户端服务获取硬件UUID通过 wmic 命令)
*/
const BASE_CLIENT = 'http://localhost:8081'
const DEVICE_ID_KEY = 'device_id'
/**
* 从客户端服务获取硬件设备ID
* 客户端会使用 wmic 命令获取硬件UUID仅Windows
*/
// 从客户端服务获取硬件UUID通过 wmic 命令)
async function fetchDeviceIdFromClient(): Promise<string> {
const response = await fetch(`${BASE_CLIENT}/api/device-id`, {
method: 'GET',
@@ -30,41 +23,13 @@ async function fetchDeviceIdFromClient(): Promise<string> {
return deviceId
}
/**
* 获取或创建设备ID
* 1. 优先从本地缓存读取
* 2. 如果没有缓存从客户端服务获取使用硬件UUID
* 3. 保存到本地缓存
*/
// 获取或创建设备ID优先读缓存没有则从客户端服务获取硬件UUID
export async function getOrCreateDeviceId(): Promise<string> {
try {
// 尝试从本地缓存获取
const response = await fetch(`${BASE_CLIENT}/api/cache/get?key=deviceId`)
if (response.ok) {
const result = await response.json()
const cachedDeviceId = result?.data
if (cachedDeviceId) {
return cachedDeviceId
}
}
} catch (error) {
console.warn('从缓存读取设备ID失败:', error)
}
const cached = localStorage.getItem(DEVICE_ID_KEY)
if (cached) return cached
// 从客户端服务获取新的设备ID硬件UUID
const newDeviceId = await fetchDeviceIdFromClient()
// 保存到本地缓存
try {
await fetch(`${BASE_CLIENT}/api/cache/save`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'deviceId', value: newDeviceId })
})
} catch (error) {
console.warn('保存设备ID到缓存失败:', error)
}
return newDeviceId
const deviceId = await fetchDeviceIdFromClient()
localStorage.setItem(DEVICE_ID_KEY, deviceId)
return deviceId
}