網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
在我們的業(yè)務(wù)中,經(jīng)常存在需要通過(guò)發(fā)送驗(yàn)證碼、校驗(yàn)驗(yàn)證碼來(lái)完成的一些業(yè)務(wù)邏輯,比如賬號(hào)注冊(cè)、找回密碼、用戶身份確認(rèn)等。
在該類業(yè)務(wù)中,發(fā)送驗(yàn)證碼的方式可以有各種各樣,比如最常見(jiàn)的手機(jī)驗(yàn)證,最古老的郵箱驗(yàn)證,到現(xiàn)在相對(duì)少見(jiàn)的微信公眾號(hào)、釘釘通知等;而驗(yàn)證碼服務(wù)端存儲(chǔ)的方式也可以各式各樣,比如存儲(chǔ)在關(guān)系型數(shù)據(jù)庫(kù)中,當(dāng)然也可以如本文標(biāo)題所示,存儲(chǔ)在Redis
中。
既然已經(jīng)預(yù)見(jiàn)到了各式各樣的發(fā)送方式,也預(yù)見(jiàn)到了各式各樣的存儲(chǔ)方式,所以,雖然本文標(biāo)題是基于Redis
,但Redis
其實(shí)只是其中的一種存儲(chǔ)方式,如果需要,我們也應(yīng)該可以和方便的切換到其它存儲(chǔ)方式。
上代碼前,我們先看下設(shè)計(jì)中的接口關(guān)系
ICodeHelper
是最終提供發(fā)送驗(yàn)證碼和校驗(yàn)驗(yàn)證碼的最終接口,其關(guān)聯(lián)了ICodeSender
和ICodeStorage
,ICodeSender
即為驗(yàn)證碼發(fā)送方式的約定接口,ICodeStorage
則為驗(yàn)證碼服務(wù)端持久化方式的約定接口。我們可以看到ICodeSender
同樣關(guān)聯(lián)了IContentFormatter
,因?yàn)樽鳛榘l(fā)送方ICodeSender
其實(shí)是不知道如何將要發(fā)送的內(nèi)容組織成一段完整的文本內(nèi)容的,這時(shí)候就需要IContentFormatter
來(lái)組織文本內(nèi)容,至于繼承自IContentFormatter
的IComplexContentFormatter
,則只是IContentFormatter
一個(gè)容器封裝,畢竟對(duì)于不同的業(yè)務(wù)類型,我們需要組織成不同的文本內(nèi)容,通過(guò)IComplexContentFormatter
,我們可以將不同業(yè)務(wù)類型文本內(nèi)容的組織過(guò)程,分散到不同的IContentFormatter
中。
下面我們來(lái)看下上述接口的規(guī)范約定,考慮到代碼的簡(jiǎn)便性,此處我們簡(jiǎn)單的將receiver
接收方定義為了string
,而不是泛型<T>
;業(yè)務(wù)標(biāo)志bizFlag
為了方便接入時(shí)無(wú)需調(diào)整代碼,所以此處也沒(méi)有將該值定義為枚舉,而是同樣定義成了通用性最強(qiáng)的string
。
ICodeStorage
/// <summary> /// 校驗(yàn)碼信息存儲(chǔ)接口 /// </summary> public interface ICodeStorage { /// <summary> /// 將校驗(yàn)碼進(jìn)行持久化,如果接收方和業(yè)務(wù)標(biāo)志組合已經(jīng)存在,則進(jìn)行覆蓋 /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <param name="code">校驗(yàn)碼</param> /// <param name="effectiveTime">校驗(yàn)碼有效時(shí)間范圍</param> /// <returns></returns> Task<bool> SetCode(string receiver, string bizFlag, string code, TimeSpan effectiveTime); /// <summary> /// 校驗(yàn)碼錯(cuò)誤次數(shù)+1,如果校驗(yàn)碼已過(guò)期,則不進(jìn)行任何操作 /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <returns></returns> Task IncreaseCodeErrors(string receiver, string bizFlag); /// <summary> /// 校驗(yàn)碼發(fā)送次數(shù)周期持久化,如果接收方和業(yè)務(wù)標(biāo)志組合已經(jīng)存在,則進(jìn)行覆蓋 /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <param name="period">周期時(shí)間范圍</param> /// <returns></returns> Task<bool> SetPeriod(string receiver, string bizFlag, TimeSpan? period); /// <summary> /// 校驗(yàn)碼周期內(nèi)發(fā)送次數(shù)+1,如果周期已到,則不進(jìn)行任何操作 /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <returns></returns> Task IncreaseSendTimes(string receiver, string bizFlag); /// <summary> /// 獲取校驗(yàn)碼及已嘗試錯(cuò)誤次數(shù),如果校驗(yàn)碼不存在或已過(guò)期,則返回null /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <returns></returns> Task<Tuple<string, int>> GetEffectiveCode(string receiver, string bizFlag); /// <summary> /// 獲取校驗(yàn)碼周期內(nèi)已發(fā)送次數(shù),如果周期已到或未發(fā)送過(guò)任何驗(yàn)證碼,則返回0 /// </summary> /// <param name="receiver"></param> /// <param name="bizFlag"></param> /// <returns></returns> Task<int> GetAreadySendTimes(string receiver, string bizFlag); }
ICodeSender
,請(qǐng)注意IsSupport
方法約定。
/// <summary> /// 校驗(yàn)碼實(shí)際發(fā)送接口 /// </summary> public interface ICodeSender { /// <summary> /// 發(fā)送校驗(yàn)碼內(nèi)容模板 /// </summary> IContentFormatter Formatter { get; } /// <summary> /// 判斷接收者是否符合發(fā)送條件,例如當(dāng)前發(fā)送者只支持郵箱,而接收方為手機(jī)號(hào),則返回結(jié)果應(yīng)當(dāng)為false /// </summary> /// <param name="receiver">接收方</param> /// <returns></returns> bool IsSupport(string receiver); /// <summary> /// 發(fā)送校驗(yàn)碼信息 /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <param name="code">校驗(yàn)碼</param> /// <param name="effectiveTime">校驗(yàn)碼有效時(shí)間范圍</param> /// <returns></returns> Task<bool> Send(string receiver, string bizFlag, string code, TimeSpan effectiveTime); }
IContentFormatter
/// <summary> /// 發(fā)送校驗(yàn)碼內(nèi)容模板接口 /// </summary> public interface IContentFormatter { /// <summary> /// 將指定參數(shù)組織成待發(fā)送的文本內(nèi)容 /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <param name="code">校驗(yàn)碼</param> /// <param name="effectiveTime">校驗(yàn)碼有效時(shí)間范圍</param> /// <returns></returns> string GetContent(string receiver, string bizFlag, string code, TimeSpan effectiveTime); }
IComplexContentFormatter
/// <summary> /// 基于業(yè)務(wù)標(biāo)志的多內(nèi)容模板 /// </summary> public interface IComplexContentFormatter : IContentFormatter { /// <summary> /// 設(shè)置指定業(yè)務(wù)對(duì)應(yīng)的內(nèi)容模板 /// </summary> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <param name="formatter">內(nèi)容模板</param> void SetFormatter(string bizFlag, IContentFormatter formatter); /// <summary> /// 移除指定業(yè)務(wù)對(duì)應(yīng)的內(nèi)容模板,如果沒(méi)有,則返回null /// </summary> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <returns></returns> IContentFormatter RemoveFormatter(string bizFlag); }
ICodeHelper
/// <summary> /// 業(yè)務(wù)校驗(yàn)碼輔助接口 /// </summary> public interface ICodeHelper { /// <summary> /// 校驗(yàn)碼實(shí)際發(fā)送者 /// </summary> ICodeSender Sender { get; } /// <summary> /// 校驗(yàn)碼信息存儲(chǔ)者 /// </summary> ICodeStorage Storage { get; } /// <summary> /// 發(fā)送校驗(yàn)碼 /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <param name="code">校驗(yàn)碼</param> /// <param name="effectiveTime">校驗(yàn)碼有效時(shí)間范圍</param> /// <param name="maxSendLimit">周期內(nèi)最大允許發(fā)送配置,為null則表示無(wú)限制</param> /// <returns></returns> Task<SendResult> SendCode(string receiver, string bizFlag, string code, TimeSpan effectiveTime, PeriodLimit maxSendLimit); /// <summary> /// 驗(yàn)證校驗(yàn)碼是否正確 /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <param name="code">校驗(yàn)碼</param> /// <param name="maxErrorLimit">最大允許錯(cuò)誤次數(shù)</param> /// <returns></returns> Task<VerificationResult> VerifyCode(string receiver, string bizFlag, string code, int maxErrorLimit); }
下面則是接口約定中的一些定義的類和枚舉。
/// <summary> /// 校驗(yàn)碼發(fā)送周期設(shè)置 /// </summary> public class PeriodLimit { /// <summary> /// 周期內(nèi)允許的最大次數(shù) /// </summary> public int MaxLimit { get; set; } /// <summary> /// 周期時(shí)間,如果不設(shè)置,則表示無(wú)周期,此時(shí)<see cref="MaxLimit"/>代表總共只允許發(fā)送多少次 /// </summary> public TimeSpan? Period { get; set; } } /// <summary> /// 校驗(yàn)碼發(fā)送結(jié)果 /// </summary> public enum SendResult { /// <summary> /// 發(fā)送成功 /// </summary> [Description("成功")] Success = 0, /// <summary> /// 超出最大發(fā)送次數(shù) /// </summary> [Description("超出最大發(fā)送次數(shù)")] MaxSendLimit = 11, /// <summary> /// 發(fā)送失敗,指<see cref="ICodeSender"/>的發(fā)送結(jié)果為false /// </summary> [Description("發(fā)送失敗")] FailInSend = 12, /// <summary> /// 無(wú)法發(fā)送,<see cref="ICodeSender.IsSupport(string)"/>結(jié)果為false /// </summary> [Description("無(wú)法發(fā)送")] NotSupprot = 13, } /// <summary> /// 校驗(yàn)碼校驗(yàn)結(jié)果 /// </summary> public enum VerificationResult { /// <summary> /// 校驗(yàn)成功 /// </summary> [Description("成功")] Success = 0, /// <summary> /// 校驗(yàn)碼已過(guò)期 /// </summary> [Description("校驗(yàn)碼已過(guò)期")] Expired = 31, /// <summary> /// 校驗(yàn)碼不一致,校驗(yàn)失敗 /// </summary> [Description("校驗(yàn)失敗")] VerificationFailed = 32, /// <summary> /// 已經(jīng)達(dá)到了最大錯(cuò)誤嘗試次數(shù),需重新發(fā)送新的校驗(yàn)碼 /// </summary> [Description("超出最大錯(cuò)誤次數(shù)")] MaxErrorLimit = 33, }
再下來(lái)就是具體的接口實(shí)現(xiàn)了,當(dāng)然這些實(shí)現(xiàn)也是通用實(shí)現(xiàn)
ContentFormatter
/// <summary> /// 通用的內(nèi)容模板 /// </summary> public class ContentFormatter : IContentFormatter { private Func<string, string, string, TimeSpan, string> _func; /// <summary> /// 通用實(shí)現(xiàn),這樣就無(wú)需每種業(yè)務(wù)類型都要實(shí)現(xiàn)<see cref="IContentFormatter"/> /// </summary> /// <param name="func">傳遞的委托,參數(shù)順序與<see cref="GetContent(string, string, string, TimeSpan)"/>一致</param> public ContentFormatter(Func<string, string, string, TimeSpan, string> func) { this._func = func ?? throw new ArgumentNullException(nameof(func)); } /// <summary> /// 將指定參數(shù)組織成待發(fā)送的文本內(nèi)容 /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <param name="code">校驗(yàn)碼</param> /// <param name="effectiveTime">校驗(yàn)碼有效時(shí)間范圍</param> /// <returns></returns> public string GetContent(string receiver, string bizFlag, string code, TimeSpan effectiveTime) { return this._func.Invoke(receiver, bizFlag, code, effectiveTime); } }
ComplexContentFormatter
using System.Collections.Concurrent; /// <summary> /// 基于業(yè)務(wù)標(biāo)志的多內(nèi)容模板實(shí)現(xiàn) /// </summary> public class ComplexContentFormatter : IComplexContentFormatter { private ConcurrentDictionary<string, IContentFormatter> _dic = new ConcurrentDictionary<string, IContentFormatter>(); /// <summary> /// 設(shè)置指定業(yè)務(wù)對(duì)應(yīng)的內(nèi)容模板 /// </summary> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <param name="formatter">內(nèi)容模板</param> public void SetFormatter(string bizFlag, IContentFormatter formatter) { if (!string.IsNullOrWhiteSpace(bizFlag) && formatter != null) { this._dic.AddOrUpdate(bizFlag, formatter, (k, v) => formatter); } } /// <summary> /// 移除指定業(yè)務(wù)對(duì)應(yīng)的內(nèi)容模板,如果沒(méi)有,則返回null /// </summary> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <returns></returns> public IContentFormatter RemoveFormatter(string bizFlag) { if (!string.IsNullOrWhiteSpace(bizFlag) && this._dic.TryRemove(bizFlag, out IContentFormatter formatter)) { return formatter; } return null; } /// <summary> /// 將指定參數(shù)組織成待發(fā)送的文本內(nèi)容 /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <param name="code">校驗(yàn)碼</param> /// <param name="effectiveTime">校驗(yàn)碼有效時(shí)間范圍</param> /// <returns></returns> public string GetContent(string receiver, string bizFlag, string code, TimeSpan effectiveTime) { if (string.IsNullOrWhiteSpace(bizFlag)) { throw new ArgumentNullException(nameof(bizFlag)); } this._dic.TryGetValue(bizFlag, out IContentFormatter formatter); if (formatter == null) { throw new KeyNotFoundException(nameof(formatter)); } return formatter.GetContent(receiver, bizFlag, code, effectiveTime); } }
CodeHelper
,注意該類除了實(shí)現(xiàn)ICodeHelper
外,還提供了一個(gè)用于生成隨機(jī)驗(yàn)證碼的靜態(tài)方法GetRandomNumber
。
/// <summary> /// 業(yè)務(wù)校驗(yàn)碼輔助接口實(shí)現(xiàn) /// </summary> public class CodeHelper : ICodeHelper { /// <summary> /// 基于接口實(shí)現(xiàn),可依賴注入 /// </summary> /// <param name="sender"></param> /// <param name="storage"></param> public CodeHelper(ICodeSender sender, ICodeStorage storage) { this.Sender = sender ?? throw new ArgumentNullException(nameof(sender)); this.Storage = storage ?? throw new ArgumentNullException(nameof(storage)); } /// <summary> /// 校驗(yàn)碼實(shí)際發(fā)送者 /// </summary> public ICodeSender Sender { get; } /// <summary> /// 校驗(yàn)碼信息存儲(chǔ)者 /// </summary> public ICodeStorage Storage { get; } /// <summary> /// 發(fā)送校驗(yàn)碼 /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <param name="code">校驗(yàn)碼</param> /// <param name="effectiveTime">校驗(yàn)碼有效時(shí)間范圍</param> /// <param name="maxSendLimit">周期內(nèi)最大允許發(fā)送配置,為null則表示無(wú)限制</param> public async Task<SendResult> SendCode(string receiver, string bizFlag, string code, TimeSpan effectiveTime, PeriodLimit maxSendLimit) { var result = SendResult.NotSupprot; if (this.Sender.IsSupport(receiver)) { result = SendResult.MaxSendLimit; bool canSend = maxSendLimit == null; int sendTimes = 0; if (!canSend) { sendTimes = await this.Storage.GetAreadySendTimes(receiver, bizFlag).ConfigureAwait(false); canSend = sendTimes < maxSendLimit.MaxLimit; } if (canSend) { result = SendResult.FailInSend; if (await this.Sender.Send(receiver, bizFlag, code, effectiveTime).ConfigureAwait(false) && await this.Storage.SetCode(receiver, bizFlag, code, effectiveTime).ConfigureAwait(false)) { result = SendResult.Success; if (maxSendLimit != null) { if (sendTimes == 0) { await this.Storage.SetPeriod(receiver, bizFlag, maxSendLimit.Period).ConfigureAwait(false); } else { await this.Storage.IncreaseSendTimes(receiver, bizFlag).ConfigureAwait(false); } } } } } return result; } /// <summary> /// 驗(yàn)證校驗(yàn)碼是否正確 /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <param name="code">校驗(yàn)碼</param> /// <param name="maxErrorLimit">最大允許錯(cuò)誤次數(shù)</param> /// <returns></returns> public async Task<VerificationResult> VerifyCode(string receiver, string bizFlag, string code, int maxErrorLimit) { var result = VerificationResult.Expired; var vCode = await this.Storage.GetEffectiveCode(receiver, bizFlag).ConfigureAwait(false); if (vCode != null && !string.IsNullOrWhiteSpace(vCode.Item1)) { result = VerificationResult.MaxErrorLimit; if (vCode.Item2 < maxErrorLimit) { result = VerificationResult.Success; if (!string.Equals(vCode.Item1, code, StringComparison.OrdinalIgnoreCase)) { result = VerificationResult.VerificationFailed; await this.Storage.IncreaseCodeErrors(receiver, bizFlag).ConfigureAwait(false); } } } return result; } /// <summary> /// 獲取由數(shù)字組成的校驗(yàn)碼 /// </summary> /// <param name="maxLength">校驗(yàn)碼長(zhǎng)度</param> /// <returns></returns> public static string GetRandomNumber(int maxLength = 6) { if (maxLength <= 0 || maxLength >= 10) { throw new ArgumentOutOfRangeException($"{nameof(maxLength)} must between {1} and {9}."); } var rd = Math.Abs(Guid.NewGuid().GetHashCode()); var tmpX = (int)Math.Pow(10, maxLength); return (rd % tmpX).ToString().PadLeft(maxLength, '0'); } }
除了上述標(biāo)準(zhǔn)通用實(shí)現(xiàn),還有一些半通用實(shí)現(xiàn),比如本文標(biāo)題中的Redis
,所謂半通用,就是指你可以直接拿來(lái)用,但有可能不符合你的技術(shù)場(chǎng)景,此時(shí)你需要自己重寫(xiě)一份。
CodeStorageWithRedisCache
,注意該類庫(kù)采用了StackExchange.Redis.Extensions.Core
,你可以在nuget
上下載該類庫(kù),如果你對(duì)默認(rèn)的Redis
鍵值生成方式不滿意,你也可以通過(guò)重寫(xiě)GetKey
方法來(lái)指定新的鍵值生成方式。當(dāng)然,因?yàn)閷?shí)際存儲(chǔ)在Redis
中的數(shù)據(jù)都只是一些簡(jiǎn)單數(shù)據(jù),并不需要額外的序列化過(guò)程,實(shí)際你也可以直接使用StackExchange.Redis
。
/// <summary> /// 校驗(yàn)碼信息存儲(chǔ)到Redis /// </summary> public class CodeStorageWithRedisCache : ICodeStorage { private readonly IRedisCacheClient _client; private const string CodeValueHashKey = "Code"; private const string CodeErrorHashKey = "Error"; private const string PeriodHashKey = "Period"; /// <summary> /// Code緩存Key值前綴 /// </summary> public string CodeKeyPrefix { get; set; } = "CC"; /// <summary> /// Period緩存Key值前綴 /// </summary> public string PeriodKeyPrefix { get; set; } = "CCT"; /// <summary> /// 緩存寫(xiě)入Redis哪個(gè)庫(kù) /// </summary> public int DbNumber { get; set; } = 8; /// <summary> /// 基于RedisCacheClient的構(gòu)造函數(shù) /// </summary> /// <param name="client"></param> public CodeStorageWithRedisCache(IRedisCacheClient client) { this._client = client; } /// <summary> /// 獲取校驗(yàn)碼周期內(nèi)已發(fā)送次數(shù),如果周期已到或未發(fā)送過(guò)任何驗(yàn)證碼,則返回0 /// </summary> /// <param name="receiver"></param> /// <param name="bizFlag"></param> /// <returns></returns> public async Task<int> GetAreadySendTimes(string receiver, string bizFlag) { var db = this.GetDatabase(); var key = this.GetPeriodKey(receiver, bizFlag); var times = await db.HashGetAsync<int>(key, PeriodHashKey).ConfigureAwait(false); #if DEBUG Console.WriteLine("Method:{0} Result:{1}", nameof(GetAreadySendTimes), times); #endif return times; } /// <summary> /// 獲取校驗(yàn)碼及已嘗試錯(cuò)誤次數(shù),如果校驗(yàn)碼不存在或已過(guò)期,則返回null /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <returns></returns> public async Task<Tuple<string, int>> GetEffectiveCode(string receiver, string bizFlag) { var db = this.GetDatabase(); var key = this.GetCodeKey(receiver, bizFlag); if (await db.ExistsAsync(key).ConfigureAwait(false)) { var code = await db.HashGetAsync<string>(key, CodeValueHashKey).ConfigureAwait(false); var errors = await db.HashGetAsync<int>(key, CodeErrorHashKey).ConfigureAwait(false); #if DEBUG Console.WriteLine("Method:{0} Result: Code {1} Errors {2} ", nameof(GetEffectiveCode), code, errors); #endif return Tuple.Create(code, errors); } return null; } /// <summary> /// 校驗(yàn)碼錯(cuò)誤次數(shù)+1,如果校驗(yàn)碼已過(guò)期,則不進(jìn)行任何操作 /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <returns></returns> public async Task IncreaseCodeErrors(string receiver, string bizFlag) { var db = this.GetDatabase(); var key = this.GetCodeKey(receiver, bizFlag); if (await db.ExistsAsync(key).ConfigureAwait(false)) { var errors = await db.HashGetAsync<int>(key, CodeErrorHashKey).ConfigureAwait(false); await db.HashSetAsync(key, CodeErrorHashKey, errors + 1).ConfigureAwait(false); } } /// <summary> /// 校驗(yàn)碼周期內(nèi)發(fā)送次數(shù)+1,如果周期已到,則不進(jìn)行任何操作 /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <returns></returns> public async Task IncreaseSendTimes(string receiver, string bizFlag) { var db = this.GetDatabase(); var key = this.GetPeriodKey(receiver, bizFlag); if (await db.ExistsAsync(key).ConfigureAwait(false)) { var times = await db.HashGetAsync<int>(key, PeriodHashKey).ConfigureAwait(false); await db.HashSetAsync(key, PeriodHashKey, times + 1).ConfigureAwait(false); } } /// <summary> /// 將校驗(yàn)碼進(jìn)行持久化,如果接收方和業(yè)務(wù)標(biāo)志組合已經(jīng)存在,則進(jìn)行覆蓋 /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <param name="code">校驗(yàn)碼</param> /// <param name="effectiveTime">校驗(yàn)碼有效時(shí)間范圍</param> /// <returns></returns> public async Task<bool> SetCode(string receiver, string bizFlag, string code, TimeSpan effectiveTime) { var db = this.GetDatabase(); var key = this.GetCodeKey(receiver, bizFlag); await db.RemoveAsync(key).ConfigureAwait(false); var ret = await db.HashSetAsync(key, CodeValueHashKey, code).ConfigureAwait(false) && await db.HashSetAsync(key, CodeErrorHashKey, 0).ConfigureAwait(false) && await db.UpdateExpiryAsync(key, effectiveTime); #if DEBUG Console.WriteLine("Method:{0} Result:{1}", nameof(SetCode), ret); #endif return ret; } /// <summary> /// 校驗(yàn)碼發(fā)送次數(shù)周期持久化,如果接收方和業(yè)務(wù)標(biāo)志組合已經(jīng)存在,則進(jìn)行覆蓋 /// </summary> /// <param name="receiver">接收方</param> /// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param> /// <param name="period">周期時(shí)間范圍</param> /// <returns></returns> public async Task<bool> SetPeriod(string receiver, string bizFlag, TimeSpan? period) { var db = this.GetDatabase(); var key = this.GetPeriodKey(receiver, bizFlag); await db.RemoveAsync(key).ConfigureAwait(false); var ret = await db.HashSetAsync(key, PeriodHashKey, 1).ConfigureAwait(false); if (period.HasValue) { ret = ret && await db.UpdateExpiryAsync(key, period.Value); } #if DEBUG Console.WriteLine("Method:{0} Result:{1}", nameof(SetPeriod), ret); #endif return ret; } /// <summary> /// 組織Redis鍵值 /// </summary> /// <param name="receiver"></param> /// <param name="bizFlag"></param> /// <param name="prefix"></param> /// <returns></returns> protected virtual string GetKey(string receiver, string bizFlag, string prefix) { return string.Format("{0}:{1}:{2}", prefix, bizFlag, receiver); } private string GetPeriodKey(string receiver, string bizFlag) { return this.GetKey(receiver, bizFlag, this.PeriodKeyPrefix); } private string GetCodeKey(string receiver, string bizFlag) { return this.GetKey(receiver, bizFlag, this.CodeKeyPrefix); } private IRedisDatabase GetDatabase() { return this._client.GetDb(this.DbNumber); } }
最后,就是不可能通用的實(shí)現(xiàn)了,對(duì)于ICodeSender
而言,先不說(shuō)發(fā)送方式不同,就算相同,比如都是手機(jī),那也還有不同的短信供應(yīng)商,所以此處必須要使用者按自己的實(shí)際業(yè)務(wù)來(lái)實(shí)現(xiàn),為了方便舉例,這里我寫(xiě)了一個(gè)在控制臺(tái)輸出驗(yàn)證碼內(nèi)容的實(shí)現(xiàn)。
ConsoleSender
,注意IsSupport
在此處輸出true
,代表支持任意receiver
/// <summary> /// 在控制臺(tái)輸出校驗(yàn)碼 /// </summary> public class ConsoleSender : ICodeSender { public ConsoleSender(IContentFormatter formatter) { this.Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); } public IContentFormatter Formatter { get; } public bool IsSupport(string receiver) => true; public Task<bool> Send(string receiver, string bizFlag, string code, TimeSpan effectiveTime) { var content = this.Formatter.GetContent(receiver, bizFlag, code, effectiveTime); Console.WriteLine("發(fā)送內(nèi)容:{0}", content); return Task.FromResult(true); } }
最后則是如何使用的代碼例子,注意此處Redis
序列化方式采用了StackExchange.Redis.Extensions.Newtonsoft
,你可以根據(jù)實(shí)際需要采用其它序列化方式,比如StackExchange.Redis.Extensions.Protobuf
等,你同樣可以在nuget
上下載到這些類庫(kù)。
static void CheckCodeHelperDemo() { var redisConfig = new RedisConfiguration { Hosts = new RedisHost[] { new RedisHost{ Host="127.0.0.1", Port=6379 } } }; var bizFlag = "forgetPassword"; var receiver = "Receiver"; var effectiveTime = TimeSpan.FromMinutes(1); var redisManager = new RedisCacheConnectionPoolManager(redisConfig); var redisClient = new RedisCacheClient(redisManager, new NewtonsoftSerializer(), redisConfig);//new ProtobufSerializer(); var storage = new CodeStorageWithRedisCache(redisClient); var simpleFormatter = new ContentFormatter( (r, b, c, e) => $"{r}您好,您的忘記密碼驗(yàn)證碼為{c},有效期為{(int)e.TotalSeconds}秒."); var formatter = new ComplexContentFormatter(); formatter.SetFormatter(bizFlag, simpleFormatter); var sender = new ConsoleSender(formatter); //如果就一個(gè)業(yè)務(wù)場(chǎng)景,也可以直接用simpleFormatter //var tmp = storage.SetPeriod(receiver, bizFlag, TimeSpan.FromMinutes(20)).Result; var helper = new CodeHelper(sender, storage); var code = CodeHelper.GetRandomNumber(); var sendResult = helper.SendCode(receiver, bizFlag, code, effectiveTime, new PeriodLimit { MaxLimit = 5, Period = TimeSpan.FromMinutes(20) }).Result; Console.WriteLine("發(fā)送結(jié)果:{0}", sendResult); if (sendResult == SendResult.Success) { Console.WriteLine("*****************************"); while (true) { Console.WriteLine("請(qǐng)輸入校驗(yàn)碼:"); var vCode = Console.ReadLine(); var vResult = helper.VerifyCode(receiver, bizFlag, vCode, 3).Result; Console.WriteLine("校驗(yàn)碼 {0} 校驗(yàn)結(jié)果:{1}", vCode, vResult); if (vResult != VerificationResult.VerificationFailed) { break; } } } redisManager.Dispose(); }
最后則是不同測(cè)試場(chǎng)景的一些截圖
驗(yàn)證碼校驗(yàn)失敗達(dá)到允許次數(shù)上限
校驗(yàn)碼已過(guò)期
校驗(yàn)碼驗(yàn)證成功
校驗(yàn)碼周期內(nèi)允許的發(fā)送次數(shù)已達(dá)到上限
最后,上述完整的代碼可見(jiàn)github。
原文鏈接:https://blog.csdn.net/starfd/article/details/90474366
相關(guān)推薦
- 2022-04-18 使用numpy對(duì)數(shù)組求平均時(shí)如何忽略nan值_python
- 2022-05-01 Android?模擬地圖定位功能的實(shí)現(xiàn)_Android
- 2022-06-16 Python數(shù)據(jù)結(jié)構(gòu)之遞歸方法詳解_python
- 2022-11-03 C#事件中關(guān)于sender的用法解讀_C#教程
- 2022-09-26 tomcat下載安裝及配置環(huán)境變量,但打開(kāi)startup文件出現(xiàn)閃退問(wèn)題解決方法
- 2022-03-15 React?Router?V6更新內(nèi)容詳解_React
- 2022-09-24 深入理解C#委托delegate的使用_C#教程
- 2022-04-06 一篇文章帶你了解C/C++的回調(diào)函數(shù)_C 語(yǔ)言
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支