在我们的业务中,经常存在需要通过发送验证码、校验验证码来完成的一些业务逻辑,比如账号注册、找回密码、用户身份确认等。
在该类业务中,发送验证码的方式可以有各种各样,比如最常见的手机验证,最古老的邮箱验证,到现在相对少见的微信公众号、钉钉通知等;而验证码服务端存储的方式也可以各式各样,比如存储在关系型数据库中,当然也可以如本文标题所示,存储在Redis中。
既然已经预见到了各式各样的发送方式,也预见到了各式各样的存储方式,所以,虽然本文标题是基于Redis,但Redis其实只是其中的一种存储方式,如果需要,我们也应该可以和方便的切换到其它存储方式。
上代码前,我们先看下设计中的接口关系
ICodeHelper是最终提供发送验证码和校验验证码的最终接口,其关联了ICodeSender和ICodeStorage,ICodeSender即为验证码发送方式的约定接口,ICodeStorage则为验证码服务端持久化方式的约定接口。我们可以看到ICodeSender同样关联了IContentFormatter,因为作为发送方ICodeSender其实是不知道如何将要发送的内容组织成一段完整的文本内容的,这时候就需要IContentFormatter来组织文本内容,至于继承自IContentFormatter的IComplexContentFormatter,则只是IContentFormatter一个容器封装,毕竟对于不同的业务类型,我们需要组织成不同的文本内容,通过IComplexContentFormatter,我们可以将不同业务类型文本内容的组织过程,分散到不同的IContentFormatter中。
下面我们来看下上述接口的规范约定,考虑到代码的简便性,此处我们简单的将receiver接收方定义为了string,而不是泛型<T>;业务标志bizFlag为了方便接入时无需调整代码,所以此处也没有将该值定义为枚举,而是同样定义成了通用性最强的string。
ICodeStorage
/// <summary> | |
/// 校验码信息存储接口 | |
/// </summary> | |
public interface ICodeStorage | |
{ | |
/// <summary> | |
/// 将校验码进行持久化,如果接收方和业务标志组合已经存在,则进行覆盖 | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <param name="bizFlag">业务标志</param> | |
/// <param name="code">校验码</param> | |
/// <param name="effectiveTime">校验码有效时间范围</param> | |
/// <returns></returns> | |
Task<bool> SetCode(string receiver, string bizFlag, string code, TimeSpan effectiveTime); | |
/// <summary> | |
/// 校验码错误次数+,如果校验码已过期,则不进行任何操作 | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <param name="bizFlag">业务标志</param> | |
/// <returns></returns> | |
Task IncreaseCodeErrors(string receiver, string bizFlag); | |
/// <summary> | |
/// 校验码发送次数周期持久化,如果接收方和业务标志组合已经存在,则进行覆盖 | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <param name="bizFlag">业务标志</param> | |
/// <param name="period">周期时间范围</param> | |
/// <returns></returns> | |
Task<bool> SetPeriod(string receiver, string bizFlag, TimeSpan? period); | |
/// <summary> | |
/// 校验码周期内发送次数+,如果周期已到,则不进行任何操作 | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <param name="bizFlag">业务标志</param> | |
/// <returns></returns> | |
Task IncreaseSendTimes(string receiver, string bizFlag); | |
/// <summary> | |
/// 获取校验码及已尝试错误次数,如果校验码不存在或已过期,则返回null | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <param name="bizFlag">业务标志</param> | |
/// <returns></returns> | |
Task<Tuple<string, int>> GetEffectiveCode(string receiver, string bizFlag); | |
/// <summary> | |
/// 获取校验码周期内已发送次数,如果周期已到或未发送过任何验证码,则返回 | |
/// </summary> | |
/// <param name="receiver"></param> | |
/// <param name="bizFlag"></param> | |
/// <returns></returns> | |
Task<int> GetAreadySendTimes(string receiver, string bizFlag); | |
} |
ICodeSender,请注意IsSupport方法约定。
/// <summary> | |
/// 校验码实际发送接口 | |
/// </summary> | |
public interface ICodeSender | |
{ | |
/// <summary> | |
/// 发送校验码内容模板 | |
/// </summary> | |
IContentFormatter Formatter { get; } | |
/// <summary> | |
/// 判断接收者是否符合发送条件,例如当前发送者只支持邮箱,而接收方为手机号,则返回结果应当为false | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <returns></returns> | |
bool IsSupport(string receiver); | |
/// <summary> | |
/// 发送校验码信息 | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <param name="bizFlag">业务标志</param> | |
/// <param name="code">校验码</param> | |
/// <param name="effectiveTime">校验码有效时间范围</param> | |
/// <returns></returns> | |
Task<bool> Send(string receiver, string bizFlag, string code, TimeSpan effectiveTime); | |
} |
IContentFormatter
/// <summary> | |
/// 发送校验码内容模板接口 | |
/// </summary> | |
public interface IContentFormatter | |
{ | |
/// <summary> | |
/// 将指定参数组织成待发送的文本内容 | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <param name="bizFlag">业务标志</param> | |
/// <param name="code">校验码</param> | |
/// <param name="effectiveTime">校验码有效时间范围</param> | |
/// <returns></returns> | |
string GetContent(string receiver, string bizFlag, string code, TimeSpan effectiveTime); | |
} |
IComplexContentFormatter
/// <summary> | |
/// 基于业务标志的多内容模板 | |
/// </summary> | |
public interface IComplexContentFormatter : IContentFormatter | |
{ | |
/// <summary> | |
/// 设置指定业务对应的内容模板 | |
/// </summary> | |
/// <param name="bizFlag">业务标志</param> | |
/// <param name="formatter">内容模板</param> | |
void SetFormatter(string bizFlag, IContentFormatter formatter); | |
/// <summary> | |
/// 移除指定业务对应的内容模板,如果没有,则返回null | |
/// </summary> | |
/// <param name="bizFlag">业务标志</param> | |
/// <returns></returns> | |
IContentFormatter RemoveFormatter(string bizFlag); | |
} |
ICodeHelper
/// <summary> | |
/// 业务校验码辅助接口 | |
/// </summary> | |
public interface ICodeHelper | |
{ | |
/// <summary> | |
/// 校验码实际发送者 | |
/// </summary> | |
ICodeSender Sender { get; } | |
/// <summary> | |
/// 校验码信息存储者 | |
/// </summary> | |
ICodeStorage Storage { get; } | |
/// <summary> | |
/// 发送校验码 | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <param name="bizFlag">业务标志</param> | |
/// <param name="code">校验码</param> | |
/// <param name="effectiveTime">校验码有效时间范围</param> | |
/// <param name="maxSendLimit">周期内最大允许发送配置,为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">业务标志</param> | |
/// <param name="code">校验码</param> | |
/// <param name="maxErrorLimit">最大允许错误次数</param> | |
/// <returns></returns> | |
Task<VerificationResult> VerifyCode(string receiver, string bizFlag, string code, int maxErrorLimit); | |
} |
下面则是接口约定中的一些定义的类和枚举。
/// <summary> | |
/// 校验码发送周期设置 | |
/// </summary> | |
public class PeriodLimit | |
{ | |
/// <summary> | |
/// 周期内允许的最大次数 | |
/// </summary> | |
public int MaxLimit { get; set; } | |
/// <summary> | |
/// 周期时间,如果不设置,则表示无周期,此时<see cref="MaxLimit"/>代表总共只允许发送多少次 | |
/// </summary> | |
public TimeSpan? Period { get; set; } | |
} | |
/// <summary> | |
/// 校验码发送结果 | |
/// </summary> | |
public enum SendResult | |
{ | |
/// <summary> | |
/// 发送成功 | |
/// </summary> | |
[ | ]|
Success =, | |
/// <summary> | |
/// 超出最大发送次数 | |
/// </summary> | |
[ | ]|
MaxSendLimit =, | |
/// <summary> | |
/// 发送失败,指<see cref="ICodeSender"/>的发送结果为false | |
/// </summary> | |
[ | ]|
FailInSend =, | |
/// <summary> | |
/// 无法发送,<see cref="ICodeSender.IsSupport(string)"/>结果为false | |
/// </summary> | |
[ | ]|
NotSupprot =, | |
} | |
/// <summary> | |
/// 校验码校验结果 | |
/// </summary> | |
public enum VerificationResult | |
{ | |
/// <summary> | |
/// 校验成功 | |
/// </summary> | |
[ | ]|
Success =, | |
/// <summary> | |
/// 校验码已过期 | |
/// </summary> | |
[ | ]|
Expired =, | |
/// <summary> | |
/// 校验码不一致,校验失败 | |
/// </summary> | |
[ | ]|
VerificationFailed =, | |
/// <summary> | |
/// 已经达到了最大错误尝试次数,需重新发送新的校验码 | |
/// </summary> | |
[ | ]|
MaxErrorLimit =, | |
} |
再下来就是具体的接口实现了,当然这些实现也是通用实现
ContentFormatter
/// <summary> | |
/// 通用的内容模板 | |
/// </summary> | |
public class ContentFormatter : IContentFormatter | |
{ | |
private Func<string, string, string, TimeSpan, string> _func; | |
/// <summary> | |
/// 通用实现,这样就无需每种业务类型都要实现<see cref="IContentFormatter"/> | |
/// </summary> | |
/// <param name="func">传递的委托,参数顺序与<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> | |
/// 将指定参数组织成待发送的文本内容 | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <param name="bizFlag">业务标志</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> | |
/// 基于业务标志的多内容模板实现 | |
/// </summary> | |
public class ComplexContentFormatter : IComplexContentFormatter | |
{ | |
private ConcurrentDictionary<string, IContentFormatter> _dic = new ConcurrentDictionary<string, IContentFormatter>(); | |
/// <summary> | |
/// 设置指定业务对应的内容模板 | |
/// </summary> | |
/// <param name="bizFlag">业务标志</param> | |
/// <param name="formatter">内容模板</param> | |
public void SetFormatter(string bizFlag, IContentFormatter formatter) | |
{ | |
if (!string.IsNullOrWhiteSpace(bizFlag) && formatter != null) | |
{ | |
this._dic.AddOrUpdate(bizFlag, formatter, (k, v) => formatter); | |
} | |
} | |
/// <summary> | |
/// 移除指定业务对应的内容模板,如果没有,则返回null | |
/// </summary> | |
/// <param name="bizFlag">业务标志</param> | |
/// <returns></returns> | |
public IContentFormatter RemoveFormatter(string bizFlag) | |
{ | |
if (!string.IsNullOrWhiteSpace(bizFlag) | |
&& this._dic.TryRemove(bizFlag, out IContentFormatter formatter)) | |
{ | |
return formatter; | |
} | |
return null; | |
} | |
/// <summary> | |
/// 将指定参数组织成待发送的文本内容 | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <param name="bizFlag">业务标志</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,注意该类除了实现ICodeHelper外,还提供了一个用于生成随机验证码的静态方法GetRandomNumber。
/// <summary> | |
/// 业务校验码辅助接口实现 | |
/// </summary> | |
public class CodeHelper : ICodeHelper | |
{ | |
/// <summary> | |
/// 基于接口实现,可依赖注入 | |
/// </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> | |
/// 校验码实际发送者 | |
/// </summary> | |
public ICodeSender Sender { get; } | |
/// <summary> | |
/// 校验码信息存储者 | |
/// </summary> | |
public ICodeStorage Storage { get; } | |
/// <summary> | |
/// 发送校验码 | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <param name="bizFlag">业务标志</param> | |
/// <param name="code">校验码</param> | |
/// <param name="effectiveTime">校验码有效时间范围</param> | |
/// <param name="maxSendLimit">周期内最大允许发送配置,为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 =; | |
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 ==) | |
{ | |
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">业务标志</param> | |
/// <param name="code">校验码</param> | |
/// <param name="maxErrorLimit">最大允许错误次数</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.Item)) | |
{ | |
result = VerificationResult.MaxErrorLimit; | |
if (vCode.Item < maxErrorLimit) | |
{ | |
result = VerificationResult.Success; | |
if (!string.Equals(vCode.Item, code, StringComparison.OrdinalIgnoreCase)) | |
{ | |
result = VerificationResult.VerificationFailed; | |
await this.Storage.IncreaseCodeErrors(receiver, bizFlag).ConfigureAwait(false); | |
} | |
} | |
} | |
return result; | |
} | |
/// <summary> | |
/// 获取由数字组成的校验码 | |
/// </summary> | |
/// <param name="maxLength">校验码长度</param> | |
/// <returns></returns> | |
public static string GetRandomNumber(int maxLength =) | |
{ | |
if (maxLength <= || maxLength >= 10) | |
{ | |
throw new ArgumentOutOfRangeException($"{nameof(maxLength)} must between {} and {9}."); | |
} | |
var rd = Math.Abs(Guid.NewGuid().GetHashCode()); | |
var tmpX = (int)Math.Pow(, maxLength); | |
return (rd % tmpX).ToString().PadLeft(maxLength, ''); | |
} | |
} |
除了上述标准通用实现,还有一些半通用实现,比如本文标题中的Redis,所谓半通用,就是指你可以直接拿来用,但有可能不符合你的技术场景,此时你需要自己重写一份。
CodeStorageWithRedisCache,注意该类库采用了StackExchange.Redis.Extensions.Core,你可以在nuget上下载该类库,如果你对默认的Redis键值生成方式不满意,你也可以通过重写GetKey方法来指定新的键值生成方式。当然,因为实际存储在Redis中的数据都只是一些简单数据,并不需要额外的序列化过程,实际你也可以直接使用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; } =; | |
/// <summary> | |
/// 基于RedisCacheClient的构造函数 | |
/// </summary> | |
/// <param name="client"></param> | |
public CodeStorageWithRedisCache(IRedisCacheClient client) | |
{ | |
this._client = client; | |
} | |
/// <summary> | |
/// 获取校验码周期内已发送次数,如果周期已到或未发送过任何验证码,则返回 | |
/// </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); | |
} | |
/// <summary> | |
/// 获取校验码及已尝试错误次数,如果校验码不存在或已过期,则返回null | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <param name="bizFlag">业务标志</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); | |
} | |
return null; | |
} | |
/// <summary> | |
/// 校验码错误次数+,如果校验码已过期,则不进行任何操作 | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <param name="bizFlag">业务标志</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 +).ConfigureAwait(false); | |
} | |
} | |
/// <summary> | |
/// 校验码周期内发送次数+,如果周期已到,则不进行任何操作 | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <param name="bizFlag">业务标志</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 +).ConfigureAwait(false); | |
} | |
} | |
/// <summary> | |
/// 将校验码进行持久化,如果接收方和业务标志组合已经存在,则进行覆盖 | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <param name="bizFlag">业务标志</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,).ConfigureAwait(false) | |
&& await db.UpdateExpiryAsync(key, effectiveTime); | |
} | |
/// <summary> | |
/// 校验码发送次数周期持久化,如果接收方和业务标志组合已经存在,则进行覆盖 | |
/// </summary> | |
/// <param name="receiver">接收方</param> | |
/// <param name="bizFlag">业务标志</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,).ConfigureAwait(false); | |
if (period.HasValue) | |
{ | |
ret = ret && await db.UpdateExpiryAsync(key, period.Value); | |
} | |
} | |
/// <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("{}:{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); | |
} | |
} |
最后,就是不可能通用的实现了,对于ICodeSender而言,先不说发送方式不同,就算相同,比如都是手机,那也还有不同的短信供应商,所以此处必须要使用者按自己的实际业务来实现,为了方便举例,这里我写了一个在控制台输出验证码内容的实现。
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("发送内容:{}", content); | |
return Task.FromResult(true); | |
} | |
} |
最后则是如何使用的代码例子,注意此处Redis序列化方式采用了StackExchange.Redis.Extensions.Newtonsoft,你可以根据实际需要采用其它序列化方式,比如StackExchange.Redis.Extensions.Protobuf等,你同样可以在nuget上下载到这些类库。
static void CheckCodeHelperDemo() | |
{ | |
var redisConfig = new RedisConfiguration | |
{ | |
Hosts = new RedisHost[] { | |
new RedisHost{ | |
Host=".0.0.1", | |
Port= | |
} | |
} | |
}; | |
var bizFlag = "forgetPassword"; | |
var receiver = "Receiver"; | |
var effectiveTime = TimeSpan.FromMinutes(); | |
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); //如果就一个业务场景,也可以直接用simpleFormatter | |
//var tmp = storage.SetPeriod(receiver, bizFlag, TimeSpan.FromMinutes()).Result; | |
var helper = new CodeHelper(sender, storage); | |
var code = CodeHelper.GetRandomNumber(); | |
var sendResult = helper.SendCode(receiver, bizFlag, code, effectiveTime, new PeriodLimit | |
{ | |
MaxLimit =, | |
Period = TimeSpan.FromMinutes() | |
}).Result; | |
Console.WriteLine("发送结果:{}", sendResult); | |
if (sendResult == SendResult.Success) | |
{ | |
Console.WriteLine("*****************************"); | |
while (true) | |
{ | |
Console.WriteLine("请输入校验码:"); | |
var vCode = Console.ReadLine(); | |
var vResult = helper.VerifyCode(receiver, bizFlag, vCode,).Result; | |
Console.WriteLine("校验码 {} 校验结果:{1}", vCode, vResult); | |
if (vResult != VerificationResult.VerificationFailed) | |
{ | |
break; | |
} | |
} | |
} | |
redisManager.Dispose(); | |
} |
最后则是不同测试场景的一些截图
验证码校验失败达到允许次数上限
校验码已过期
校验码验证成功
校验码周期内允许的发送次数已达到上限
最后,上述完整的代码可见github。