- 辅助服务,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与服务器存入的不一样,或多次请求,验证不会通过,在此不再贴图演示了