2026-06-30 10:47:21 +08:00

295 lines
8.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
loginRiskUserWhitelistCacheTTL = 5 * time.Minute
)
// LoginRiskWhitelistCacheSnapshot 是后台保存配置后主动刷新缓存的结果。
// IP 白名单仍沿用原短 TTL用户白名单按运营要求缓存 5 分钟,避免每个风控任务回源 MySQL。
type LoginRiskWhitelistCacheSnapshot struct {
Refreshed bool
IPWhitelistCount int
UserWhitelistCount int
UserCacheExpiresAtMs int64
}
// 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
}
// CheckLoginRiskUserWhitelisted 判断登录用户是否命中后台白名单。
// 该白名单只在 user-service 登录后风控 worker 中使用gateway 登录前没有可信 user_id不能用它跳过入口快拦。
func (s *Service) CheckLoginRiskUserWhitelisted(ctx context.Context, userID int64) (bool, error) {
if userID <= 0 {
return false, nil
}
userIDs, err := s.listLoginRiskUserWhitelist(ctx)
if err != nil {
return false, err
}
for _, item := range userIDs {
if item == userID {
return true, nil
}
}
return false, nil
}
// RefreshLoginRiskWhitelistCache 从 MySQL 读取当前 App 白名单并立即写入 Redis。
// 后台保存直接调用该方法,避免新增白名单后仍等 5 分钟 TTL 自然过期才生效。
func (s *Service) RefreshLoginRiskWhitelistCache(ctx context.Context) (LoginRiskWhitelistCacheSnapshot, error) {
if s.authRepository == nil {
return LoginRiskWhitelistCacheSnapshot{}, nil
}
ips, err := s.loadLoginRiskIPWhitelist(ctx)
if err != nil {
return LoginRiskWhitelistCacheSnapshot{}, err
}
userIDs, err := s.loadLoginRiskUserWhitelist(ctx)
if err != nil {
return LoginRiskWhitelistCacheSnapshot{}, err
}
snapshot := LoginRiskWhitelistCacheSnapshot{
IPWhitelistCount: len(ips),
UserWhitelistCount: len(userIDs),
UserCacheExpiresAtMs: s.now().Add(loginRiskUserWhitelistCacheTTL).UnixMilli(),
}
if s.ipDecisionCache == nil {
return snapshot, nil
}
// IP 和用户白名单共用一次刷新入口;任一缓存写失败都返回错误,让后台保存页能提示重试刷新。
if err := s.ipDecisionCache.SetIPWhitelist(ctx, appcode.FromContext(ctx), ips, loginRiskIPWhitelistCacheTTL); err != nil {
return LoginRiskWhitelistCacheSnapshot{}, err
}
if err := s.ipDecisionCache.SetUserWhitelist(ctx, appcode.FromContext(ctx), userIDs, loginRiskUserWhitelistCacheTTL); err != nil {
return LoginRiskWhitelistCacheSnapshot{}, err
}
snapshot.Refreshed = true
return snapshot, 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
}
}
ips, err := s.loadLoginRiskIPWhitelist(ctx)
if err != nil {
return nil, err
}
if s.ipDecisionCache != nil {
// 白名单读取结果按 App 整体缓存 30 秒;空列表也缓存,避免无配置时每次登录都回源 DB。
_ = s.ipDecisionCache.SetIPWhitelist(ctx, appCode, ips, loginRiskIPWhitelistCacheTTL)
}
return ips, nil
}
func (s *Service) listLoginRiskUserWhitelist(ctx context.Context) ([]int64, error) {
appCode := appcode.FromContext(ctx)
if s.ipDecisionCache != nil {
userIDs, ok, err := s.ipDecisionCache.GetUserWhitelist(ctx, appCode)
if err == nil && ok {
return normalizeLoginRiskUserIDList(userIDs), nil
}
}
userIDs, err := s.loadLoginRiskUserWhitelist(ctx)
if err != nil {
return nil, err
}
if s.ipDecisionCache != nil {
// 用户白名单按运营要求缓存 5 分钟;空列表也缓存,减少无配置时的任务回源。
_ = s.ipDecisionCache.SetUserWhitelist(ctx, appCode, userIDs, loginRiskUserWhitelistCacheTTL)
}
return userIDs, nil
}
func (s *Service) loadLoginRiskIPWhitelist(ctx context.Context) ([]string, error) {
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)
}
}
return normalizeLoginRiskIPList(ips), nil
}
func (s *Service) loadLoginRiskUserWhitelist(ctx context.Context) ([]int64, error) {
if s.authRepository == nil {
return nil, nil
}
items, err := s.authRepository.ListEnabledLoginRiskUserWhitelist(ctx)
if err != nil {
return nil, err
}
userIDs := make([]int64, 0, len(items))
for _, item := range items {
if item.UserID > 0 {
userIDs = append(userIDs, item.UserID)
}
}
return normalizeLoginRiskUserIDList(userIDs), 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
}
func normalizeLoginRiskUserIDList(raw []int64) []int64 {
seen := make(map[int64]struct{}, len(raw))
userIDs := make([]int64, 0, len(raw))
for _, userID := range raw {
if userID <= 0 {
continue
}
if _, ok := seen[userID]; ok {
continue
}
seen[userID] = struct{}{}
userIDs = append(userIDs, userID)
}
sort.Slice(userIDs, func(i, j int) bool {
return userIDs[i] < userIDs[j]
})
return userIDs
}