返回顶部
首页 > 资讯 > 数据库 >基于Redis验证码发送及校验方案实现
  • 528
分享到

基于Redis验证码发送及校验方案实现

Redis验证码发送及校验Redis验证码发送 2023-01-04 16:01:12 528人浏览 薄情痞子
摘要

在我们的业务中,经常存在需要通过发送验证码、校验验证码来完成的一些业务逻辑,比如账号注册、找回密码、用户身份确认等。 在该类业务中,发送验证码的方式可以有各种各样,比如最常见的手机验证,最古老的邮箱验证,到现在相对少见的

在我们的业务中,经常存在需要通过发送验证码、校验验证码来完成的一些业务逻辑,比如账号注册、找回密码、用户身份确认等。

在该类业务中,发送验证码的方式可以有各种各样,比如最常见的手机验证,最古老的邮箱验证,到现在相对少见的微信公众号、钉钉通知等;而验证码服务端存储的方式也可以各式各样,比如存储在关系型数据库中,当然也可以如本文标题所示,存储在Redis中。

既然已经预见到了各式各样的发送方式,也预见到了各式各样的存储方式,所以,虽然本文标题是基于Redis,但Redis其实只是其中的一种存储方式,如果需要,我们也应该可以和方便的切换到其它存储方式。

上代码前,我们先看下设计中的接口关系

在这里插入图片描述

ICodeHelper是最终提供发送验证码和校验验证码的最终接口,其关联了ICodeSenderICodeStorageICodeSender即为验证码发送方式的约定接口,ICodeStorage则为验证码服务端持久化方式的约定接口。我们可以看到ICodeSender同样关联了IContentFORMatter,因为作为发送方ICodeSender其实是不知道如何将要发送的内容组织成一段完整的文本内容的,这时候就需要IContentFormatter来组织文本内容,至于继承自IContentFormatterIComplexContentFormatter,则只是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>
        /// 校验码错误次数+1,如果校验码已过期,则不进行任何操作
        /// </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>
        /// 校验码周期内发送次数+1,如果周期已到,则不进行任何操作
        /// </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>
        /// 获取校验码周期内已发送次数,如果周期已到或未发送过任何验证码,则返回0
        /// </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>
        [Description("成功")]
        Success = 0,
        /// <summary>
        /// 超出最大发送次数
        /// </summary>
        [Description("超出最大发送次数")]
        MaxSendLimit = 11,
        /// <summary>
        /// 发送失败,指<see cref="ICodeSender"/>的发送结果为false
        /// </summary>
        [Description("发送失败")]
        FailInSend = 12,
        /// <summary>
        /// 无法发送,<see cref="ICodeSender.IsSupport(string)"/>结果为false
        /// </summary>
        [Description("无法发送")]
        NotSupprot = 13,
    }
    /// <summary>
    /// 校验码校验结果
    /// </summary>
    public enum VerificationResult
    {
        /// <summary>
        /// 校验成功
        /// </summary>
        [Description("成功")]
        Success = 0,
        /// <summary>
        /// 校验码已过期
        /// </summary>
        [Description("校验码已过期")]
        Expired = 31,
        /// <summary>
        /// 校验码不一致,校验失败
        /// </summary>
        [Description("校验失败")]
        VerificationFailed = 32,
        /// <summary>
        /// 已经达到了最大错误尝试次数,需重新发送新的校验码
        /// </summary>
        [Description("超出最大错误次数")]
        MaxErrorLimit = 33,
    }

再下来就是具体的接口实现了,当然这些实现也是通用实现

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 = 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">业务标志</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.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>
        /// 获取由数字组成的校验码
        /// </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');
        }
    }

除了上述标准通用实现,还有一些半通用实现,比如本文标题中的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; } = 8;
        /// <summary>
        /// 基于RedisCacheClient的构造函数
        /// </summary>
        /// <param name="client"></param>
        public CodeStorageWithRedisCache(IRedisCacheClient client)
        {
            this._client = client;
        }
        /// <summary>
        /// 获取校验码周期内已发送次数,如果周期已到或未发送过任何验证码,则返回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>
        /// 获取校验码及已尝试错误次数,如果校验码不存在或已过期,则返回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);
#if DEBUG
                Console.WriteLine("Method:{0} Result:  Code {1} Errors {2} ", nameof(GetEffectiveCode), code, errors);
#endif
                return Tuple.Create(code, errors);
            }
            return null;
        }
        /// <summary>
        /// 校验码错误次数+1,如果校验码已过期,则不进行任何操作
        /// </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 + 1).ConfigureAwait(false);
            }
        }
        /// <summary>
        /// 校验码周期内发送次数+1,如果周期已到,则不进行任何操作
        /// </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 + 1).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, 0).ConfigureAwait(false)
            && await db.UpdateExpiryAsync(key, effectiveTime);
#if DEBUG
            Console.WriteLine("Method:{0} Result:{1}", nameof(SetCode), ret);
#endif
            return ret;
        }
        /// <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, 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);
        }
    }

最后,就是不可能通用的实现了,对于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("发送内容:{0}", 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="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); //如果就一个业务场景,也可以直接用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("发送结果:{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} 校验结果:{1}", vCode, vResult);
            if (vResult != VerificationResult.VerificationFailed)
            {
                break;
            }
        }
    }
    redisManager.Dispose();
}

最后则是不同测试场景的一些截图

验证码校验失败达到允许次数上限

错误上限

校验码已过期

过期

校验码验证成功

校验通过

校验码周期内允许的发送次数已达到上限

发送上限

最后,上述完整的代码可见GitHub

到此这篇关于基于Redis验证码发送及校验方案实现 的文章就介绍到这了,更多相关Redis验证码发送及校验内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

您可能感兴趣的文档:

--结束END--

本文标题: 基于Redis验证码发送及校验方案实现

本文链接: https://lsjlt.com/news/176720.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 基于Redis验证码发送及校验方案实现
    在我们的业务中,经常存在需要通过发送验证码、校验验证码来完成的一些业务逻辑,比如账号注册、找回密码、用户身份确认等。 在该类业务中,发送验证码的方式可以有各种各样,比如最常见的手机验证,最古老的邮箱验证,到现在相对少见的...
    99+
    2023-01-04
    Redis验证码发送及校验 Redis验证码发送
  • javascript实现发送短信验证码案例
    本文实例为大家分享了javascript实现发送短信验证码的具体代码,供大家参考,具体内容如下 效果如下: 代码思路: 1.按钮点击之后,会禁用disabled 为true2.同时...
    99+
    2024-04-02
  • JavaScript实现随机生成验证码及校验
    本文实例为大家分享了JavaScript实现随机生成验证码及校验的具体代码,供大家参考,具体内容如下 输入验证码(区分大小写)点击确认,进行校验。出错就弹框提示 点击 看不清 重新随...
    99+
    2024-04-02
  • java实现发送邮箱验证码
    本文实例为大家分享了java实现发送邮箱验证码的具体代码,供大家参考,具体内容如下 添加依赖 <!-- 邮箱验证码 https://mvnrepository.com/ar...
    99+
    2024-04-02
  • android怎么实现发送验证码
    在Android中,可以通过使用短信管理器(SmsManager)来发送验证码。首先,你需要在AndroidManifest.xml...
    99+
    2023-09-04
    android
  • Jedis操作Redis实现模拟验证码发送功能
    目录jedis的创建1.先启动redis 如果报2.创建一个maven工程 3.创建一个classjedis实现模拟验证码相关数据类型测试KeyStringListsethashzs...
    99+
    2024-04-02
  • Redis怎么实现验证码发送并限制每日发送次数
    这篇文章主要讲解了“Redis怎么实现验证码发送并限制每日发送次数”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Redis怎么实现验证码发送并限制每日发送次数”吧!1、功能输入手机号,点击发...
    99+
    2023-06-30
  • 基于WPF实现验证码控件
    代码如下 一、创建CheckCode.xaml代码如下 <ResourceDictionary xmlns="http://schemas.microsoft.com/winf...
    99+
    2022-11-13
    WPF验证码控件 WPF验证码 WPF 控件
  • JSP页面实现验证码校验功能
    目录验证码校验分析生成验证码测试验证码校验验证码测试验证码校验添加验证码刷新在网页页面的使用中为防止“非人类”的大量操作和防止一些的信息冗余,增加验证码校验是...
    99+
    2022-11-13
    JSP验证码 JSP验证码校验 JSP页面验证码
  • Java实现发送短信验证码+redis限制发送的次数功能
    java实现短信验证码发送,由于我们使用第三方平台进行验证码的发送,所以首先,我们要在一个平台进行注册。这样的平台有很多,有的平台在新建账号的时候会附带赠几条免费短信。这里我仅做测试...
    99+
    2024-04-02
  • springboot整合redis实现发送邮箱并验证
    目录1.起步2.工具类邮箱工具类redis乱码解决3.controller搭建4.前端搭建结果总结1.起步 pom文件 <!--集成redis--> ...
    99+
    2024-04-02
  • SpringBoot实现滑块验证码验证登陆校验功能详解
    目录前言一、实现效果二、实现思路三、实现步骤1. 后端 java 代码1.1 新建一个拼图验证码类1.2 新建一个拼图验证码工具类1.3 新建一个 service 类1.4 新建一个...
    99+
    2024-04-02
  • Redis实现验证码发送并限制每日发送次数的示例代码
    目录1、功能2、分析3、实现1、功能 输入手机号,点击发送后随机生成六位数字码,2分钟有效输入验证码,点击验证,返回成功或失败每个手机号每天只能输3次 2、分析 每个手机每天只能输3...
    99+
    2024-04-02
  • Redis模仿发送手机验证码功能
    流程图 一:添加jedis依赖包 二:测试连接Redis服务是否成功 // 创建Jedis对象用于连接Redis服务(在服务器上通过redis-server需要指定配置文件:...
    99+
    2024-04-02
  • Redis如何模仿手机验证码发送
    这篇文章将为大家详细讲解有关Redis如何模仿手机验证码发送,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。具体如下:流程图一:添加jedis依赖包二:测试连接Redis服务是否成功// 创建Je...
    99+
    2023-06-25
  • nodejs实现发送邮箱验证码功能
    本文实例为大家分享了nodejs实现发送邮箱验证码的具体代码,供大家参考,具体内容如下 今天做了个小demo,是用nodejs实现注册时(当然在别的地方也是可以用的)的邮箱验证功能,...
    99+
    2024-04-02
  • javascript如何实现发送短信验证码
    这篇文章主要介绍“javascript如何实现发送短信验证码”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“javascript如何实现发送短信验证码”文章能帮助大家解决问题。效果如下:代码思路:按钮...
    99+
    2023-07-02
  • 基于ASP.NET实现验证码生成详解
    作业:验证码 要求: (1)验证码应该是图片格式,不能是文字格式,即无法用鼠标选中。 (2)验证码上应该有噪点和干扰线条。 (3)验证码应该回避相似字符,如“0&rdqu...
    99+
    2024-04-02
  • 基于JavaScript实现密码框验证信息
    本文实例为大家分享了JavaScript实现密码框验证信息的具体代码,供大家参考,具体内容如下 <!DOCTYPE html> <html lang="en...
    99+
    2024-04-02
  • JavaScript实现验证码案例
    本文实例为大家分享了JavaScript实现验证码效果的具体代码,供大家参考,具体内容如下 今天的案例,效果如下: 这个案例的实现其实没有很多难点,让我们一起来看看吧~ html和...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作