# ASP.NET 中怎么利用WebApi服務接口防止重復請求
## 引言
在Web應用開發中,重復請求是一個常見但容易被忽視的問題。當用戶頻繁點擊提交按鈕、網絡延遲導致多次請求或惡意攻擊時,都可能產生重復請求。這些重復請求可能導致數據不一致、資源浪費甚至業務邏輯錯誤。本文將深入探討在ASP.NET WebAPI中如何有效防止重復請求。
## 一、重復請求的常見場景
### 1.1 用戶操作導致的重復請求
- 快速多次點擊提交按鈕
- 瀏覽器刷新導致的表單重復提交
- 后退/前進操作引起的請求重發
### 1.2 網絡問題引發的重復請求
- 請求超時后的客戶端自動重試
- 網絡抖動導致的請求重發
### 1.3 惡意攻擊行為
- 自動化腳本的暴力請求
- CSRF攻擊嘗試
## 二、防止重復請求的核心思路
### 2.1 冪等性設計
```csharp
// 示例:冪等的PUT請求
[HttpPut("orders/{id}")]
public IActionResult UpdateOrder(int id, [FromBody] Order order)
{
// 無論調用多少次,結果都相同
_repository.UpdateOrInsert(id, order);
return Ok();
}
為每個請求分配唯一Token,服務端進行校驗。
限制單位時間內的請求頻率。
public class PreventDuplicateRequestAttribute : ActionFilterAttribute
{
private static readonly ConcurrentDictionary<string, bool> _processingRequests = new();
private const int DefaultTimeoutSeconds = 5;
public override void OnActionExecuting(ActionExecutingContext context)
{
var requestKey = GenerateRequestKey(context);
if (_processingRequests.TryAdd(requestKey, true))
{
// 設置過期時間自動移除
var removalTimer = new Timer(_ =>
{
_processingRequests.TryRemove(requestKey, out _);
}, null, DefaultTimeoutSeconds * 1000, Timeout.Infinite);
context.HttpContext.Items["RemovalTimer"] = removalTimer;
}
else
{
context.Result = new ConflictObjectResult("請求正在處理中,請勿重復提交");
}
}
private string GenerateRequestKey(ActionExecutingContext context)
{
// 組合用戶ID+Action+參數作為唯一鍵
var userId = context.HttpContext.User.Identity.Name ?? "anonymous";
var action = context.ActionDescriptor.RouteValues["action"];
var parameters = JsonSerializer.Serialize(context.ActionArguments);
return $"{userId}_{action}_{parameters}";
}
public override void OnActionExecuted(ActionExecutedContext context)
{
var requestKey = GenerateRequestKey(context);
if (context.HttpContext.Items["RemovalTimer"] is Timer timer)
{
timer.Dispose();
}
_processingRequests.TryRemove(requestKey, out _);
}
}
public class RedisDistributedLock
{
private readonly IDatabase _redisDb;
private readonly TimeSpan _defaultExpiry = TimeSpan.FromSeconds(30);
public RedisDistributedLock(IConnectionMultiplexer redis)
{
_redisDb = redis.GetDatabase();
}
public async Task<bool> AcquireLockAsync(string key, string value)
{
return await _redisDb.LockTakeAsync(key, value, _defaultExpiry);
}
public async Task ReleaseLockAsync(string key, string value)
{
await _redisDb.LockReleaseAsync(key, value);
}
}
// 在Controller中使用
[HttpPost("payments")]
[PreventDuplicateRequest]
public async Task<IActionResult> ProcessPayment([FromBody] PaymentRequest request)
{
var lockKey = $"payment_{request.OrderId}";
var lockValue = Guid.NewGuid().ToString();
if (!await _redisLock.AcquireLockAsync(lockKey, lockValue))
{
return Conflict("訂單正在處理中");
}
try
{
// 處理支付邏輯
return Ok();
}
finally
{
await _redisLock.ReleaseLockAsync(lockKey, lockValue);
}
}
// 使用axios攔截器
let pendingRequests = new Map();
axios.interceptors.request.use(config => {
const requestKey = `${config.method}-${config.url}-${JSON.stringify(config.data)}`;
if (pendingRequests.has(requestKey)) {
return Promise.reject(new Error('重復請求已取消'));
}
const cancelToken = new axios.CancelToken(cancel => {
pendingRequests.set(requestKey, cancel);
});
config.cancelToken = cancelToken;
return config;
});
axios.interceptors.response.use(response => {
const requestKey = `${response.config.method}-${response.config.url}-${JSON.stringify(response.config.data)}`;
pendingRequests.delete(requestKey);
return response;
}, error => {
if (!axios.isCancel(error)) {
const requestKey = `${error.config.method}-${error.config.url}-${JSON.stringify(error.config.data)}`;
pendingRequests.delete(requestKey);
}
return Promise.reject(error);
});
public string GenerateRequestHash(HttpRequestMessage request)
{
var contentHash = request.Content != null
? ComputeHash(await request.Content.ReadAsStringAsync())
: string.Empty;
return $"{request.Method}-{request.RequestUri}-{contentHash}";
}
private string ComputeHash(string input)
{
using var sha256 = SHA256.Create();
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(input));
return Convert.ToBase64String(bytes);
}
在JWT中包含請求唯一標識(nonce):
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
// 其他配置...
ValidateNonce = true,
NonceValidator = (nonce, securityToken, validationParameters) =>
{
var cache = validationParameters.NonceCache;
return !cache.Contains(nonce) && cache.TryAdd(nonce, DateTime.UtcNow);
}
};
});
public class RateLimiter
{
private readonly ConcurrentDictionary<string, LinkedList<DateTime>> _requestLogs = new();
private readonly int _maxRequests;
private readonly TimeSpan _timeWindow;
public bool TryAcquire(string clientId)
{
var now = DateTime.UtcNow;
var windowStart = now - _timeWindow;
var requestTimes = _requestLogs.GetOrAdd(clientId, _ => new LinkedList<DateTime>());
lock (requestTimes)
{
// 移除過期記錄
while (requestTimes.Count > 0 && requestTimes.First.Value < windowStart)
{
requestTimes.RemoveFirst();
}
if (requestTimes.Count < _maxRequests)
{
requestTimes.AddLast(now);
return true;
}
}
return false;
}
}
| 方案 | 適用場景 | 優點 | 缺點 |
|---|---|---|---|
| ActionFilter | 單機環境 | 實現簡單,零依賴 | 不適用于分布式環境 |
| Redis分布式鎖 | 分布式系統 | 可靠性高,性能好 | 需要Redis基礎設施 |
| 前端攔截 | 用戶體驗優化 | 減少無效請求 | 無法防止惡意繞過 |
| 滑動窗口限流 | API流量控制 | 精細控制請求頻率 | 實現復雜度較高 |
防止重復請求是構建健壯WebAPI的重要環節。通過本文介紹的各種方案,開發者可以根據實際業務場景選擇合適的技術組合。在ASP.NET Core的靈活架構下,我們可以通過中間件、過濾器、分布式鎖等多種方式優雅地解決這一問題,既保證了系統穩定性,又提升了用戶體驗。 “`
這篇文章提供了從原理到實踐的完整解決方案,包含了多種技術實現和對比分析,總字數約2000字。您可以根據實際需求調整代碼示例或補充更多細節。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。