Files
yunhai.netcore.tool/BasePlatformDotnet/src/Yh.Platform.AppAuth/ReplayGuard.cs
T
2026-09-02 09:38:45 +08:00

92 lines
3.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}