using System.Collections.Concurrent; namespace MoShiro.Backend.Application; /// 登录限流:每个来源在滑动的一分钟内最多尝试若干次。 public sealed class LoginRateLimiter { private readonly ConcurrentDictionary> _attempts = new(); private readonly BackendOptions _options; private readonly TimeProvider _time; public LoginRateLimiter(BackendOptions options, TimeProvider time) { _options = options; _time = time; } /// 登记一次尝试;超限时返回 false 与建议的重试等待秒数。 public bool TryAcquire(string source, out int retryAfterSeconds) { long now = _time.GetUtcNow().ToUnixTimeMilliseconds(); Queue queue = _attempts.GetOrAdd(source, _ => new Queue()); lock (queue) { while (queue.Count > 0 && now - queue.Peek() >= 60_000) { queue.Dequeue(); } if (queue.Count >= _options.LoginAttemptsPerMinute) { retryAfterSeconds = (int)Math.Max(1, (60_000 - (now - queue.Peek()) + 999) / 1000); return false; } queue.Enqueue(now); retryAfterSeconds = 0; return true; } } /// 清理很久没有尝试的来源,避免字典无限增长。 public void Prune() { long now = _time.GetUtcNow().ToUnixTimeMilliseconds(); foreach ((string key, Queue queue) in _attempts) { lock (queue) { if (queue.Count == 0 || now - queue.Peek() >= 60_000) { _attempts.TryRemove(key, out _); } } } } }