2026-04-27 02:29:42 +08:00

148 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
}
}