195 lines
6.1 KiB
Go
195 lines
6.1 KiB
Go
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")
|
||
}
|
||
// 多来源会并发撤销同一 sid(rotation/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)
|
||
}
|