Files
cpzs-frontend/Store登录状态管理增强说明.md

352 lines
8.3 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Store 登录状态管理增强说明
## 📋 问题背景
在多设备登录踢出场景中,仅依赖 API 拦截器**被动响应**是不够的Store 层面也需要:
1. **主动检查**登录状态是否有效
2. **标记状态**区分正常登出和被踢出
3. **提供方法**供其他组件调用检查
---
## ✅ 已实现的增强功能
### 1. **新增状态字段**
```javascript
export const userStore = reactive({
user: null,
isLoggedIn: false,
isKickedOut: false, // 🆕 标记是否被其他设备踢出
lastCheckTime: null, // 🆕 最后一次检查登录状态的时间
// ...
})
```
### 2. **增强 `logout()` 方法**
支持标记被踢出状态:
```javascript
// 正常登出
userStore.logout()
// 被踢出时登出
userStore.logout(true) // 会标记 isKickedOut = true
```
**实现代码:**
```javascript
logout(isKickedOut = false) {
// 如果是被踢出,标记状态
if (isKickedOut) {
this.isKickedOut = true
console.log('[安全] 账号在其他设备登录,当前会话已被踢出')
}
this.user = null
this.isLoggedIn = false
this.lastCheckTime = null
// 清除本地存储...
}
```
### 3. **增强 `adminLogout()` 方法**
同样支持标记被踢出:
```javascript
adminLogout(isKickedOut = false) {
if (isKickedOut) {
this.isKickedOut = true
console.log('[安全] 管理员账号在其他设备登录,当前会话已被踢出')
}
// 清除状态和存储...
}
```
### 4. **新增 `checkLoginStatus()` 方法** 🔥
主动检查登录状态是否仍然有效:
```javascript
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()` 方法**
用于重新登录后重置被踢出状态:
```javascript
resetKickedOutStatus() {
this.isKickedOut = false
}
```
### 6. **增强 `login()` 方法**
登录时自动重置被踢出状态:
```javascript
login(userInfo) {
// ... 设置用户信息
this.isLoggedIn = true
this.isKickedOut = false // 🆕 重置被踢出状态
this.lastCheckTime = Date.now() // 🆕 记录登录时间
// 保存到本地存储...
}
```
---
## 🎯 使用场景
### 场景 1组件中主动检查登录状态
```javascript
// 在关键操作前检查登录状态
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路由守卫中使用
```javascript
// 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定期心跳检测可选
```javascript
// 在 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判断被踢出状态
```javascript
// 在登录页面显示提示
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()` 方法返回对象:
```typescript
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 拦截器已更新,会在检测到被踢出时调用:
```javascript
// src/api/index.js
userStore.logout(true) // 前台用户
userStore.adminLogout(true) // 后台管理员
```
这样确保了**被动响应**和**主动检查**两种机制的完美配合。
---
## 🎨 最佳实践
### ✅ 推荐
1. **关键操作前检查**:支付、提交表单等操作前主动检查
2. **路由守卫检查**:切换到需要登录的页面时检查
3. **显示友好提示**:根据 `isKickedOut` 显示不同的提示信息
4. **定期心跳检测**:长时间停留在页面时定期检查(可选)
### ❌ 避免
1. **过度频繁检查**:利用 10 秒缓存机制,避免每次操作都检查
2. **忽略返回值**:检查失败时要及时处理,引导用户登录
3. **混淆状态**:不区分正常登出和被踢出,提示信息不明确
---
## 🔧 相关文件
-`src/store/user.js` - Store 状态管理(已增强)
-`src/api/index.js` - API 拦截器(已更新)
- 📄 `前端多设备登录处理方案.md` - 总体方案说明
---
## 🎉 总结
通过在 Store 层面添加:
- ✅ 被踢出状态标记 `isKickedOut`
- ✅ 主动检查方法 `checkLoginStatus()`
- ✅ 增强的登出方法 `logout(isKickedOut)`
- ✅ 防频繁检查机制10秒缓存
实现了**被动响应 + 主动检查**的双重保障机制,确保多设备登录场景下的安全性和用户体验!