package auth import ( "context" "net/netip" "sort" "strings" "time" "hyapp/pkg/appcode" authdomain "hyapp/services/user-service/internal/domain/auth" ) const loginRiskIPWhitelistCacheTTL = 30 * time.Second // ListLoginRiskBlockedCountries 返回当前 App 实际生效的登录地区风控词表。 // 静态配置和后台动态配置都下发给 App;App 可异步做本地风险提示,最终封禁仍以服务端 worker 为准。 func (s *Service) ListLoginRiskBlockedCountries(ctx context.Context) ([]authdomain.LoginRiskCountryBlock, error) { if !s.loginRiskPolicy.Enabled { return nil, nil } appCode := appcode.FromContext(ctx) blocks := make([]authdomain.LoginRiskCountryBlock, 0, len(s.loginRiskPolicy.UnsupportedCountries)) seen := map[string]struct{}{} for _, country := range s.loginRiskPolicy.UnsupportedCountries { countryCode := normalizeCountryCode(country) if countryCode == "" { continue } block := authdomain.LoginRiskCountryBlock{ AppCode: appCode, Keyword: countryCode, CountryCode: countryCode, Enabled: true, } blocks = appendUniqueCountryBlock(blocks, seen, block) } if s.authRepository != nil { dynamicBlocks, err := s.authRepository.ListEnabledLoginRiskCountryBlocks(ctx) if err != nil { return nil, err } for _, block := range dynamicBlocks { block.AppCode = appcode.Normalize(block.AppCode) block.Keyword = strings.TrimSpace(block.Keyword) block.CountryCode = normalizeCountryCode(block.CountryCode) block.Enabled = true if block.Keyword == "" { continue } blocks = appendUniqueCountryBlock(blocks, seen, block) } } sort.SliceStable(blocks, func(i, j int) bool { if blocks[i].CountryCode != blocks[j].CountryCode { return blocks[i].CountryCode < blocks[j].CountryCode } return strings.ToLower(blocks[i].Keyword) < strings.ToLower(blocks[j].Keyword) }) return blocks, nil } // CheckLoginRiskIPWhitelisted 判断入口 IP 是否命中后台白名单。 // 白名单是“跳过登录地区屏蔽”的明确运营例外,只做标准化后的单 IP 精确匹配,不扩展为网段或地区推断。 func (s *Service) CheckLoginRiskIPWhitelisted(ctx context.Context, clientIP string) (bool, error) { ip := normalizeLoginRiskIP(clientIP) if ip == "" { return false, nil } ips, err := s.listLoginRiskIPWhitelist(ctx) if err != nil { return false, err } for _, item := range ips { if item == ip { return true, nil } } return false, nil } func (s *Service) listLoginRiskIPWhitelist(ctx context.Context) ([]string, error) { appCode := appcode.FromContext(ctx) if s.ipDecisionCache != nil { ips, ok, err := s.ipDecisionCache.GetIPWhitelist(ctx, appCode) if err == nil && ok { return normalizeLoginRiskIPList(ips), nil } } if s.authRepository == nil { return nil, nil } items, err := s.authRepository.ListEnabledLoginRiskIPWhitelist(ctx) if err != nil { return nil, err } ips := make([]string, 0, len(items)) for _, item := range items { if ip := normalizeLoginRiskIP(item.IPAddress); ip != "" { ips = append(ips, ip) } } ips = normalizeLoginRiskIPList(ips) if s.ipDecisionCache != nil { // 白名单读取结果按 App 整体缓存 30 秒;空列表也缓存,避免无配置时每次登录都回源 DB。 _ = s.ipDecisionCache.SetIPWhitelist(ctx, appCode, ips, loginRiskIPWhitelistCacheTTL) } return ips, nil } func appendUniqueCountryBlock(blocks []authdomain.LoginRiskCountryBlock, seen map[string]struct{}, block authdomain.LoginRiskCountryBlock) []authdomain.LoginRiskCountryBlock { key := strings.ToLower(block.CountryCode + "\x00" + block.Keyword) if _, ok := seen[key]; ok { return blocks } seen[key] = struct{}{} return append(blocks, block) } func (s *Service) countryBlockedByStaticPolicy(countryCode string) bool { for _, country := range s.loginRiskPolicy.UnsupportedCountries { if normalizeCountryCode(country) == countryCode { return true } } return false } func (s *Service) countryBlockedByDynamicPolicy(ctx context.Context, countryCode string) (bool, error) { if s.authRepository == nil { return false, nil } blocks, err := s.authRepository.ListEnabledLoginRiskCountryBlocks(ctx) if err != nil { return false, err } for _, block := range blocks { if normalizeCountryCode(block.CountryCode) == countryCode || normalizeCountryCode(block.Keyword) == countryCode { return true, nil } } return false, nil } func normalizeLoginRiskIP(raw string) string { addr, err := netip.ParseAddr(strings.TrimSpace(raw)) if err != nil { return "" } return addr.String() } func normalizeLoginRiskIPList(raw []string) []string { seen := make(map[string]struct{}, len(raw)) ips := make([]string, 0, len(raw)) for _, item := range raw { ip := normalizeLoginRiskIP(item) if ip == "" { continue } if _, ok := seen[ip]; ok { continue } seen[ip] = struct{}{} ips = append(ips, ip) } sort.Strings(ips) return ips }