- 輔助服務,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與服務器存入的不一樣,或多次請求,驗證不會通過,在此不再貼圖演示了