diff --git a/internal/logic/auth/deviceLoginLogic.go b/internal/logic/auth/deviceLoginLogic.go index 895dbcf..1c29ed7 100644 --- a/internal/logic/auth/deviceLoginLogic.go +++ b/internal/logic/auth/deviceLoginLogic.go @@ -140,9 +140,21 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) } - if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.SessionLimit()); err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error()) - } + if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.SessionLimit()); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error()) + } + // If device had a previous session, invalidate it first + oldDeviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, req.Identifier) + if oldSid, getErr := l.svcCtx.Redis.Get(l.ctx, oldDeviceCacheKey).Result(); getErr == nil && oldSid != "" { + oldSessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, oldSid) + if uidStr, _ := l.svcCtx.Redis.Get(l.ctx, oldSessionKey).Result(); uidStr != "" { + _ = l.svcCtx.Redis.Del(l.ctx, oldSessionKey).Err() + sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, uidStr) + _ = l.svcCtx.Redis.ZRem(l.ctx, sessionsKey, oldSid).Err() + } + _ = l.svcCtx.Redis.Del(l.ctx, oldDeviceCacheKey).Err() + } + // Store session id in redis sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { @@ -164,10 +176,10 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ } loginStatus = true - return &types.LoginResponse{ - Token: token, - Limit: l.svcCtx.SessionLimit(), - }, nil + return &types.LoginResponse{ + Token: token, + Limit: l.svcCtx.SessionLimit(), + }, nil } func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest) (*user.User, error) { diff --git a/internal/logic/public/user/bindEmailWithVerificationLogic.go b/internal/logic/public/user/bindEmailWithVerificationLogic.go index 91f5446..f9a36aa 100644 --- a/internal/logic/public/user/bindEmailWithVerificationLogic.go +++ b/internal/logic/public/user/bindEmailWithVerificationLogic.go @@ -2,6 +2,7 @@ package user import ( "context" + "encoding/json" "fmt" "time" @@ -47,6 +48,27 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } + // 校验邮箱验证码(安全场景) + type payload struct { + Code string `json:"code"` + LastAt int64 `json:"lastAt"` + } + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security, req.Email) + value, getErr := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if getErr != nil || value == "" { + l.Errorw("邮箱验证码校验失败", logger.Field("cacheKey", cacheKey), logger.Field("error", getErr)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "验证码错误") + } + var p payload + if err := json.Unmarshal([]byte(value), &p); err != nil { + l.Errorw("邮箱验证码反序列化失败", logger.Field("error", err.Error()), logger.Field("value", value)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "验证码错误") + } + if p.Code != req.Code { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "验证码错误") + } + _ = l.svcCtx.Redis.Del(l.ctx, cacheKey).Err() + // 获取当前用户的设备标识符 deviceIdentifier, err := l.getCurrentUserDeviceIdentifier(l.ctx, u.Id) if err != nil { @@ -71,23 +93,37 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi var emailUserId int64 if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - // 邮箱不存在,不创建新用户,直接将邮箱认证绑定到当前设备用户 + // 邮箱不存在,准备绑定到当前设备用户 + // 设备绑定数量上限校验 + if limit := l.svcCtx.SessionLimit(); limit > 0 { + if _, count, cntErr := l.svcCtx.UserModel.QueryDeviceList(l.ctx, u.Id); cntErr == nil { + if count >= limit { + return nil, xerr.NewErrCodeMsg(xerr.DeviceBindLimitExceeded, "账户绑定设备数已达上限,请移除其他设备后再登录,您也可以再注册一个新账户使用,点击帮助中心查看更多详情。") + } + } + } l.Infow(" 为当前设备做 邮箱绑定操作; 在 user_auth_methods 中添加记录", logger.Field("email", req.Email)) err = l.addAuthMethodForEmailUser(u.Id, req.Email) if err != nil { l.Errorw("添加邮箱用户认证方法失败", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "添加邮箱用户认证方法失败") } - // 关键修复:为后续 token 生成与返回结果赋值绑定后的用户ID emailUserId = u.Id } else { - // 数据库查询错误 l.Errorw("查询邮箱绑定状态失败", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "查询邮箱绑定状态失败") } } else if existingMethod.Id != 0 { // 邮箱已存在,使用现有的邮箱用户 emailUserId = existingMethod.UserId + // 设备绑定数量上限校验(目标邮箱用户) + if limit := l.svcCtx.SessionLimit(); limit > 0 { + if _, count, cntErr := l.svcCtx.UserModel.QueryDeviceList(l.ctx, emailUserId); cntErr == nil { + if count >= limit { + return nil, xerr.NewErrCodeMsg(xerr.DeviceBindLimitExceeded, "账户绑定设备数已达上限,请移除其他设备后再登录,您也可以再注册一个新账户使用,点击帮助中心查看更多详情。") + } + } + } l.Infow("邮箱已存在,将设备转移到现有邮箱用户", logger.Field("email", req.Email), logger.Field("email_user_id", emailUserId))