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 _);
}
}
}
}
}