feat: 建立可靠通知、吊销与跨实例状态
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const statePrefix = "silk:auth:"
|
||||
|
||||
// StateStore 跨实例认证状态存储。
|
||||
type StateStore interface {
|
||||
RevokeToken(ctx context.Context, id string, exp time.Time) error
|
||||
IsTokenRevoked(ctx context.Context, id string) (bool, error)
|
||||
CheckLoginLock(ctx context.Context, key string) (bool, time.Duration, error)
|
||||
RecordLoginFailure(ctx context.Context, key string) error
|
||||
RecordLoginSuccess(ctx context.Context, key string) error
|
||||
}
|
||||
|
||||
var (
|
||||
authState StateStore
|
||||
appEnv string
|
||||
)
|
||||
|
||||
// InitState 设置认证状态存储;store 为 nil 时认证相关接口保守失败。
|
||||
func InitState(store StateStore, env string) {
|
||||
authState = store
|
||||
appEnv = env
|
||||
}
|
||||
|
||||
// RedisState Redis 实现。
|
||||
type RedisState struct {
|
||||
rdb *redis.Client
|
||||
prefix string
|
||||
}
|
||||
|
||||
// NewRedisState 创建 Redis 状态存储。
|
||||
func NewRedisState(rdb *redis.Client) *RedisState {
|
||||
return &RedisState{rdb: rdb, prefix: statePrefix}
|
||||
}
|
||||
|
||||
func (s *RedisState) RevokeToken(ctx context.Context, id string, exp time.Time) error {
|
||||
ttl := time.Until(exp)
|
||||
if ttl <= 0 {
|
||||
return nil
|
||||
}
|
||||
return s.rdb.Set(ctx, s.prefix+"revoked:"+id, "1", ttl).Err()
|
||||
}
|
||||
|
||||
func (s *RedisState) IsTokenRevoked(ctx context.Context, id string) (bool, error) {
|
||||
count, err := s.rdb.Exists(ctx, s.prefix+"revoked:"+id).Result()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (s *RedisState) CheckLoginLock(ctx context.Context, key string) (bool, time.Duration, error) {
|
||||
lockKey := s.prefix + "login-lock:" + key
|
||||
if _, err := s.rdb.Get(ctx, lockKey).Result(); err == redis.Nil {
|
||||
return false, 0, nil
|
||||
} else if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
ttl, err := s.rdb.TTL(ctx, lockKey).Result()
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
return true, ttl, nil
|
||||
}
|
||||
|
||||
var loginFailureScript = redis.NewScript(`
|
||||
local count = redis.call('INCR', KEYS[1])
|
||||
redis.call('EXPIRE', KEYS[1], ARGV[1])
|
||||
if tonumber(count) >= tonumber(ARGV[2]) then
|
||||
redis.call('SET', KEYS[2], '1', 'PX', ARGV[3])
|
||||
end
|
||||
return count
|
||||
`)
|
||||
|
||||
func (s *RedisState) RecordLoginFailure(ctx context.Context, key string) error {
|
||||
return loginFailureScript.Run(ctx, s.rdb,
|
||||
[]string{s.prefix + "login-failures:" + key, s.prefix + "login-lock:" + key},
|
||||
int(failureWindow.Seconds()), maxFailures, int(lockDuration.Milliseconds()),
|
||||
).Err()
|
||||
}
|
||||
|
||||
func (s *RedisState) RecordLoginSuccess(ctx context.Context, key string) error {
|
||||
pipe := s.rdb.Pipeline()
|
||||
pipe.Del(ctx, s.prefix+"login-failures:"+key, s.prefix+"login-lock:"+key)
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func stateUnavailable(operation string) error {
|
||||
msg := "Redis 状态服务不可用,无法" + operation
|
||||
if appEnv == "production" {
|
||||
slog.Error(msg)
|
||||
}
|
||||
return fmt.Errorf("%s", msg)
|
||||
}
|
||||
Reference in New Issue
Block a user