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

8.3 KiB
Raw Blame History

Store 登录状态管理增强说明

📋 问题背景

在多设备登录踢出场景中,仅依赖 API 拦截器被动响应是不够的Store 层面也需要:

  1. 主动检查登录状态是否有效
  2. 标记状态区分正常登出和被踢出
  3. 提供方法供其他组件调用检查

已实现的增强功能

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) // 后台管理员

这样确保了被动响应主动检查两种机制的完美配合。


🎨 最佳实践

推荐

  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秒缓存

实现了被动响应 + 主动检查的双重保障机制,确保多设备登录场景下的安全性和用户体验!