2026-07-16 18:51:58 +08:00

195 lines
6.1 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"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/redis/go-redis/v9"
"hyapp/pkg/appcode"
authdomain "hyapp/services/user-service/internal/domain/auth"
)
type RedisIPDecisionCache struct {
client *redis.Client
}
var setRevokedSessionMaxTTLScript = redis.NewScript(`
local current_ttl = redis.call('PTTL', KEYS[1])
local requested_ttl = tonumber(ARGV[2])
if current_ttl == -2 then
redis.call('PSETEX', KEYS[1], requested_ttl, ARGV[1])
return requested_ttl
end
if current_ttl == -1 then
return current_ttl
end
if current_ttl < requested_ttl then
redis.call('PSETEX', KEYS[1], requested_ttl, ARGV[1])
return requested_ttl
end
return current_ttl
`)
func NewRedisIPDecisionCache(client *redis.Client) *RedisIPDecisionCache {
return &RedisIPDecisionCache{client: client}
}
func NewLoginRiskRedisClient(ctx context.Context, addr string, password string, db int) (*redis.Client, error) {
client := redis.NewClient(&redis.Options{
Addr: strings.TrimSpace(addr),
Password: password,
DB: db,
})
if err := client.Ping(ctx).Err(); err != nil {
_ = client.Close()
return nil, err
}
return client, nil
}
func (c *RedisIPDecisionCache) GetIPDecision(ctx context.Context, appCode string, clientIPHash string) (IPDecision, bool, error) {
raw, err := c.client.Get(ctx, loginRiskIPDecisionKey(appCode, clientIPHash)).Result()
if errors.Is(err, redis.Nil) {
return IPDecision{}, false, nil
}
if err != nil {
return IPDecision{}, false, err
}
var decision IPDecision
if err := json.Unmarshal([]byte(raw), &decision); err != nil {
return IPDecision{}, false, err
}
return decision, true, nil
}
func (c *RedisIPDecisionCache) SetIPDecision(ctx context.Context, appCode string, clientIPHash string, decision IPDecision, ttl time.Duration) error {
raw, err := json.Marshal(decision)
if err != nil {
return err
}
return c.client.Set(ctx, loginRiskIPDecisionKey(appCode, clientIPHash), raw, ttl).Err()
}
func (c *RedisIPDecisionCache) SetRevokedSession(ctx context.Context, appCode string, sessionID string, reason string, ttl time.Duration) error {
if c == nil || c.client == nil {
return errors.New("session denylist redis client is unavailable")
}
ttlMs := ttl.Milliseconds()
if ttlMs <= 0 {
return fmt.Errorf("session denylist TTL must be positive")
}
// 多来源会并发撤销同一 sidrotation/reuse/logout/risk普通 SET 会让较短旧任务覆盖较长 deny_until。
// Lua 原子比较 PTTL只允许延长绝对保护时间已有永久 key 也绝不被改短。
return setRevokedSessionMaxTTLScript.Run(ctx, c.client, []string{revokedSessionKey(appCode, sessionID)}, strings.TrimSpace(reason), ttlMs).Err()
}
func (c *RedisIPDecisionCache) SessionDenylistEpoch(ctx context.Context) (string, error) {
if c == nil || c.client == nil {
return "", errors.New("session denylist redis client is unavailable")
}
info, err := c.client.Info(ctx, "server").Result()
if err != nil {
return "", err
}
for _, line := range strings.Split(info, "\n") {
key, value, found := strings.Cut(strings.TrimSpace(line), ":")
if found && key == "run_id" && strings.TrimSpace(value) != "" {
return strings.TrimSpace(value), nil
}
}
return "", errors.New("redis run_id is unavailable")
}
func (c *RedisIPDecisionCache) GetIPWhitelist(ctx context.Context, appCode string) ([]string, bool, error) {
raw, err := c.client.Get(ctx, loginRiskIPWhitelistKey(appCode)).Result()
if errors.Is(err, redis.Nil) {
return nil, false, nil
}
if err != nil {
return nil, false, err
}
var ips []string
if err := json.Unmarshal([]byte(raw), &ips); err != nil {
return nil, false, err
}
return ips, true, nil
}
func (c *RedisIPDecisionCache) SetIPWhitelist(ctx context.Context, appCode string, ips []string, ttl time.Duration) error {
raw, err := json.Marshal(ips)
if err != nil {
return err
}
return c.client.Set(ctx, loginRiskIPWhitelistKey(appCode), raw, ttl).Err()
}
func (c *RedisIPDecisionCache) GetUserWhitelist(ctx context.Context, appCode string) ([]int64, bool, error) {
raw, err := c.client.Get(ctx, loginRiskUserWhitelistKey(appCode)).Result()
if errors.Is(err, redis.Nil) {
return nil, false, nil
}
if err != nil {
return nil, false, err
}
var userIDs []int64
if err := json.Unmarshal([]byte(raw), &userIDs); err != nil {
return nil, false, err
}
return userIDs, true, nil
}
func (c *RedisIPDecisionCache) SetUserWhitelist(ctx context.Context, appCode string, userIDs []int64, ttl time.Duration) error {
raw, err := json.Marshal(userIDs)
if err != nil {
return err
}
return c.client.Set(ctx, loginRiskUserWhitelistKey(appCode), raw, ttl).Err()
}
func (c *RedisIPDecisionCache) GetRegisterRiskConfig(ctx context.Context, appCode string) (authdomain.RegisterRiskConfig, bool, error) {
raw, err := c.client.Get(ctx, registerRiskConfigKey(appCode)).Result()
if errors.Is(err, redis.Nil) {
return authdomain.RegisterRiskConfig{}, false, nil
}
if err != nil {
return authdomain.RegisterRiskConfig{}, false, err
}
var config authdomain.RegisterRiskConfig
if err := json.Unmarshal([]byte(raw), &config); err != nil {
return authdomain.RegisterRiskConfig{}, false, err
}
return config, true, nil
}
func (c *RedisIPDecisionCache) SetRegisterRiskConfig(ctx context.Context, config authdomain.RegisterRiskConfig, ttl time.Duration) error {
raw, err := json.Marshal(config)
if err != nil {
return err
}
return c.client.Set(ctx, registerRiskConfigKey(config.AppCode), raw, ttl).Err()
}
func loginRiskIPDecisionKey(appCode string, clientIPHash string) string {
return "login_risk:ip:" + appcode.Normalize(appCode) + ":" + strings.TrimSpace(clientIPHash)
}
func loginRiskIPWhitelistKey(appCode string) string {
return "login_risk:ip_whitelist:" + appcode.Normalize(appCode)
}
func loginRiskUserWhitelistKey(appCode string) string {
return "login_risk:user_whitelist:" + appcode.Normalize(appCode)
}
func revokedSessionKey(appCode string, sessionID string) string {
return "auth:revoked_session:" + appcode.Normalize(appCode) + ":" + strings.TrimSpace(sessionID)
}
func registerRiskConfigKey(appCode string) string {
return "auth:risk_config:register:" + appcode.Normalize(appCode)
}