2026-06-23 12:11:05 +08:00

170 lines
4.9 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"
"net/netip"
"sort"
"strings"
"time"
"hyapp/pkg/appcode"
authdomain "hyapp/services/user-service/internal/domain/auth"
)
const loginRiskIPWhitelistCacheTTL = 30 * time.Second
// ListLoginRiskBlockedCountries 返回当前 App 实际生效的登录地区风控词表。
// 静态配置和后台动态配置都下发给 AppApp 可异步做本地风险提示,最终封禁仍以服务端 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
}