8.3 KiB
8.3 KiB
Store 登录状态管理增强说明
📋 问题背景
在多设备登录踢出场景中,仅依赖 API 拦截器被动响应是不够的,Store 层面也需要:
- 主动检查登录状态是否有效
- 标记状态区分正常登出和被踢出
- 提供方法供其他组件调用检查
✅ 已实现的增强功能
1. 新增状态字段
export const userStore = reactive({
user: null,
isLoggedIn: false,
isKickedOut: false, // 🆕 标记是否被其他设备踢出
lastCheckTime: null, // 🆕 最后一次检查登录状态的时间
// ...
})
2. 增强 logout() 方法
支持标记被踢出状态:
// 正常登出
userStore.logout()
// 被踢出时登出
userStore.logout(true) // 会标记 isKickedOut = true
实现代码:
logout(isKickedOut = false) {
// 如果是被踢出,标记状态
if (isKickedOut) {
this.isKickedOut = true
console.log('[安全] 账号在其他设备登录,当前会话已被踢出')
}
this.user = null
this.isLoggedIn = false
this.lastCheckTime = null
// 清除本地存储...
}
3. 增强 adminLogout() 方法
同样支持标记被踢出:
adminLogout(isKickedOut = false) {
if (isKickedOut) {
this.isKickedOut = true
console.log('[安全] 管理员账号在其他设备登录,当前会话已被踢出')
}
// 清除状态和存储...
}
4. 新增 checkLoginStatus() 方法 🔥
主动检查登录状态是否仍然有效:
async checkLoginStatus() {
// 1️⃣ 如果没有登录,直接返回
if (!this.isLoggedIn) {
return { valid: false, reason: 'not_logged_in' }
}
// 2️⃣ 避免频繁检查(10秒内只检查一次)
const now = Date.now()
if (this.lastCheckTime && (now - this.lastCheckTime) < 10000) {
return { valid: true, reason: 'recently_checked' }
}
try {
// 3️⃣ 调用后端接口验证登录状态
const response = await lotteryApi.getLoginUser()
if (response.success === true) {
// ✅ 登录状态有效
this.lastCheckTime = now
this.isKickedOut = false
return { valid: true, reason: 'verified' }
} else {
// ❌ 登录状态无效
const message = response.message || ''
// 检查是否是被踢出
if (message.includes('其他设备登录') || message.includes('当前会话已失效')) {
this.logout(true) // 标记为被踢出
return { valid: false, reason: 'kicked_out', message }
} else {
this.logout(false)
return { valid: false, reason: 'session_expired', message }
}
}
} catch (error) {
// ⚠️ 网络错误等情况,不清除登录状态,让用户继续使用
console.error('[Store] 检查登录状态失败:', error)
return { valid: true, reason: 'check_failed', error }
}
}
5. 新增 resetKickedOutStatus() 方法
用于重新登录后重置被踢出状态:
resetKickedOutStatus() {
this.isKickedOut = false
}
6. 增强 login() 方法
登录时自动重置被踢出状态:
login(userInfo) {
// ... 设置用户信息
this.isLoggedIn = true
this.isKickedOut = false // 🆕 重置被踢出状态
this.lastCheckTime = Date.now() // 🆕 记录登录时间
// 保存到本地存储...
}
🎯 使用场景
场景 1:组件中主动检查登录状态
// 在关键操作前检查登录状态
async function performCriticalAction() {
const status = await userStore.checkLoginStatus()
if (!status.valid) {
if (status.reason === 'kicked_out') {
ElMessage.warning('您的账号已在其他设备登录')
// 跳转到登录页
router.push('/login')
} else {
ElMessage.error('登录已过期,请重新登录')
router.push('/login')
}
return
}
// 继续执行操作
// ...
}
场景 2:路由守卫中使用
// router/index.js
router.beforeEach(async (to, from, next) => {
if (to.meta.requiresAuth) {
const status = await userStore.checkLoginStatus()
if (!status.valid) {
next('/login')
return
}
}
next()
})
场景 3:定期心跳检测(可选)
// 在 App.vue 中设置定期检查
import { onMounted, onUnmounted } from 'vue'
import { userStore } from '@/store/user'
let heartbeatTimer = null
onMounted(() => {
// 每 5 分钟检查一次登录状态
heartbeatTimer = setInterval(async () => {
if (userStore.isLoggedIn) {
await userStore.checkLoginStatus()
}
}, 5 * 60 * 1000) // 5分钟
})
onUnmounted(() => {
if (heartbeatTimer) {
clearInterval(heartbeatTimer)
}
})
场景 4:判断被踢出状态
// 在登录页面显示提示
if (userStore.isKickedOut) {
ElMessage.warning({
message: '您的账号已在其他设备登录,请重新登录',
duration: 5000
})
userStore.resetKickedOutStatus() // 重置状态
}
🔄 完整流程
被动检测(API 拦截器)
用户操作 → API 请求 → 后端检测 token 不一致 → 返回错误
↓
API 拦截器捕获 → 调用 userStore.logout(true)
↓
标记 isKickedOut = true → 显示提示 → 跳转登录页
主动检测(Store 方法)
组件调用 checkLoginStatus()
↓
10秒内检查过?
├─ 是 → 返回缓存结果
└─ 否 → 调用后端 API
↓
token 有效?
├─ 是 → 更新检查时间,返回 valid: true
└─ 否 → 检查错误类型
├─ 被踢出 → logout(true), 返回 kicked_out
└─ 其他 → logout(false), 返回 session_expired
🛡️ 防护机制
1. 防止频繁检查
- 10 秒内只检查一次
- 避免给后端造成压力
2. 容错处理
- 网络错误时不清除登录状态
- 让用户继续使用,避免误判
3. 状态标记
- 明确区分正常登出和被踢出
- 便于显示不同的提示信息
4. 自动恢复
- 登录时自动重置被踢出状态
- 提供手动重置方法
📊 返回值说明
checkLoginStatus() 方法返回对象:
interface CheckResult {
valid: boolean // 登录状态是否有效
reason: string // 原因
message?: string // 错误消息(可选)
error?: Error // 错误对象(可选)
}
Reason 枚举值:
| Reason | 说明 | valid |
|---|---|---|
not_logged_in |
未登录 | false |
recently_checked |
10秒内已检查过 | true |
verified |
后端验证通过 | true |
kicked_out |
被其他设备踢出 | false |
session_expired |
会话过期 | false |
check_failed |
检查失败(网络错误等) | true |
✅ 配合 API 拦截器
API 拦截器已更新,会在检测到被踢出时调用:
// src/api/index.js
userStore.logout(true) // 前台用户
userStore.adminLogout(true) // 后台管理员
这样确保了被动响应和主动检查两种机制的完美配合。
🎨 最佳实践
✅ 推荐
- 关键操作前检查:支付、提交表单等操作前主动检查
- 路由守卫检查:切换到需要登录的页面时检查
- 显示友好提示:根据
isKickedOut显示不同的提示信息 - 定期心跳检测:长时间停留在页面时定期检查(可选)
❌ 避免
- 过度频繁检查:利用 10 秒缓存机制,避免每次操作都检查
- 忽略返回值:检查失败时要及时处理,引导用户登录
- 混淆状态:不区分正常登出和被踢出,提示信息不明确
🔧 相关文件
- ✅
src/store/user.js- Store 状态管理(已增强) - ✅
src/api/index.js- API 拦截器(已更新) - 📄
前端多设备登录处理方案.md- 总体方案说明
🎉 总结
通过在 Store 层面添加:
- ✅ 被踢出状态标记
isKickedOut - ✅ 主动检查方法
checkLoginStatus() - ✅ 增强的登出方法
logout(isKickedOut) - ✅ 防频繁检查机制(10秒缓存)
实现了被动响应 + 主动检查的双重保障机制,确保多设备登录场景下的安全性和用户体验!