bag
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
using System.Collections.Concurrent;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace Yh.Platform.AppAuth;
|
||||
|
||||
/// <summary>
|
||||
/// 防重放守卫:同一 (appKey, timestamp, nonce) 只允许提交一次,有效期同请求窗口(默认 5 分钟)。
|
||||
/// </summary>
|
||||
public interface IReplayGuard
|
||||
{
|
||||
/// <summary>true = 首次提交放行;false = 重复提交拒绝。</summary>
|
||||
Task<bool> TryBeginAsync(string appKey, string timestamp, string nonce, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>进程内防重放(单实例部署或 Redis 未配置时使用)。</summary>
|
||||
public sealed class MemoryReplayGuard : IReplayGuard
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, DateTimeOffset> _seen = new(StringComparer.Ordinal);
|
||||
private readonly long _windowMs;
|
||||
private DateTimeOffset _lastSweep = DateTimeOffset.MinValue;
|
||||
private readonly object _sweepLock = new();
|
||||
|
||||
public MemoryReplayGuard(long windowMs = SignUtil.DefaultExpireMs)
|
||||
{
|
||||
_windowMs = windowMs;
|
||||
}
|
||||
|
||||
public Task<bool> TryBeginAsync(string appKey, string timestamp, string nonce, CancellationToken ct = default)
|
||||
{
|
||||
Sweep();
|
||||
var key = appKey + ":" + timestamp + ":" + nonce;
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var expiry = now.AddMilliseconds(_windowMs);
|
||||
// 原子插入:首次出现即放行;已存在且未过期 → 拒绝;已过期 → 允许覆盖重放窗口
|
||||
if (!_seen.TryAdd(key, expiry))
|
||||
{
|
||||
if (_seen.TryGetValue(key, out var existing) && existing > now)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
_seen[key] = expiry;
|
||||
}
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
private void Sweep()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
lock (_sweepLock)
|
||||
{
|
||||
if (now - _lastSweep < TimeSpan.FromSeconds(30))
|
||||
{
|
||||
return;
|
||||
}
|
||||
_lastSweep = now;
|
||||
}
|
||||
foreach (var kv in _seen)
|
||||
{
|
||||
if (kv.Value <= now)
|
||||
{
|
||||
_seen.TryRemove(kv.Key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redis 防重放:SETNX 键 <c>sign:replay:{appKey}:{timestamp}:{nonce}</c>,TTL = 请求窗口。
|
||||
/// </summary>
|
||||
public sealed class RedisReplayGuard : IReplayGuard
|
||||
{
|
||||
private readonly Func<IDatabase?> _redisDatabaseFactory;
|
||||
private readonly long _windowMs;
|
||||
|
||||
public RedisReplayGuard(Func<IDatabase?> redisDatabaseFactory, long windowMs = SignUtil.DefaultExpireMs)
|
||||
{
|
||||
_redisDatabaseFactory = redisDatabaseFactory;
|
||||
_windowMs = windowMs;
|
||||
}
|
||||
|
||||
public async Task<bool> TryBeginAsync(string appKey, string timestamp, string nonce, CancellationToken ct = default)
|
||||
{
|
||||
var redis = _redisDatabaseFactory();
|
||||
if (redis == null)
|
||||
{
|
||||
return true; // Redis 未配置时不拦截
|
||||
}
|
||||
var key = "sign:replay:" + appKey + ":" + timestamp + ":" + nonce;
|
||||
return await redis.StringSetAsync(key, "1", TimeSpan.FromMilliseconds(_windowMs), When.NotExists);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user