This commit is contained in:
wanghu
2026-09-02 09:38:45 +08:00
commit 8774b6cc44
128 changed files with 9382 additions and 0 deletions
@@ -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);
}
}