基于Redis驗證碼發(fā)送及校驗方案實現(xiàn)
在我們的業(yè)務(wù)中,經(jīng)常存在需要通過發(fā)送驗證碼、校驗驗證碼來完成的一些業(yè)務(wù)邏輯,比如賬號注冊、找回密碼、用戶身份確認等。
在該類業(yè)務(wù)中,發(fā)送驗證碼的方式可以有各種各樣,比如最常見的手機驗證,最古老的郵箱驗證,到現(xiàn)在相對少見的微信公眾號、釘釘通知等;而驗證碼服務(wù)端存儲的方式也可以各式各樣,比如存儲在關(guān)系型數(shù)據(jù)庫中,當(dāng)然也可以如本文標(biāo)題所示,存儲在Redis中。
既然已經(jīng)預(yù)見到了各式各樣的發(fā)送方式,也預(yù)見到了各式各樣的存儲方式,所以,雖然本文標(biāo)題是基于Redis,但Redis其實只是其中的一種存儲方式,如果需要,我們也應(yīng)該可以和方便的切換到其它存儲方式。
上代碼前,我們先看下設(shè)計中的接口關(guān)系

ICodeHelper是最終提供發(fā)送驗證碼和校驗驗證碼的最終接口,其關(guān)聯(lián)了ICodeSender和ICodeStorage,ICodeSender即為驗證碼發(fā)送方式的約定接口,ICodeStorage則為驗證碼服務(wù)端持久化方式的約定接口。我們可以看到ICodeSender同樣關(guān)聯(lián)了IContentFormatter,因為作為發(fā)送方ICodeSender其實是不知道如何將要發(fā)送的內(nèi)容組織成一段完整的文本內(nèi)容的,這時候就需要IContentFormatter來組織文本內(nèi)容,至于繼承自IContentFormatter的IComplexContentFormatter,則只是IContentFormatter一個容器封裝,畢竟對于不同的業(yè)務(wù)類型,我們需要組織成不同的文本內(nèi)容,通過IComplexContentFormatter,我們可以將不同業(yè)務(wù)類型文本內(nèi)容的組織過程,分散到不同的IContentFormatter中。
下面我們來看下上述接口的規(guī)范約定,考慮到代碼的簡便性,此處我們簡單的將receiver接收方定義為了string,而不是泛型<T>;業(yè)務(wù)標(biāo)志bizFlag為了方便接入時無需調(diào)整代碼,所以此處也沒有將該值定義為枚舉,而是同樣定義成了通用性最強的string。
ICodeStorage
/// <summary>
/// 校驗碼信息存儲接口
/// </summary>
public interface ICodeStorage
{
/// <summary>
/// 將校驗碼進行持久化,如果接收方和業(yè)務(wù)標(biāo)志組合已經(jīng)存在,則進行覆蓋
/// </summary>
/// <param name="receiver">接收方</param>
/// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param>
/// <param name="code">校驗碼</param>
/// <param name="effectiveTime">校驗碼有效時間范圍</param>
/// <returns></returns>
Task<bool> SetCode(string receiver, string bizFlag, string code, TimeSpan effectiveTime);
/// <summary>
/// 校驗碼錯誤次數(shù)+1,如果校驗碼已過期,則不進行任何操作
/// </summary>
/// <param name="receiver">接收方</param>
/// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param>
/// <returns></returns>
Task IncreaseCodeErrors(string receiver, string bizFlag);
/// <summary>
/// 校驗碼發(fā)送次數(shù)周期持久化,如果接收方和業(yè)務(wù)標(biāo)志組合已經(jīng)存在,則進行覆蓋
/// </summary>
/// <param name="receiver">接收方</param>
/// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param>
/// <param name="period">周期時間范圍</param>
/// <returns></returns>
Task<bool> SetPeriod(string receiver, string bizFlag, TimeSpan? period);
/// <summary>
/// 校驗碼周期內(nèi)發(fā)送次數(shù)+1,如果周期已到,則不進行任何操作
/// </summary>
/// <param name="receiver">接收方</param>
/// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param>
/// <returns></returns>
Task IncreaseSendTimes(string receiver, string bizFlag);
/// <summary>
/// 獲取校驗碼及已嘗試錯誤次數(shù),如果校驗碼不存在或已過期,則返回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>
/// 獲取校驗碼周期內(nèi)已發(fā)送次數(shù),如果周期已到或未發(fā)送過任何驗證碼,則返回0
/// </summary>
/// <param name="receiver"></param>
/// <param name="bizFlag"></param>
/// <returns></returns>
Task<int> GetAreadySendTimes(string receiver, string bizFlag);
}
ICodeSender,請注意IsSupport方法約定。
/// <summary>
/// 校驗碼實際發(fā)送接口
/// </summary>
public interface ICodeSender
{
/// <summary>
/// 發(fā)送校驗碼內(nèi)容模板
/// </summary>
IContentFormatter Formatter { get; }
/// <summary>
/// 判斷接收者是否符合發(fā)送條件,例如當(dāng)前發(fā)送者只支持郵箱,而接收方為手機號,則返回結(jié)果應(yīng)當(dāng)為false
/// </summary>
/// <param name="receiver">接收方</param>
/// <returns></returns>
bool IsSupport(string receiver);
/// <summary>
/// 發(fā)送校驗碼信息
/// </summary>
/// <param name="receiver">接收方</param>
/// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param>
/// <param name="code">校驗碼</param>
/// <param name="effectiveTime">校驗碼有效時間范圍</param>
/// <returns></returns>
Task<bool> Send(string receiver, string bizFlag, string code, TimeSpan effectiveTime);
}
IContentFormatter
/// <summary>
/// 發(fā)送校驗碼內(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">校驗碼</param>
/// <param name="effectiveTime">校驗碼有效時間范圍</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ù)對應(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ù)對應(yīng)的內(nèi)容模板,如果沒有,則返回null
/// </summary>
/// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param>
/// <returns></returns>
IContentFormatter RemoveFormatter(string bizFlag);
}
ICodeHelper
/// <summary>
/// 業(yè)務(wù)校驗碼輔助接口
/// </summary>
public interface ICodeHelper
{
/// <summary>
/// 校驗碼實際發(fā)送者
/// </summary>
ICodeSender Sender { get; }
/// <summary>
/// 校驗碼信息存儲者
/// </summary>
ICodeStorage Storage { get; }
/// <summary>
/// 發(fā)送校驗碼
/// </summary>
/// <param name="receiver">接收方</param>
/// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param>
/// <param name="code">校驗碼</param>
/// <param name="effectiveTime">校驗碼有效時間范圍</param>
/// <param name="maxSendLimit">周期內(nèi)最大允許發(fā)送配置,為null則表示無限制</param>
/// <returns></returns>
Task<SendResult> SendCode(string receiver, string bizFlag, string code, TimeSpan effectiveTime, PeriodLimit maxSendLimit);
/// <summary>
/// 驗證校驗碼是否正確
/// </summary>
/// <param name="receiver">接收方</param>
/// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param>
/// <param name="code">校驗碼</param>
/// <param name="maxErrorLimit">最大允許錯誤次數(shù)</param>
/// <returns></returns>
Task<VerificationResult> VerifyCode(string receiver, string bizFlag, string code, int maxErrorLimit);
}
下面則是接口約定中的一些定義的類和枚舉。
/// <summary>
/// 校驗碼發(fā)送周期設(shè)置
/// </summary>
public class PeriodLimit
{
/// <summary>
/// 周期內(nèi)允許的最大次數(shù)
/// </summary>
public int MaxLimit { get; set; }
/// <summary>
/// 周期時間,如果不設(shè)置,則表示無周期,此時<see cref="MaxLimit"/>代表總共只允許發(fā)送多少次
/// </summary>
public TimeSpan? Period { get; set; }
}
/// <summary>
/// 校驗碼發(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>
/// 無法發(fā)送,<see cref="ICodeSender.IsSupport(string)"/>結(jié)果為false
/// </summary>
[Description("無法發(fā)送")]
NotSupprot = 13,
}
/// <summary>
/// 校驗碼校驗結(jié)果
/// </summary>
public enum VerificationResult
{
/// <summary>
/// 校驗成功
/// </summary>
[Description("成功")]
Success = 0,
/// <summary>
/// 校驗碼已過期
/// </summary>
[Description("校驗碼已過期")]
Expired = 31,
/// <summary>
/// 校驗碼不一致,校驗失敗
/// </summary>
[Description("校驗失敗")]
VerificationFailed = 32,
/// <summary>
/// 已經(jīng)達到了最大錯誤嘗試次數(shù),需重新發(fā)送新的校驗碼
/// </summary>
[Description("超出最大錯誤次數(shù)")]
MaxErrorLimit = 33,
}
再下來就是具體的接口實現(xiàn)了,當(dāng)然這些實現(xiàn)也是通用實現(xiàn)
ContentFormatter
/// <summary>
/// 通用的內(nèi)容模板
/// </summary>
public class ContentFormatter : IContentFormatter
{
private Func<string, string, string, TimeSpan, string> _func;
/// <summary>
/// 通用實現(xiàn),這樣就無需每種業(yè)務(wù)類型都要實現(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">校驗碼</param>
/// <param name="effectiveTime">校驗碼有效時間范圍</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)容模板實現(xiàn)
/// </summary>
public class ComplexContentFormatter : IComplexContentFormatter
{
private ConcurrentDictionary<string, IContentFormatter> _dic = new ConcurrentDictionary<string, IContentFormatter>();
/// <summary>
/// 設(shè)置指定業(yè)務(wù)對應(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ù)對應(yīng)的內(nè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">校驗碼</param>
/// <param name="effectiveTime">校驗碼有效時間范圍</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,注意該類除了實現(xiàn)ICodeHelper外,還提供了一個用于生成隨機驗證碼的靜態(tài)方法GetRandomNumber。
/// <summary>
/// 業(yè)務(wù)校驗碼輔助接口實現(xiàn)
/// </summary>
public class CodeHelper : ICodeHelper
{
/// <summary>
/// 基于接口實現(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>
/// 校驗碼實際發(fā)送者
/// </summary>
public ICodeSender Sender { get; }
/// <summary>
/// 校驗碼信息存儲者
/// </summary>
public ICodeStorage Storage { get; }
/// <summary>
/// 發(fā)送校驗碼
/// </summary>
/// <param name="receiver">接收方</param>
/// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param>
/// <param name="code">校驗碼</param>
/// <param name="effectiveTime">校驗碼有效時間范圍</param>
/// <param name="maxSendLimit">周期內(nèi)最大允許發(fā)送配置,為null則表示無限制</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>
/// 驗證校驗碼是否正確
/// </summary>
/// <param name="receiver">接收方</param>
/// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param>
/// <param name="code">校驗碼</param>
/// <param name="maxErrorLimit">最大允許錯誤次數(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ù)字組成的校驗碼
/// </summary>
/// <param name="maxLength">校驗碼長度</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)通用實現(xiàn),還有一些半通用實現(xiàn),比如本文標(biāo)題中的Redis,所謂半通用,就是指你可以直接拿來用,但有可能不符合你的技術(shù)場景,此時你需要自己重寫一份。
CodeStorageWithRedisCache,注意該類庫采用了StackExchange.Redis.Extensions.Core,你可以在nuget上下載該類庫,如果你對默認的Redis鍵值生成方式不滿意,你也可以通過重寫GetKey方法來指定新的鍵值生成方式。當(dāng)然,因為實際存儲在Redis中的數(shù)據(jù)都只是一些簡單數(shù)據(jù),并不需要額外的序列化過程,實際你也可以直接使用StackExchange.Redis。
/// <summary>
/// 校驗碼信息存儲到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>
/// 緩存寫入Redis哪個庫
/// </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>
/// 獲取校驗碼周期內(nèi)已發(fā)送次數(shù),如果周期已到或未發(fā)送過任何驗證碼,則返回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>
/// 獲取校驗碼及已嘗試錯誤次數(shù),如果校驗碼不存在或已過期,則返回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>
/// 校驗碼錯誤次數(shù)+1,如果校驗碼已過期,則不進行任何操作
/// </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>
/// 校驗碼周期內(nèi)發(fā)送次數(shù)+1,如果周期已到,則不進行任何操作
/// </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è)務(wù)標(biāo)志組合已經(jīng)存在,則進行覆蓋
/// </summary>
/// <param name="receiver">接收方</param>
/// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param>
/// <param name="code">校驗碼</param>
/// <param name="effectiveTime">校驗碼有效時間范圍</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>
/// 校驗碼發(fā)送次數(shù)周期持久化,如果接收方和業(yè)務(wù)標(biāo)志組合已經(jīng)存在,則進行覆蓋
/// </summary>
/// <param name="receiver">接收方</param>
/// <param name="bizFlag">業(yè)務(wù)標(biāo)志</param>
/// <param name="period">周期時間范圍</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);
}
}
最后,就是不可能通用的實現(xiàn)了,對于ICodeSender而言,先不說發(fā)送方式不同,就算相同,比如都是手機,那也還有不同的短信供應(yīng)商,所以此處必須要使用者按自己的實際業(yè)務(wù)來實現(xiàn),為了方便舉例,這里我寫了一個在控制臺輸出驗證碼內(nèi)容的實現(xiàn)。
ConsoleSender,注意IsSupport在此處輸出true,代表支持任意receiver
/// <summary>
/// 在控制臺輸出校驗碼
/// </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ù)實際需要采用其它序列化方式,比如StackExchange.Redis.Extensions.Protobuf等,你同樣可以在nuget上下載到這些類庫。
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}您好,您的忘記密碼驗證碼為{c},有效期為{(int)e.TotalSeconds}秒.");
var formatter = new ComplexContentFormatter();
formatter.SetFormatter(bizFlag, simpleFormatter);
var sender = new ConsoleSender(formatter); //如果就一個業(yè)務(wù)場景,也可以直接用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("請輸入校驗碼:");
var vCode = Console.ReadLine();
var vResult = helper.VerifyCode(receiver, bizFlag, vCode, 3).Result;
Console.WriteLine("校驗碼 {0} 校驗結(jié)果:{1}", vCode, vResult);
if (vResult != VerificationResult.VerificationFailed)
{
break;
}
}
}
redisManager.Dispose();
}
最后則是不同測試場景的一些截圖
驗證碼校驗失敗達到允許次數(shù)上限

校驗碼已過期

校驗碼驗證成功

校驗碼周期內(nèi)允許的發(fā)送次數(shù)已達到上限

最后,上述完整的代碼可見github。
到此這篇關(guān)于基于Redis驗證碼發(fā)送及校驗方案實現(xiàn) 的文章就介紹到這了,更多相關(guān)Redis驗證碼發(fā)送及校驗內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
redis事務(wù)_動力節(jié)點Java學(xué)院整理
這篇文章主要介紹了redis事務(wù),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-08-08
redis 實現(xiàn)登陸次數(shù)限制的思路詳解
這篇文章主要介紹了redis 實現(xiàn)登陸次數(shù)限制的思路詳解,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-08-08
詳解RedisTemplate下Redis分布式鎖引發(fā)的系列問題
這篇文章主要介紹了詳解RedisTemplate下Redis分布式鎖引發(fā)的系列問題,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03

