using System.Collections.Concurrent; using StackExchange.Redis; namespace Yh.Platform.AppAuth; /// /// 防重放守卫:同一 (appKey, timestamp, nonce) 只允许提交一次,有效期同请求窗口(默认 5 分钟)。 /// public interface IReplayGuard { /// true = 首次提交放行;false = 重复提交拒绝。 Task TryBeginAsync(string appKey, string timestamp, string nonce, CancellationToken ct = default); } /// 进程内防重放(单实例部署或 Redis 未配置时使用)。 public sealed class MemoryReplayGuard : IReplayGuard { private readonly ConcurrentDictionary _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 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 _); } } } } /// /// Redis 防重放:SETNX 键 sign:replay:{appKey}:{timestamp}:{nonce},TTL = 请求窗口。 /// public sealed class RedisReplayGuard : IReplayGuard { private readonly Func _redisDatabaseFactory; private readonly long _windowMs; public RedisReplayGuard(Func redisDatabaseFactory, long windowMs = SignUtil.DefaultExpireMs) { _redisDatabaseFactory = redisDatabaseFactory; _windowMs = windowMs; } public async Task 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); } }