C#WEB 防重复提交控制

生产环境由于业务较为频繁,若网络或设备出现卡顿可能存在重复提交等问题,该问题通过前端的防止重复提交已经不管用,需要在后端或数据库增加控制。

若项目已经上线一段时间后发生此类问题,整体改造系统的成本较大,可以通过增加拦截器,拦截请求,设定1秒或者一个合理的时间周期为控制条件,如果同一个账户同一个请求路径再次期间内连续请求则拦截本次请求并返回前端错误信息,一定程度上避免业务数据混乱。

本次从.net frameowrk 4.5 级.net Core -.Net 各版本入手,设定公共且兼容的方法来应对并发问题。

本次功能逻辑范围:

1、通过拦截器调用此方法,传入用户、请求路径信息

2、设置白名单,将不需要控制的路径填写在此处,系统判断并发时不会处理白名单内的请求

3、使用内存记录请求时间

4、增加定期内存垃圾处理,将超过10秒未更新的请求记录清空

封装一个方法类:RequestThrottler.cs

复制代码
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

public static class RequestThrottler
{
    // 白名单路径(不区分大小写)
    private static readonly HashSet<string> WhitelistPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
    {
        "/api/health",
        "/login",
        "/logout",
        "/static/",
        // 可扩展
    };

    // 存储结构:key -> { LastAccessTime, LastRequestTime }
    private static readonly ConcurrentDictionary<string, TimestampEntry> Cache =
        new ConcurrentDictionary<string, TimestampEntry>();

    // 清理任务控制
    private static Timer _cleanupTimer;
    private static readonly TimeSpan CleanupInterval = TimeSpan.FromSeconds(10);   // 每10秒清理一次
    private static readonly TimeSpan ExpiryThreshold = TimeSpan.FromSeconds(10);   // 超过10秒未使用则清除

    // 静态构造函数:启动清理定时器
    static RequestThrottler()
    {
        // 使用 Timer 兼容 .NET Framework 4.5 和 .NET Core+
        _cleanupTimer = new Timer(CleanupExpiredEntries, null, CleanupInterval, CleanupInterval);
    }

    public static bool IsAllowed(string userId, string requestPath)
    {
        if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(requestPath))
            return true; // 容错:允许

        var normalizedPath = NormalizePath(requestPath);

        if (IsWhitelisted(normalizedPath))
            return true;

        var key = $"{userId}|{normalizedPath}";
        var now = DateTime.UtcNow;

        // 获取或添加条目,并更新最后访问时间
        var entry = Cache.GetOrAdd(key, new TimestampEntry { LastRequestTime = now });
        
        // 原子性检查是否在1秒内重复请求
        var lastReq = entry.LastRequestTime;
        if ((now - lastReq).TotalSeconds < 1.0)
        {
            // 更新最后访问时间(用于清理判断),但不更新请求时间(防止绕过限流)
            Interlocked.Exchange(ref entry.LastAccessTime, now.Ticks);
            return false;
        }

        // 允许请求:更新请求时间和访问时间
        var newEntry = new TimestampEntry
        {
            LastRequestTime = now,
            LastAccessTime = now.Ticks
        };
        Cache[key] = newEntry; // 覆盖旧值(线程安全)
        return true;
    }

    // 后台清理逻辑
    private static void CleanupExpiredEntries(object state)
    {
        try
        {
            var cutoffTicks = (DateTime.UtcNow - ExpiryThreshold).Ticks;
            var keysToRemove = new List<string>();

            foreach (var kvp in Cache)
            {
                // 如果最后访问时间早于阈值,则标记删除
                if (kvp.Value.LastAccessTime < cutoffTicks)
                {
                    keysToRemove.Add(kvp.Key);
                }
            }

            foreach (var key in keysToRemove)
            {
                Cache.TryRemove(key, out _);
            }
        }
        catch
        {
            // 忽略清理异常,避免 Timer 崩溃
        }
    }

    private static string NormalizePath(string path)
    {
        if (string.IsNullOrEmpty(path)) return path;
        var idx = path.IndexOf('?');
        if (idx >= 0) path = path.Substring(0, idx);
        return path.ToLowerInvariant();
    }

    private static bool IsWhitelisted(string path)
    {
        if (string.IsNullOrEmpty(path)) return false;

        if (WhitelistPaths.Contains(path)) return true;

        foreach (var prefix in WhitelistPaths)
        {
            if (prefix.EndsWith("/") && path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
                return true;
        }

        return false;
    }

    // 内部结构体:存储两个时间戳
    private class TimestampEntry
    {
        public DateTime LastRequestTime; // 上次发起有效请求的时间(用于限流判断)
        public long LastAccessTime;      // 上次被访问的时间(用于清理判断,用 Ticks 提升性能)
    }
}

1、NET Framework 4.5(ASP.NET MVC / Web API)全局拦截(推荐) :通过 ActionFilterAttribute

复制代码
public class ThrottleAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var user = filterContext.HttpContext.User?.Identity?.Name ?? "anonymous";
        var path = filterContext.HttpContext.Request.Path;

        if (!RequestThrottler.IsAllowed(user, path))
        {
            filterContext.Result = new HttpStatusCodeResult(429, "请求过于频繁");
        }
        base.OnActionExecuting(filterContext);
    }
}

2. .NET Core 3.1 / .NET 6 / .NET 7 / .NET 8(ASP.NET Core)

Action Filter(局部控制)
复制代码
public class ThrottleActionFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        var userId = context.HttpContext.User?.Identity?.Name ?? "anonymous";
        var path = context.HttpContext.Request.Path;

        if (!RequestThrottler.IsAllowed(userId, path))
        {
            context.Result = new ObjectResult("请求过于频繁") { StatusCode = 429 };
        }
    }

    public void OnActionExecuted(ActionExecutedContext context) { }
}

// 注册
services.AddScoped<ThrottleActionFilter>();

// 使用
[ServiceFilter(typeof(ThrottleActionFilter))]
public IActionResult Submit() { ... }
相关推荐
aq553560011 小时前
PHP vs Python:30秒看懂核心区别
开发语言·python·php
我是无敌小恐龙11 小时前
Java SE 零基础入门Day01 超详细笔记(开发前言+环境搭建+基础语法)
java·开发语言·人工智能·opencv·spring·机器学习
码云数智-大飞12 小时前
零基础微信小程序制作平台哪个好
开发语言
IT_陈寒12 小时前
Vite的热更新突然失效,原来是因为这个配置
前端·人工智能·后端
神仙别闹12 小时前
基于 MATLAB 实现的 DCT 域的信息隐藏
开发语言·matlab
ZC跨境爬虫13 小时前
3D 地球卫星轨道可视化平台开发 Day8(分步渲染200颗卫星+ 前端分页控制)
前端·python·3d·重构·html
techdashen13 小时前
Go 标准库 JSON 包迎来重大升级:encoding/json/v2 实验版来了
开发语言·golang·json
竹林81813 小时前
RainbowKit快速集成多链钱包连接,我如何从“连不上”到“丝滑切换”
前端·javascript
笨笨狗吞噬者13 小时前
Opus 4.7 使用体验
前端·ai编程
.千余13 小时前
【Linux】基本指令3
linux·服务器·开发语言·学习