# 多设备登录防护实现文档

## 功能概述

实现 **"后登录踢掉先登录"** 的单设备登录机制，确保同一账号同一时间只能在一个终端登录。

---

## 后端实现

### 核心机制：Redis + Token 验证

#### 1. 登录时处理

```java
// 位置：UserServiceImpl.java 第130-139行
String token = UUID.randomUUID().toString().replace("-", "");
String redisKey = UserConstant.REDIS_USER_LOGIN_TOKEN_PREFIX + user.getId();

// 存储到Redis（新token覆盖旧token）
redisTemplate.opsForValue().set(redisKey, token, 86400, TimeUnit.SECONDS);

// 存储到Session
request.getSession().setAttribute(UserConstant.USER_LOGIN_TOKEN, token);
request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, user);
```

**关键点：**
- Redis Key 格式：`user:login:token:{userId}`
- 新token覆盖旧token，实现单设备登录
- Token有效期：24小时

#### 2. 请求验证

```java
// 位置：UserServiceImpl.java 第160-178行
String sessionToken = (String) request.getSession()
    .getAttribute(UserConstant.USER_LOGIN_TOKEN);
String redisToken = (String) redisTemplate.opsForValue().get(redisKey);

// 对比token
if (redisToken == null || !redisToken.equals(sessionToken)) {
    throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, 
        "账号在其他设备登录，当前会话已失效");
}
```

**验证流程：**
- Session token ≠ Redis token → 被踢出
- Session token = Redis token → 验证通过

---

## 前端实现

### 1. API 响应拦截器

**位置：** `src/api/index.js`

```javascript
// 防止重复提示的标志
let isKickedOut = false

api.interceptors.response.use(
  response => {
    const data = response.data
    const message = data.message || ''
    
    // 检测"其他设备登录"错误
    if (message.includes('其他设备登录') || message.includes('当前会话已失效')) {
      if (!isKickedOut) {
        isKickedOut = true
        
        // 显示提示
        ElMessage.warning('您的账号已在其他设备登录，请重新登录')
        
        // 清除状态并跳转
        import('../store/user.js').then(({ userStore }) => {
          userStore.logout(true)  // 传入true标记为被踢出
          setTimeout(() => {
            window.location.href = '/login'
            isKickedOut = false
          }, 1500)
        })
      }
    }
    return data
  }
)
```

### 2. Store 状态管理

**位置：** `src/store/user.js`

```javascript
export const userStore = reactive({
  user: null,
  isLoggedIn: false,
  isKickedOut: false,      // 标记是否被踢出
  lastCheckTime: null,     // 最后检查时间
  
  // 增强的登出方法
  logout(isKickedOut = false) {
    if (isKickedOut) {
      this.isKickedOut = true
      console.log('[安全] 账号在其他设备登录')
    }
    this.user = null
    this.isLoggedIn = false
    localStorage.clear()
  },
  
  // 主动检查登录状态
  async checkLoginStatus() {
    if (!this.isLoggedIn) return { valid: false }
    
    // 10秒内只检查一次
    const now = Date.now()
    if (this.lastCheckTime && (now - this.lastCheckTime) < 10000) {
      return { valid: true, reason: 'recently_checked' }
    }
    
    const response = await lotteryApi.getLoginUser()
    if (response.success) {
      this.lastCheckTime = now
      return { valid: true }
    } else {
      this.logout(true)
      return { valid: false, reason: 'kicked_out' }
    }
  }
})
```

---

## 完整流程

### 场景：用户A先登录，然后在另一设备登录

```
设备1登录
  ├─ Redis: user:login:token:123 = token1
  └─ Session: token1  ✅可用

设备2登录（新登录）
  ├─ Redis: user:login:token:123 = token2  ⚠️覆盖token1
  └─ Session: token2  ✅可用

设备1再请求
  ├─ Session token: token1
  ├─ Redis token: token2
  ├─ token1 ≠ token2  ❌不匹配
  ├─ 后端返回错误："账号在其他设备登录"
  ├─ 前端拦截器捕获
  ├─ 显示提示
  ├─ 清除状态
  └─ 跳转登录页
```

---

## 使用示例

### 在组件中主动检查

```javascript
async function handlePayment() {
  const status = await userStore.checkLoginStatus()
  if (!status.valid) {
    ElMessage.warning('您的账号已在其他设备登录')
    return
  }
  // 继续操作
}
```

### 显示被踢出提示

```javascript
if (userStore.isKickedOut) {
  ElMessage.warning('您的账号已在其他设备登录')
  userStore.resetKickedOutStatus()
}
```

---

## 测试验证

1. ✅ 浏览器A登录 → 正常使用
2. ✅ 浏览器B登录（同账号）
3. ✅ 浏览器A刷新 → 显示提示 → 跳转登录页
4. ✅ 只提示一次，不重复弹窗

---

## 实现检查清单

### 后端
- ✅ 登录时生成token并存储Redis
- ✅ 新token覆盖旧token
- ✅ 请求验证时对比token
- ✅ token不一致返回特定错误
- ✅ token自动过期（24小时）

### 前端
- ✅ 响应拦截器捕获错误
- ✅ 显示友好提示
- ✅ 调用logout(true)标记状态
- ✅ 自动跳转登录页
- ✅ 防止重复提示
- ✅ Store增加isKickedOut状态
- ✅ Store增加checkLoginStatus()方法
- ✅ 防频繁检查（10秒缓存）

---

## 关键文件

- **后端：** `UserServiceImpl.java`
- **前端API：** `src/api/index.js`
- **前端Store：** `src/store/user.js`

---

## 总结

实现了完整的多设备登录防护：

✅ **后端**：Redis + Token 三层验证  
✅ **前端**：被动响应 + 主动检查  
✅ **体验**：友好提示、自动跳转  
✅ **安全**：实时生效、自动过期

这套机制确保同一账号只能在一个设备登录，有效防止账号被盗用！
