// Package auth 实现 user-service 的认证、三方登录、密码登录和 refresh session 用例。 package auth import ( "context" "strings" "hyapp/pkg/appcode" "hyapp/pkg/xerr" authdomain "hyapp/services/user-service/internal/domain/auth" ) // Meta 是 gateway 透传到 user-service 的审计元信息。 type Meta struct { // AppCode 是 gateway 已解析的 App 租户键。 AppCode string // RequestID 是 gateway 透传的请求 ID,用于登录审计。 RequestID string // ClientIP 是客户端 IP。 ClientIP string // UserAgent 是客户端 UA。 UserAgent string // CountryByIP 是 gateway 或可信边缘层根据入口 IP 得到的国家快照。 CountryByIP string // Platform 是客户端平台快照,认证链路只做审计和风险任务透传。 Platform string // Language 是客户端语言偏好,gateway 只用作弱信号快拦。 Language string // Timezone 是客户端 IANA 时区,gateway 只用作弱信号快拦。 Timezone string } func (s *Service) audit(ctx context.Context, meta Meta, userID int64, loginType string, provider string, result string, failureCode string) { s.recordAudit(ctx, authdomain.LoginAudit{ AppCode: appcode.Normalize(meta.AppCode), RequestID: meta.RequestID, UserID: userID, LoginType: loginType, Provider: provider, Channel: auditChannel(loginType, provider), Platform: normalizePlatform(meta.Platform), Result: result, FailureCode: failureCode, ClientIP: meta.ClientIP, IPCountryCode: normalizeCountryCode(meta.CountryByIP), UserAgent: meta.UserAgent, CreatedAtMs: s.now().UnixMilli(), }) } func (s *Service) recordAudit(ctx context.Context, audit authdomain.LoginAudit) { if s.authRepository == nil { // 没有 repository 时无法写审计,但不应让辅助审计路径触发 panic。 return } // 审计失败不影响登录主链路,所以这里有意忽略错误。 _ = s.authRepository.RecordLoginAudit(ctx, audit) } // RecordLoginBlocked 写入 gateway fast guard 拒绝的登录审计;失败不应影响 gateway 返回阻断响应。 func (s *Service) RecordLoginBlocked(ctx context.Context, meta Meta, channel string, platform string, language string, timezone string, blockReason string) bool { if s.authRepository == nil { return false } nowMs := s.now().UnixMilli() audit := authdomain.LoginAudit{ AppCode: appcode.Normalize(meta.AppCode), RequestID: meta.RequestID, LoginType: blockedLoginType(channel), Provider: blockedProvider(channel), Channel: normalizeChannel(channel), Platform: normalizePlatform(firstNonEmpty(platform, meta.Platform)), Result: "blocked", FailureCode: string(xerr.AuthLoginBlocked), ClientIP: meta.ClientIP, IPCountryCode: normalizeCountryCode(meta.CountryByIP), Blocked: true, BlockReason: strings.TrimSpace(blockReason), UserAgent: meta.UserAgent, CreatedAtMs: nowMs, } if audit.Channel == "" { audit.Channel = "account" } return s.authRepository.RecordLoginAudit(ctx, audit) == nil } func auditChannel(loginType string, provider string) string { if loginType == loginPassword { return "account" } if loginType == loginThird { return normalizeThirdPartyChannel(provider) } return strings.TrimSpace(loginType) } func blockedLoginType(channel string) string { if normalizeChannel(channel) == "account" { return loginPassword } return loginThird } func blockedProvider(channel string) string { channel = normalizeChannel(channel) if channel == "account" { return "" } return channel } func normalizeThirdPartyChannel(provider string) string { provider = strings.ToLower(strings.TrimSpace(provider)) switch provider { case "google", "google.com", "firebase": return "google" case "facebook", "facebook.com": return "facebook" case "twitter", "x": return "twitter" case "tiktok": return "tiktok" default: return provider } } func normalizeChannel(channel string) string { channel = strings.ToLower(strings.TrimSpace(channel)) switch channel { case "", "password": return "account" case "third_party": return "" default: return normalizeThirdPartyChannel(channel) } } func normalizePlatform(platform string) string { platform = strings.ToLower(strings.TrimSpace(platform)) if platform == "android" || platform == "ios" { return platform } return platform } func normalizeCountryCode(country string) string { country = strings.ToUpper(strings.TrimSpace(country)) if len(country) < 2 || len(country) > 3 { return "" } return country } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return value } } return "" }