hi-server/.trae/documents/实现按用户Token并发登录上限N并超限逐出.md
shanshanzhong 1d5d361ae8
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m32s
feat(auth): 实现用户会话数限制功能
添加用户会话数限制功能,当超过最大会话数时自动移除最旧的会话
- 在config中添加UserSessionsKeyPrefix常量
- 在JwtAuth配置中新增MaxSessionsPerUser字段
- 在ServiceContext中实现EnforceUserSessionLimit方法
- 在所有登录逻辑中调用会话限制检查
2025-11-26 17:52:12 -08:00

3.7 KiB
Raw Blame History

修复目标

  • 解决首次设备登录时在 internal/logic/auth/deviceLoginLogic.go:99deviceInfo 赋值导致的空指针崩溃,确保接口稳定返回。

根因定位

  • 设备不存在分支仅创建用户与设备记录,但未为局部变量 deviceInfo 赋值;随后在 internal/logic/auth/deviceLoginLogic.go:99-100 使用 deviceInfo 导致 nil 解引用。

  • 参考位置:

    • 赋值处:internal/logic/auth/deviceLoginLogic.go:99-101

    • 设备存在分支赋值:internal/logic/auth/deviceLoginLogic.go:88-95

    • 设备不存在分支未赋值:internal/logic/auth/deviceLoginLogic.go:74-79

    • UpdateDevice 需要有效设备 Idinternal/model/user/device.go:58-69

修改方案

  1. 在“设备不存在”分支注册完成后,立即通过标识重新查询设备,赋值给 deviceInfo

    • internal/logic/auth/deviceLoginLogic.goif errors.Is(err, gorm.ErrRecordNotFound) 分支中,userInfo, err = l.registerUserAndDevice(req) 之后追加:

      • deviceInfo, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)

      • 如果查询失败则返回数据库查询错误(与现有风格一致)。

  2. 在更新设备 UA 前增加空指针保护,并不再忽略更新错误:

    • internal/logic/auth/deviceLoginLogic.go:99-101 改为:

      • 检查 deviceInfo != nil

      • deviceInfo.UserAgent = req.UserAgent

      • if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil { 记录错误并返回包装后的错误 xerr.DatabaseUpdateError

  3. 可选优化(减少二次查询):

    • registerUserAndDevice(req) 的返回值改为 (*user.User, *user.Device, error),在注册时直接返回新建设备对象;调用点随之调整。若选择此方案,仍需在更新前做空指针保护。

代码示例方案1最小改动

// internal/logic/auth/deviceLoginLogic.go
// 设备不存在分支注册后追加一次设备查询
userInfo, err = l.registerUserAndDevice(req)
if err != nil {
    return nil, err
}
deviceInfo, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)
if err != nil {
    l.Errorw("query device after register failed",
        logger.Field("identifier", req.Identifier),
        logger.Field("error", err.Error()),
    )
    return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device after register failed: %v", err.Error())
}

// 更新 UA不忽略更新错误
if deviceInfo != nil {
    deviceInfo.UserAgent = req.UserAgent
    if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil {
        l.Errorw("update device failed",
            logger.Field("user_id", userInfo.Id),
            logger.Field("identifier", req.Identifier),
            logger.Field("error", err.Error()),
        )
        return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err.Error())
    }
}

测试用例与验证

  • 用例1首次设备标识登录设备不存在应成功返回 Token日志包含注册与登录记录无 500。

  • 用例2已存在设备标识登录设备存在应正常更新 UA 并返回 Token。

  • 用例3模拟数据库异常时应返回一致的业务错误码不产生 panic

风险与回滚

  • 改动限定在登录逻辑,属最小范围;若出现异常,回滚为当前版本即可。

  • 不改变数据结构与外部接口行为,兼容现有客户端。

后续优化(可选)

  • 统一 UpdateDevice 错误处理路径,避免 _ = ... 静默失败。

  • 为“首次设备登录”场景补充集成测试,保证不再回归。