用户靓号白名单

This commit is contained in:
zhx 2026-06-30 10:52:52 +08:00
parent 9717929231
commit 82dd3cc300
2 changed files with 132 additions and 20 deletions

View File

@ -13,6 +13,9 @@ import (
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/integration/userclient"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
@ -20,6 +23,7 @@ const (
maxRegionBlockKeywordLength = 128
maxIPWhitelistItems = 500
maxUserWhitelistItems = 500
maxUserWhitelistLookupLength = 64
)
var (
@ -51,11 +55,12 @@ type IPWhitelistItem struct {
}
type UserWhitelistItem struct {
WhitelistID int64 `json:"whitelist_id"`
UserID int64 `json:"user_id,string"`
Enabled bool `json:"enabled"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
WhitelistID int64 `json:"whitelist_id"`
UserID int64 `json:"user_id,string"`
DisplayUserID string `json:"display_user_id"`
Enabled bool `json:"enabled"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type Config struct {
@ -121,7 +126,7 @@ func (s *Service) Replace(ctx context.Context, requestID string, actorID int64,
}
var normalizedUserIDs []int64
if whitelistUserIDs != nil {
normalizedUserIDs, err = normalizeUserIDWhitelist(*whitelistUserIDs)
normalizedUserIDs, err = s.normalizeUserIDWhitelist(ctx, requestID, *whitelistUserIDs)
if err != nil {
return Config{}, err
}
@ -265,7 +270,7 @@ func normalizeIPWhitelist(raw []string) ([]string, error) {
return result, nil
}
func normalizeUserIDWhitelist(raw []string) ([]int64, error) {
func (s *Service) normalizeUserIDWhitelist(ctx context.Context, requestID string, raw []string) ([]int64, error) {
if len(raw) > maxUserWhitelistItems {
return nil, errInvalidPayload
}
@ -276,7 +281,7 @@ func normalizeUserIDWhitelist(raw []string) ([]int64, error) {
if value == "" {
return nil, errInvalidPayload
}
userID, err := strconv.ParseInt(value, 10, 64)
userID, err := s.resolveWhitelistUserID(ctx, requestID, value)
if err != nil || userID <= 0 {
return nil, errInvalidPayload
}
@ -289,6 +294,75 @@ func normalizeUserIDWhitelist(raw []string) ([]int64, error) {
return result, nil
}
func (s *Service) resolveWhitelistUserID(ctx context.Context, requestID string, raw string) (int64, error) {
value := strings.TrimSpace(raw)
if value == "" || utf8.RuneCountInString(value) > maxUserWhitelistLookupLength {
return 0, errInvalidPayload
}
if s.user == nil {
// 本地隔离测试没有 user-service client 时只接受真实 user_id避免把短号误写成未知用户。
numericID, numericOK := parsePositiveInt64(value)
if !numericOK {
return 0, errInvalidPayload
}
return numericID, nil
}
// 后台输入优先按当前 active 展示号解析,默认短号和靓号都由 user-service 统一处理;
// 保存时只落真实 user_id后续用户改短号或靓号过期也不会影响登录风控白名单。
if identity, err := s.user.ResolveDisplayUserID(ctx, userclient.ResolveDisplayUserIDRequest{
RequestID: requestID,
Caller: "admin-server",
DisplayUserID: value,
}); err != nil {
if !isGRPCNotFound(err) && !isGRPCInvalidArgument(err) {
return 0, err
}
} else if identity != nil && identity.UserID > 0 {
return identity.UserID, nil
}
// 展示号没有命中时,纯数字输入再兜底按内部 user_id 校验;非数字靓号未命中就直接视为无效配置。
numericID, numericOK := parsePositiveInt64(value)
if !numericOK {
return 0, errInvalidPayload
}
if found, err := s.userExists(ctx, requestID, numericID); err != nil {
return 0, err
} else if found {
return numericID, nil
}
return 0, errInvalidPayload
}
func (s *Service) userExists(ctx context.Context, requestID string, userID int64) (bool, error) {
user, err := s.user.GetUser(ctx, userclient.GetUserRequest{
RequestID: requestID,
Caller: "admin-server",
UserID: userID,
})
if err != nil {
if isGRPCNotFound(err) {
return false, nil
}
return false, err
}
return user != nil && user.UserID > 0, nil
}
func parsePositiveInt64(text string) (int64, bool) {
parsed, err := strconv.ParseInt(strings.TrimSpace(text), 10, 64)
return parsed, err == nil && parsed > 0
}
func isGRPCNotFound(err error) bool {
return status.Code(err) == codes.NotFound
}
func isGRPCInvalidArgument(err error) bool {
return status.Code(err) == codes.InvalidArgument
}
func (s *Service) refreshWhitelistCache(ctx context.Context, requestID string) error {
if s.user == nil {
return nil
@ -385,10 +459,13 @@ func queryEnabledIPWhitelist(ctx context.Context, db *sql.DB, appCode string) ([
func queryEnabledUserWhitelist(ctx context.Context, db *sql.DB, appCode string) ([]UserWhitelistItem, error) {
rows, err := db.QueryContext(ctx, `
SELECT whitelist_id, user_id, enabled, created_at_ms, updated_at_ms
FROM login_risk_user_whitelist
WHERE app_code = ? AND enabled = 1
ORDER BY user_id ASC, whitelist_id ASC
SELECT w.whitelist_id, w.user_id, COALESCE(u.current_display_user_id, ''), w.enabled, w.created_at_ms, w.updated_at_ms
FROM login_risk_user_whitelist w
LEFT JOIN users u
ON u.app_code = w.app_code
AND u.user_id = w.user_id
WHERE w.app_code = ? AND w.enabled = 1
ORDER BY w.user_id ASC, w.whitelist_id ASC
`, appCode)
if err != nil {
return nil, err
@ -398,7 +475,7 @@ func queryEnabledUserWhitelist(ctx context.Context, db *sql.DB, appCode string)
items := make([]UserWhitelistItem, 0)
for rows.Next() {
var item UserWhitelistItem
if err := rows.Scan(&item.WhitelistID, &item.UserID, &item.Enabled, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
if err := rows.Scan(&item.WhitelistID, &item.UserID, &item.DisplayUserID, &item.Enabled, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
return nil, err
}
if item.UserID > 0 {

View File

@ -7,13 +7,18 @@ import (
"testing"
"hyapp-admin-server/internal/integration/userclient"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type fakeWhitelistCacheUserClient struct {
userclient.Client
called bool
err error
called bool
err error
users map[int64]*userclient.User
identities map[string]*userclient.UserIdentity
}
func (f *fakeWhitelistCacheUserClient) RefreshLoginRiskWhitelistCache(_ context.Context, req userclient.RefreshLoginRiskWhitelistCacheRequest) (*userclient.LoginRiskWhitelistCacheSnapshot, error) {
@ -27,20 +32,50 @@ func (f *fakeWhitelistCacheUserClient) RefreshLoginRiskWhitelistCache(_ context.
return &userclient.LoginRiskWhitelistCacheSnapshot{Refreshed: true, UserWhitelistCount: 2}, nil
}
func TestNormalizeUserIDWhitelistDeduplicatesPositiveIDs(t *testing.T) {
got, err := normalizeUserIDWhitelist([]string{" 900025 ", "900026", "900025"})
func (f *fakeWhitelistCacheUserClient) ResolveDisplayUserID(_ context.Context, req userclient.ResolveDisplayUserIDRequest) (*userclient.UserIdentity, error) {
if req.RequestID != "req-refresh" || req.Caller != "admin-server" {
return nil, errors.New("unexpected resolve request")
}
if identity := f.identities[req.DisplayUserID]; identity != nil {
return identity, nil
}
return nil, status.Error(codes.NotFound, "display user id not found")
}
func (f *fakeWhitelistCacheUserClient) GetUser(_ context.Context, req userclient.GetUserRequest) (*userclient.User, error) {
if req.RequestID != "req-refresh" || req.Caller != "admin-server" {
return nil, errors.New("unexpected get user request")
}
if user := f.users[req.UserID]; user != nil {
return user, nil
}
return nil, status.Error(codes.NotFound, "user not found")
}
func TestNormalizeUserIDWhitelistResolvesDisplayIDsAndInternalIDs(t *testing.T) {
service := NewService(nil, &fakeWhitelistCacheUserClient{
users: map[int64]*userclient.User{
990000: {UserID: 990000},
},
identities: map[string]*userclient.UserIdentity{
"VIP2026": {UserID: 900025, DisplayUserID: "VIP2026"},
"123456789012": {UserID: 900026, DisplayUserID: "123456789012"},
},
})
got, err := service.normalizeUserIDWhitelist(context.Background(), "req-refresh", []string{" VIP2026 ", "123456789012", "990000", "VIP2026"})
if err != nil {
t.Fatalf("normalize user whitelist failed: %v", err)
}
want := []int64{900025, 900026}
want := []int64{900025, 900026, 990000}
if !reflect.DeepEqual(got, want) {
t.Fatalf("normalized user whitelist = %+v, want %+v", got, want)
}
}
func TestNormalizeUserIDWhitelistRejectsInvalidID(t *testing.T) {
for _, raw := range [][]string{{""}, {"0"}, {"-1"}, {"abc"}} {
if _, err := normalizeUserIDWhitelist(raw); !errors.Is(err, errInvalidPayload) {
service := NewService(nil, &fakeWhitelistCacheUserClient{})
for _, raw := range [][]string{{""}, {"0"}, {"-1"}, {"VIP-2026"}, {"123456"}} {
if _, err := service.normalizeUserIDWhitelist(context.Background(), "req-refresh", raw); !errors.Is(err, errInvalidPayload) {
t.Fatalf("normalize %v error = %v, want errInvalidPayload", raw, err)
}
}