添加用户会话数限制功能,当超过最大会话数时自动移除最旧的会话 - 在config中添加UserSessionsKeyPrefix常量 - 在JwtAuth配置中新增MaxSessionsPerUser字段 - 在ServiceContext中实现EnforceUserSessionLimit方法 - 在所有登录逻辑中调用会话限制检查
3.7 KiB
修复目标
- 解决首次设备登录时在
internal/logic/auth/deviceLoginLogic.go:99对deviceInfo赋值导致的空指针崩溃,确保接口稳定返回。
根因定位
-
设备不存在分支仅创建用户与设备记录,但未为局部变量
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需要有效设备Id:internal/model/user/device.go:58-69
-
修改方案
-
在“设备不存在”分支注册完成后,立即通过标识重新查询设备,赋值给
deviceInfo:-
在
internal/logic/auth/deviceLoginLogic.go的if errors.Is(err, gorm.ErrRecordNotFound)分支中,userInfo, err = l.registerUserAndDevice(req)之后追加:-
deviceInfo, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) -
如果查询失败则返回数据库查询错误(与现有风格一致)。
-
-
-
在更新设备 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。
-
-
-
可选优化(减少二次查询):
- 将
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错误处理路径,避免_ = ...静默失败。 -
为“首次设备登录”场景补充集成测试,保证不再回归。