package auth import ( "context" "strings" "hyapp/pkg/xerr" authdomain "hyapp/services/user-service/internal/domain/auth" userdomain "hyapp/services/user-service/internal/domain/user" ) // LoginThirdParty 校验三方凭证,存在则登录,不存在则创建用户和三方绑定。 func (s *Service) LoginThirdParty(ctx context.Context, provider string, credential string, deviceID string, meta Meta) (authdomain.Token, bool, error) { provider = strings.ToLower(strings.TrimSpace(provider)) if provider == "" || strings.TrimSpace(credential) == "" || strings.TrimSpace(deviceID) == "" { // device_id 用于后续 refresh token 轮换绑定。 return authdomain.Token{}, false, xerr.New(xerr.InvalidArgument, "provider, credential and device_id are required") } if s.authRepository == nil { return authdomain.Token{}, false, xerr.New(xerr.Unavailable, "auth repository is not configured") } // provider credential 只在 user-service 内校验,gateway 不接触 provider secret。 profile, err := s.thirdPartyVerifier.Verify(ctx, provider, credential) if err != nil { s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.AuthFailed)) return authdomain.Token{}, false, authFailed() } // 已存在 provider + subject 绑定时走登录路径,否则走注册事务。 identity, err := s.authRepository.FindThirdPartyIdentity(ctx, provider, profile.ProviderSubject) if err == nil { return s.loginExistingThirdParty(ctx, identity, deviceID, meta) } if !xerr.IsCode(err, xerr.NotFound) { // 非 not found 的存储错误不能退化为注册。 s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.CodeOf(err))) return authdomain.Token{}, false, err } return s.createThirdPartyUser(ctx, provider, profile, deviceID, meta) } func (s *Service) loginExistingThirdParty(ctx context.Context, identity authdomain.ThirdPartyIdentity, deviceID string, meta Meta) (authdomain.Token, bool, error) { // 已绑定三方用户登录时仍要重新读取用户快照,确保禁用状态和靓号过期生效。 user, err := s.freshUser(ctx, identity.UserID, s.now().UnixMilli(), meta.RequestID) if err != nil { s.audit(ctx, meta, identity.UserID, loginThird, identity.Provider, resultFailed, string(xerr.CodeOf(err))) return authdomain.Token{}, false, err } if !user.CanLogin() { // 禁用用户不能通过三方登录绕过状态限制。 s.audit(ctx, meta, user.UserID, loginThird, identity.Provider, resultFailed, string(xerr.UserDisabled)) return authdomain.Token{}, false, xerr.New(xerr.UserDisabled, "user is disabled") } // 每次三方登录创建新的 refresh session。 session, refreshToken, err := s.newSession(user.UserID, deviceID) if err != nil { return authdomain.Token{}, false, err } if err := s.authRepository.CreateSession(ctx, session); err != nil { return authdomain.Token{}, false, err } s.audit(ctx, meta, user.UserID, loginThird, identity.Provider, resultSuccess, "") // isNewUser=false 表示本次只创建 session,没有创建用户。 token, err := s.issueToken(user, session.SessionID, refreshToken) return token, false, err } func (s *Service) createThirdPartyUser(ctx context.Context, provider string, profile authdomain.ThirdPartyProfile, deviceID string, meta Meta) (authdomain.Token, bool, error) { var lastErr error for attempt := 0; attempt < s.allocateMaxAttempts; attempt++ { // 三方首次登录要同时生成 user_id 和默认短号,短号冲突时有限重试。 user, displayIdentity := s.newUserWithIdentity(attempt) if !userdomain.ValidDisplayUserID(displayIdentity.DisplayUserID) { // 非法候选直接跳过,避免进入 repository 唯一性判断。 continue } session, refreshToken, err := s.newSession(user.UserID, deviceID) if err != nil { return authdomain.Token{}, false, err } thirdParty := authdomain.ThirdPartyIdentity{ UserID: user.UserID, Provider: provider, ProviderSubject: profile.ProviderSubject, ProviderUnionID: profile.ProviderUnionID, CreatedAtMs: user.CreatedAtMs, UpdatedAtMs: user.UpdatedAtMs, } err = s.authRepository.CreateThirdPartyUser(ctx, user, displayIdentity, thirdParty, session) if err == nil { // repository 事务成功后,用户、默认短号、三方绑定和首个 session 同时存在。 s.audit(ctx, meta, user.UserID, loginThird, provider, resultSuccess, "") token, tokenErr := s.issueToken(user, session.SessionID, refreshToken) return token, true, tokenErr } if xerr.IsCode(err, xerr.DisplayUserIDExists) { // 默认短号冲突可重试,user_id 和 display_user_id 都会重新生成。 lastErr = err continue } if xerr.IsCode(err, xerr.Conflict) { // 并发注册同一 provider subject 时,尝试转为已有三方登录。 identity, findErr := s.authRepository.FindThirdPartyIdentity(ctx, provider, profile.ProviderSubject) if findErr == nil { return s.loginExistingThirdParty(ctx, identity, deviceID, meta) } } s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.CodeOf(err))) return authdomain.Token{}, false, err } if lastErr != nil { // 明确记录是短号分配耗尽,而不是 provider credential 失败。 s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.DisplayUserIDAllocateFailed)) } return authdomain.Token{}, false, xerr.New(xerr.DisplayUserIDAllocateFailed, "display_user_id allocation failed") } func (s *Service) newUserWithIdentity(attempt int) (userdomain.User, userdomain.Identity) { // user 和 identity 必须成对创建,避免出现没有默认短号的用户。 userID := s.idGenerator.NewInt64() nowMs := s.now().UnixMilli() displayUserID := s.displayUserIDAllocator.NextDisplayUserID(userID, attempt) return userdomain.User{ UserID: userID, DefaultDisplayUserID: displayUserID, CurrentDisplayUserID: displayUserID, CurrentDisplayUserIDKind: userdomain.DisplayUserIDKindDefault, Status: userdomain.StatusActive, CreatedAtMs: nowMs, UpdatedAtMs: nowMs, }, userdomain.Identity{ UserID: userID, DisplayUserID: displayUserID, DefaultDisplayUserID: displayUserID, DisplayUserIDKind: userdomain.DisplayUserIDKindDefault, Status: userdomain.DisplayUserIDStatusActive, } }