5.8 KiB
5.8 KiB
设备管理系统 Bug 分析 - 最终确认版
场景还原
用户操作流程
-
设备A 最初通过设备登录(DeviceLogin),系统自动创建用户1 + 设备A记录
-
设备B 最初也通过设备登录,系统自动创建用户2 + 设备B记录
-
设备A 绑定邮箱 xxx@example.com,用户1变为"邮箱+设备"用户
-
设备B 绑定同一个邮箱 xxx@example.com
- 系统发现邮箱已存在,执行设备转移
- 设备B 从用户2迁移到用户1
- 用户2被删除
- 现在用户1拥有:设备A + 设备B + 邮箱认证
-
在设备A上操作,从设备列表移除设备B
-
问题:设备B没有被踢下线,仍然能使用
数据流分析
绑定邮箱后的状态(第4步后)
User 表:
┌─────┬───────────────┐
│ Id │ 用户1 │
└─────┴───────────────┘
user_device 表:
┌─────────────┬───────────┐
│ Identifier │ UserId │
├─────────────┼───────────┤
│ device-a │ 用户1 │
│ device-b │ 用户1 │ <- 设备B迁移到用户1
└─────────────┴───────────┘
user_auth_methods 表:
┌────────────┬────────────────┬───────────┐
│ AuthType │ AuthIdentifier │ UserId │
├────────────┼────────────────┼───────────┤
│ device │ device-a │ 用户1 │
│ device │ device-b │ 用户1 │
│ email │ xxx@email.com │ 用户1 │
└────────────┴────────────────┴───────────┘
DeviceManager (内存 WebSocket 连接):
┌───────────────────────────────────────────────────┐
│ userDevices sync.Map │
├───────────────────────────────────────────────────┤
│ 用户1 -> [Device{DeviceID="device-a", ...}] │
│ 用户2 -> [Device{DeviceID="device-b", ...}] ❌ │ <- 问题!设备B的连接仍在用户2名下
└───────────────────────────────────────────────────┘
问题根源
设备B绑定邮箱时(bindEmailWithVerificationLogic.go):
- ✅ 数据库:设备B的
UserId被更新为用户1 - ❌ 内存:
DeviceManager中设备B的 WebSocket 连接仍然在用户2名下 - ❌ 缓存:
device:device-b-> 旧的 sessionId(可能关联用户2)
解绑设备B时(unbindDeviceLogic.go):
// 第 48 行:验证设备属于当前用户
if device.UserId != u.Id { // device.UserId=用户1, u.Id=用户1, 验证通过
return errors.Wrapf(...)
}
// 第 123 行:踢出设备
l.svcCtx.DeviceManager.KickDevice(u.Id, identifier)
// KickDevice(用户1, "device-b")
KickDevice 执行时:
func (dm *DeviceManager) KickDevice(userID int64, deviceID string) {
val, ok := dm.userDevices.Load(userID) // 查找用户1的设备列表
// 用户1的设备列表只有 device-a
// 找不到 device-b!因为 device-b 的连接还在用户2名下
}
根本原因总结
| 操作 | 数据库 | DeviceManager 内存 | Redis 缓存 |
|---|---|---|---|
| 设备B绑定邮箱 | ✅ 更新 UserId | ❌ 未更新 | ❌ 未清理 |
| 解绑设备B | ✅ 创建新用户 | ❌ 找不到设备 | ✅ 尝试清理 |
核心问题:设备绑定邮箱(转移用户)时,没有更新 DeviceManager 中的连接归属。
修复方案
方案1:在绑定邮箱时踢出旧连接(推荐)
在 bindEmailWithVerificationLogic.go 迁移设备后,踢出设备的旧连接:
// 迁移设备到邮箱用户后
for _, device := range devices {
// 更新设备归属
device.UserId = emailUserId
err = l.svcCtx.UserModel.UpdateDevice(l.ctx, device)
// 新增:踢出旧连接(使用原用户ID)
l.svcCtx.DeviceManager.KickDevice(u.Id, device.Identifier)
}
方案2:在解绑时遍历所有用户查找设备
修改 KickDevice 或 unbindDeviceLogic 逻辑,不依赖用户ID查找设备。
方案3:清理 Redis 缓存使旧 Token 失效
确保设备转移后,旧的 session 和 device 缓存被清理:
deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, device.Identifier)
if sessionId, _ := l.svcCtx.Redis.Get(ctx, deviceCacheKey).Result(); sessionId != "" {
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
l.svcCtx.Redis.Del(ctx, deviceCacheKey, sessionIdCacheKey)
}
推荐修复策略
双管齐下:
-
修复
bindEmailWithVerificationLogic.go:- 设备转移后立即踢出旧连接
- 清理旧用户的缓存
-
修复
unbindDeviceLogic.go(防御性编程):- 补充
user_sessions清理逻辑(参考deleteUserDeviceLogic.go)
- 补充
涉及文件
| 文件 | 修改内容 |
|---|---|
internal/logic/public/user/bindEmailWithVerificationLogic.go |
设备转移后踢出旧连接 |
internal/logic/public/user/unbindDeviceLogic.go |
补充 user_sessions 清理 |
验收标准
- 设备B绑定邮箱后,设备B的旧连接被踢出
- 从设备A解绑设备B后,设备B立即被踢下线
- 设备B的 Token 失效,无法继续调用 API