166 lines
4.7 KiB
Go
166 lines
4.7 KiB
Go
// 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 ""
|
||
}
|