.netcore WebApi接口 防止按钮重复点击

 

  • 辅助服务,redisHelper类
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace  MyWebApi.Service
{
    public interface IRedisService
    {
        Task<bool> StringSetAsync(string key, string value, TimeSpan? span);
        Task<bool> LockTakeAsync(string key, string value, TimeSpan span, string prefix = "locker:");
        Task<bool> LockReleaseAsync(string key, string value, string prefix = "locker:");
        Task<string> StringGetAsync(string key);
        Task<bool> KeyDeleteAsync(string key);
        Task<bool> KeyExistsAsync(string key);
        Task<bool> FuzzySearchExistsAsync(string prefix,string merchantId);
    }
    public class RedisService : IRedisService
    {
        IDatabase _redis;
        public RedisService(IDatabase redis)
        {
            _redis = redis;
        }
        public async Task<bool> KeyDeleteAsync(string key)
        {
            return await _redis.KeyDeleteAsync(key);
        }

        public async Task<bool> KeyExistsAsync(string key)
        {
            return await _redis.KeyExistsAsync(key);
        }

        public async Task<bool> StringSetAsync(string key, string value, TimeSpan? span)
        {
            return await _redis.StringSetAsync(key, value, span);
        }
        public async Task<string> StringGetAsync(string key)
        {
            return await _redis.StringGetAsync(key);
        }
        public async Task<bool> LockTakeAsync(string key, string value, TimeSpan span, string prefix = "locker:")
        {
            return await _redis.LockTakeAsync(prefix + key, value, span);
        }
        public async Task<bool> LockReleaseAsync(string key, string value, string prefix = "locker:")
        {
            return await _redis.LockReleaseAsync(prefix + key, value);
        }
        public async Task<bool> FuzzySearchExistsAsync(string prefix,string UserId) 
        {
            var pattern = $"{prefix}:{UserId}*";
            var redisResult =await _redis.ScriptEvaluateAsync(LuaScript.Prepare(
                         //Redis的keys模糊查询:
                         " local res = redis.call('KEYS', @keypattern) " +
                         " return res "), new { @keypattern = pattern });
            string[] preSult = (string[])redisResult;//将返回的结果集转为数组
            return preSult.Length > 0;
        }
    }
}
  • 创建action执行完成后,执行ResultFilterAttribute过滤器,返回multiclick值
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using MyWebApi.Service;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Filters;
using Newtonsoft.Json;

namespace MyWebApi.Filters
{
    public class MulticlickHeader : ResultFilterAttribute
    {
        IRedisService _redisService;
        string _dependencyKey;
        public MulticlickHeader(string dependencyKey, IRedisService redisService)
        {
            _redisService = redisService;
            _dependencyKey = dependencyKey;
        }

        public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
        {
            if (!string.IsNullOrEmpty(_dependencyKey))
            {
                string[] dependencies = _dependencyKey.Split(':');
                string dependencyType = dependencies[0];
                string dependencySource = dependencies[1];
                string dependencyWhenReturnkey = dependencies[2];
                var needReturnKey = "";
                if (dependencyType == "query")
                {
                    needReturnKey = context.HttpContext.Request.Query[dependencySource];
                }
                else if (dependencyType == "body")
                {
                    context.HttpContext.Request.EnableRewind();
                    context.HttpContext.Request.Body.Seek(0, 0);
                    using (var ms = new MemoryStream())
                    {
                        context.HttpContext.Request.Body.CopyTo(ms);
                        var b = ms.ToArray();
                        var body = Encoding.UTF8.GetString(b);
                        needReturnKey = JsonConvert.DeserializeAnonymousType(body, new Dictionary<string, object>())[dependencySource].ToString();
                    }
                }
                if (needReturnKey.ToUpper() == dependencyWhenReturnkey.ToUpper())
                {
                    await GenerateHeader(context);
                }
            }
            else
            {
                await GenerateHeader(context);
            }
            //添加自定义header,返回给前端
            context.HttpContext.Response.Headers.Add("custom_headers",new string[]{"Origin","Accept","Content-Type","Date","multiclick"});
            //Access-Control-Expose-Headers作用是,里面的参数值能被前端获取到
            context.HttpContext.Response.Headers.Add("Access-Control-Expose-Headers", "Origin,Accept,Content-Type,Date,multiclick");
            var resultContext = await next();
        }

        private async Task GenerateHeader(ResultExecutingContext context)
        {
            var value = Guid.NewGuid().ToString("N");
            if (await _redisService.StringSetAsync(value, "10", TimeSpan.FromDays(1)))
            {
                context.HttpContext.Response.Headers.Add("multiclick", new string[] {value});
            }
        }
    }
}
  •  创建Action方法执行前,multikey验证过滤器MulticlickValidateFilter
 
using MyWebApi.Service;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
 

namespace MyWebApi.Filters
{
    public class MulticlickValidateFilter : IAsyncActionFilter
    {
        IRedisService _redisService;
        string _dependencyKey;
        public MulticlickValidateFilter(string dependencyKey, IRedisService redisService)
        {
            _redisService = redisService;
            _dependencyKey = dependencyKey;
        }
        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            if (!string.IsNullOrEmpty(_dependencyKey))
            {
                string[] dependencies = _dependencyKey.Split(':');
                string dependencyType = dependencies[0];
                string dependencySource = dependencies[1];
                string dependencyWhenReturnkey = dependencies[2];
                var needReturnKey = "";
                if (dependencyType == "query")
                {
                    needReturnKey = context.HttpContext.Request.Query[dependencySource];
                }
                else if (dependencyType == "body")
                {
                    context.HttpContext.Request.EnableRewind();
                    context.HttpContext.Request.Body.Seek(0, 0);
                    using (var ms = new MemoryStream())
                    {
                        context.HttpContext.Request.Body.CopyTo(ms);
                        var b = ms.ToArray();
                        var body = Encoding.UTF8.GetString(b);
                        needReturnKey = JsonConvert.DeserializeAnonymousType(body, new Dictionary<string, object>())[dependencySource].ToString();
                    }
                }
                if (needReturnKey.ToUpper() == dependencyWhenReturnkey.ToUpper())
                {
                    await Validate(context, next);
                }
                else
                {
                    var resultContext = await next();
                }
            }
            else
            {
                await Validate(context, next);
            }

        }

        private async Task Validate(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            var ticket = context.HttpContext.Request.Headers["multiclick"];
            if (string.IsNullOrEmpty(ticket))
            {
                context.Result = new JsonResult(  new { code=-1,msg= "无法获取请求ticket" });
                return;
            }

            if (await _redisService.LockTakeAsync(ticket, ticket, TimeSpan.FromDays(1)))
            {
                if (await _redisService.StringGetAsync(ticket) != "100")
                {
                    context.Result = new JsonResult(new { code = -1, msg = "ticket失效,请刷新页面重新获取" } );
                    return;
                }

                var resultContext = await next();
                await _redisService.StringSetAsync(ticket, "200", TimeSpan.FromDays(1));
                await _redisService.LockReleaseAsync(ticket, ticket);
            }
            else
            {
                context.Result = new JsonResult(new { code = -1, msg = "处理中,请勿重复点击" });
                return;
            }
        }
    }
}
  • 在Startup.cs中注入服务
  public void ConfigureServices(IServiceCollection services)
        {

            services.AddScoped<MulticlickValidateFilter>();
            services.AddScoped<MulticlickHeader>();

 

  • 例如,某一账单 按钮需要防止用户重复点击,那么在这个列表展示时,我们把multikey传给浏览器,用户支付时再把multikey传给后端接口
  MulticlickHeader作用是生成唯一key,初始化状态为100存入redis后,反回给浏览器
  /// <summary>
        /// 账单列表
        /// </summary>
        /// <param name="info"></param>
        /// <returns></returns>
        [HttpPost("ListOrder")]
        [TypeFilter(typeof(MulticlickHeader), Arguments = new object[] { "" })]
        public async Task<BaseJsonResult> SearchFinance([FromBody] SearchInfo info)
        {

 

   如支付时,get请求参数值从query中获取,post请求参数值从body中获取

        [HttpGet("PayOrder")]
        [Consumes("application/json")]
        [TypeFilter(typeof(MulticlickHeader), Arguments = new object[] { "query:isSaveDb:false" })]
        [TypeFilter(typeof(MulticlickValidateFilter), Arguments = new object[] { "query:isSaveDb:true" })]
        public async Task<WebApiResult> PayOrder(string order_id, bool isSaveDb)
        {
        [HttpPost("PayOrder")]
        [Consumes("application/json")]
        [TypeFilter(typeof(MulticlickHeader), Arguments = new object[] { "body:isSaveDb:false" })]
        [TypeFilter(typeof(MulticlickValidateFilter), Arguments = new object[] { "body:isSaveDb:true" })]
        public async Task<WebApiResult> PayOrder(string order_id, bool isSaveDb)
        {

   支付时带着Multiclickheader作用是防止,支付过程中再次生成新的key,影响前端浏览器传过来的值失效问题

  获取multiclick值效果图

  如支付请求时,传入的key与服务器存入的不一样,或多次请求,验证不会通过,在此不再贴图演示了

  

posted @ 2021-03-11 23:21  低调码农哥!  阅读(1099)  评论(2编辑  收藏  举报