148 lines
6.3 KiB
Go
148 lines
6.3 KiB
Go
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,
|
||
}
|
||
}
|