diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 0000000..df369e6 --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,53 @@ +name: sonarcloud +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] +jobs: + build: + name: Build + runs-on: windows-2019 + steps: + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 1.11 + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.x + - name: Restore dependencies + run: dotnet restore .\src\CheckCodeHelper.sln + - name: Cache SonarCloud packages + uses: actions/cache@v1 + with: + path: ~\sonar\cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache SonarCloud scanner + id: cache-sonar-scanner + uses: actions/cache@v1 + with: + path: .\.sonar\scanner + key: ${{ runner.os }}-sonar-scanner + restore-keys: ${{ runner.os }}-sonar-scanner + - name: Install SonarCloud scanner + if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' + shell: powershell + run: | + New-Item -Path .\.sonar\scanner -ItemType Directory + dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner + - name: Build and analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + shell: powershell + run: | + .\.sonar\scanner\dotnet-sonarscanner begin /k:"fdstar_CheckCodeHelper" /o:"fdstar" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" + dotnet build .\src\CheckCodeHelper.sln --no-restore + .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}" diff --git a/README.md b/README.md index fed46ee..6eeb0ce 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,85 @@ 支持周期内限制最大发送次数,支持单次发送后最大校验错误次数 存储方案默认提供了`Redis`和`MemoryCache`实现,你也可以自己实现`ICodeStorage`来支持其它存储方案。 -## .NET版本支持 -支持以下版本:`.NET461`、`.NET Standard 2.0` + +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://mit-license.org/) +| Lib | Version | Summary | .Net | +|---|---|---|---| +|CheckCodeHelper|[![NuGet version (CheckCodeHelper)](https://img.shields.io/nuget/v/CheckCodeHelper.svg?style=flat-square)](https://www.nuget.org/packages/CheckCodeHelper/)|主类库|`.NET45`、`.NET Standard 2.0`| +|CheckCodeHelper.Sender.EMail|[![NuGet version (CheckCodeHelper.Sender.EMail)](https://img.shields.io/nuget/v/CheckCodeHelper.Sender.EMail.svg?style=flat-square)](https://www.nuget.org/packages/CheckCodeHelper.Sender.EMail/)|基于EMail的`ICodeSender`实现|`.NET45`、`.NET Standard 2.0`| +|CheckCodeHelper.Sender.Sms|[![NuGet version (CheckCodeHelper.Sender.Sms)](https://img.shields.io/nuget/v/CheckCodeHelper.Sender.Sms.svg?style=flat-square)](https://www.nuget.org/packages/CheckCodeHelper.Sender.Sms/)|基于非模板短信的`ICodeSender`实现,默认提供`emay`短信实现|`.NET452`、`.NET Standard 2.0`| +|CheckCodeHelper.Sender.AlibabaSms|[![NuGet version (CheckCodeHelper.Sender.AlibabaSms)](https://img.shields.io/nuget/v/CheckCodeHelper.Sender.AlibabaSms.svg?style=flat-square)](https://www.nuget.org/packages/CheckCodeHelper.Sender.AlibabaSms/)|基于阿里模板短信的`ICodeSender`实现|`.NET45`、`.NET Standard 2.0`| +|CheckCodeHelper.Storage.Redis|[![NuGet version (CheckCodeHelper.Storage.Redis)](https://img.shields.io/nuget/v/CheckCodeHelper.Storage.Redis.svg?style=flat-square)](https://www.nuget.org/packages/CheckCodeHelper.Storage.Redis/)|基于Redis的`ICodeStorage`实现|`.NET45`、`.NET Standard 2.0`| +|CheckCodeHelper.Storage.Memory|[![NuGet version (CheckCodeHelper.Storage.Memory)](https://img.shields.io/nuget/v/CheckCodeHelper.Storage.Memory.svg?style=flat-square)](https://www.nuget.org/packages/CheckCodeHelper.Storage.Memory/)|基于MemoryCache的`ICodeStorage`实现|`.NET45`、`.NET Standard 2.0`| + ## 如何使用 -你可以在此处查看使用例子 https://github.com/fdstar/CheckCodeHelper/blob/master/src/CheckCodeHelper.Samples/Program.cs +你可以在此处查看使用例子 https://github.com/fdstar/CheckCodeHelper/blob/master/src/CheckCodeHelper.Samples +- `Program.PrevDemo()`为`new`显示声明方式实现的Demo +- `ComplexHelperTest`为`.Net Core`依赖注入方式实现的Demo +- `ConsoleSender`为自定义`ICodeSender`及`ICodeSenderSupportAsync`的例子 + +## Release History +**2021年11月30日 Release** +|CheckCodeHelper v1.0.6| +|:--| +|鉴于`IComplexContentFormatter`在`ComplexHelper`中作为构造参数传入,其本身可能已包含如何构造要发送的文本内容,故调整`ComplexHelper.ComplexSetting.ContentFormatters`为空未设置时,不再抛出异常| + +**2021年11月22日 Release** +|CheckCodeHelper v1.0.5| +|:--| +|`ComplexHelper`公开读取配置的方法以支持某些特殊场景| +|`ComplexContentFormatter`调整唯一标志的组成方式,使其与`ComplexHelper`保持一致以减少字符串碎片| +|**CheckCodeHelper.Storage.Redis v1.0.2**| +|`ICodeStorage.RemovePeriodAsync`同时移除周期限制及错误次数记录| +|**CheckCodeHelper.Storage.Memory v1.0.1**| +|`ICodeStorage.RemovePeriodAsync`同时移除周期限制及错误次数记录| + +**2021年11月03日 Release** +|CheckCodeHelper v1.0.4| +|:--| +|增加`ICodeSenderSupportAsync`以支持`ICodeSender.IsSupport`异步场景,如果`ICodeSender`同时实现了`ICodeSenderSupportAsync`,则`CodeHelper`会通过`ICodeSenderSupportAsync.IsSupportAsync`判断`SendResult.NotSupport`| +|修正`ComplexHelper`获取`PeriodLimit`时,如果未配置`ComplexSetting.PeriodMaxLimits`会导致`ComplexSetting.PeriodLimitIntervalSeconds`无效的问题| + +**2021年10月20日 Release** +|
CheckCodeHelper.Storage.Redis v1.0.1
| +|:--| +|调整为通过`Lua`脚本合并执行`Redis`的多条更新指令| + +**2021年10月18日 Release** +|CheckCodeHelper v1.0.3| +|:--| +|增加`EffectiveTimeDisplayedInContent`以调整验证码有效期在发送内容中的展示方式,`ComplexHelper`已支持该枚举,`ComplexSetting.EffectiveTimeDisplayed`默认设置为`Seconds`,即在所有的发送内容中以秒对应的数字进行展示,设置为`Auto`时如果有效时间为整360秒或以上且可被360整除,则展示为对应的小时数,有效时间为整60秒或以上且可被60整除,则展示为对应的分钟数
+`SendResult.NotSupprot`修正拼写错误为`SendResult.NotSupport`;`VerificationResult.VerificationFailed`简化为`Failed`。注意对于`CheckCodeHelper`这是**破坏性调整**,会造成升级后相关判断产生错误| +|**CheckCodeHelper.Sender.EMail v1.0.3**| +|移除不必要的`Subject Func`,同步调整`TextFormat`及`Subject`到`EMailMimeMessageSetting`,注意对于`EMail`部分这是一个**破坏性调整** | +|**CheckCodeHelper.Sender.AlibabaSms v1.0.0**| +|基于阿里模板短信的`ICodeSender`实现| + +**2021年07月20日 Release** +|CheckCodeHelper v1.0.2| +|:--| +|增加`PeriodLimit.Interval`以支持限制发送检验码的时间间隔| +|**CheckCodeHelper.Sender.Sms v1.0.1**| +|升级依赖的`RestSharp`版本至`106.12.0`以解决潜在的安全问题| + +**2021年07月17日 Release** +|CheckCodeHelper v1.0.1| +|:--| +|增加`ComplexHelper`以统一在单个应用中校验码的发送与验证入口,支持按需调用指定的`ICodeSender`| +|**CheckCodeHelper.Sender.EMail v1.0.2**| +|增加`Subject Func`以支持`ComplexHelper`| + +**2021年07月15日 Release** +|CheckCodeHelper v1.0.0| +|:--| +|主体功能| +|**CheckCodeHelper.Sender.Sms v1.0.0**| +|基于非模板短信的`ICodeSender`实现| +|**CheckCodeHelper.Sender.EMail v1.0.1**| +|基于EMail的`ICodeSender`实现| +|**CheckCodeHelper.Storage.Redis v1.0.0**| +|基于`Redis`的`ICodeStorage`实现| +|**CheckCodeHelper.Storage.Memory v1.0.0**| +|基于`MemoryCache`的`ICodeStorage`实现| + diff --git a/src/CheckCodeHelper.Samples/CheckCodeHelper.Samples.csproj b/src/CheckCodeHelper.Samples/CheckCodeHelper.Samples.csproj index 77807e8..660475f 100644 --- a/src/CheckCodeHelper.Samples/CheckCodeHelper.Samples.csproj +++ b/src/CheckCodeHelper.Samples/CheckCodeHelper.Samples.csproj @@ -1,19 +1,35 @@ - + Exe - netcoreapp2.1 + netcoreapp3.1 + + + + + + + Always + + + + + + + + + - + diff --git a/src/CheckCodeHelper.Samples/ComplexHelperTest.cs b/src/CheckCodeHelper.Samples/ComplexHelperTest.cs new file mode 100644 index 0000000..a65d3af --- /dev/null +++ b/src/CheckCodeHelper.Samples/ComplexHelperTest.cs @@ -0,0 +1,165 @@ +using CheckCodeHelper.Sender.AlibabaSms; +using CheckCodeHelper.Sender.EMail; +using CheckCodeHelper.Sender.Sms; +using CheckCodeHelper.Storage.Memory; +using CheckCodeHelper.Storage.Redis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace CheckCodeHelper.Samples +{ + public static class ComplexHelperTest + { + static IServiceCollection services; + static IConfiguration configuration; + static ComplexHelperTest() + { + var basePath = Path.GetDirectoryName(typeof(ComplexHelperTest).Assembly.Location); + services = new ServiceCollection(); + configuration = new ConfigurationBuilder() + .AddJsonFile(Path.Combine(basePath, "appsettings.json"), false) + .Build(); + + Init(); + } + + public static void Init() + { + services.AddOptions(); + + //注册ICodeSender + services.AddSingletonForNoneSender(); + services.AddSingletonForSmsSenderWithEmay(configuration.GetSection("EmaySetting")); + services.AddSingletonForEMailSender(configuration.GetSection("EMailSetting"), configuration.GetSection("EMailMimeMessageSetting")); + services.AddSingletonForAlibabaSms(configuration.GetSection("AlibabaConfig"), configuration.GetSection("AlibabaSmsParameterSetting")); + + //增加自定义ICodeSender例子,且该Sender未设置周期限制 + services.AddSingleton(); + + //注册ICodeStorage + services.AddSingletonForRedisStorage(configuration.GetValue("Redis:Configuration")); + //services.AddSingletonForMemoryCacheStorage(); + + //注册ComplexHelper + services.AddSingletonForComplexHelper(configuration.GetSection("ComplexSetting")); + } + + public static void Start() + { + var serviceProvider = services.BuildServiceProvider(); + + //获取Helper,如果默认的InitComplexContentFormatter不符合业务需求,可继承后重写 + //ComplexHelper依赖ICodeSender.Key来获取实际的验证码发送者,如果找不到,会产生异常 + var complexHelper = serviceProvider.GetRequiredService(); + + var bizFlag = configuration.GetValue("BizFlag"); + if (bizFlag == "LoginValidError") + { + LoginError(complexHelper); + return; + } + else + { + CodeValidator(complexHelper); + } + } + + private static void CodeValidator(ComplexHelper complexHelper) + { + Console.WriteLine("****** 您当前正在进行验证码校验Demo ******"); + var bizFlag = configuration.GetValue("BizFlag"); + var senderKey = configuration.GetValue("CurrentSenderKey"); + var receiver = configuration.GetValue("Receiver"); + var code = CodeHelper.GetRandomNumber(); //生成随机的验证码 + + Action getTimeAction = () => + { + var time = complexHelper.CodeStorage.GetLastSetCodeTimeAsync(receiver, bizFlag).Result; + if (time.HasValue) + { + Console.WriteLine("上次发送时间:{0:yy-MM-dd HH:mm:ss.fff}", time.Value.LocalDateTime); + } + else + { + Console.WriteLine("未能获取到最后一次发送时间"); + } + var ts = complexHelper.GetSendCDAsync(senderKey, receiver, bizFlag).Result; + Console.WriteLine("校验码发送剩余CD时间:{0}秒", ts.TotalSeconds); + }; + + getTimeAction(); + + var sendResult = complexHelper.SendCodeAsync(senderKey, receiver, bizFlag, code).Result; + + Console.WriteLine("发送结果:{0} 请求时间:{1:yy-MM-dd HH:mm:ss}", sendResult, DateTime.Now); + if (sendResult == SendResult.Success || sendResult == SendResult.IntervalLimit) + { + Console.WriteLine("*****************************"); + while (true) + { + Console.WriteLine("请输入校验码:"); + var vCode = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(vCode)) continue; + getTimeAction(); + var vResult = complexHelper.VerifyCodeAsync(senderKey, receiver, bizFlag, vCode).Result; + Console.WriteLine("{2:yy-MM-dd HH:mm:ss }校验码 {0} 校验结果:{1}", vCode, vResult, DateTime.Now); + if (vResult != VerificationResult.Failed) + { + break; + } + } + } + } + + private static void LoginError(ComplexHelper complexHelper) + { + Console.WriteLine("****** 您当前正在进行密码校验错误Demo ******"); + var senderKey = NoneSender.DefaultKey; //密码校验错误无需实际发送内容 + var bizFlag = configuration.GetValue("BizFlag"); + var correct = "1"; //密码正确时,得到的code + var error = "0"; //密码错误时,得到的code + var correctPwd = "123456";//这里不管输入的是什么账号,密码假设为同一个密码 + Console.WriteLine("所有账户的正确密码为:" + correctPwd); + do + { + var account = ReadLine("** 请输入您的账号 **", "账号不能为空"); + var pwd = ReadLine("** 请输入您的密码 **", "密码不能为空"); + //检验前进行一次发送,因为周期设置成只能发送一次,所以无需关心发送结果 + var sendResult = complexHelper.SendCodeAsync(senderKey, account, bizFlag, correct).Result; + Console.WriteLine("周期设置结果:" + sendResult + " 注意周期设置结果不影响校验"); + + var code = GetCode(pwd); + var verifyResult = complexHelper.VerifyCodeAsync(senderKey, account, bizFlag, code, true).Result;//密码校验正确时,重置允许密码连续错误的次数 + Console.WriteLine("您账号 {0} 的密码验证结果 {1}", account, verifyResult); + } + while (true); + + string GetCode(string pwd) + { + return pwd == correctPwd ? correct : error; + } + + string ReadLine(string title, string errorNotice) + { + Console.WriteLine(title); + do + { + var input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) + { + Console.WriteLine(errorNotice); + } + else + { + return input; + } + } + while (true); + } + } + } +} diff --git a/src/CheckCodeHelper.Samples/ConsoleSender.cs b/src/CheckCodeHelper.Samples/ConsoleSender.cs index 2756796..8ed477a 100644 --- a/src/CheckCodeHelper.Samples/ConsoleSender.cs +++ b/src/CheckCodeHelper.Samples/ConsoleSender.cs @@ -8,7 +8,7 @@ namespace CheckCodeHelper.Samples /// /// 在控制台输出校验码 /// - public class ConsoleSender : ICodeSender + public class ConsoleSender : ICodeSender, ICodeSenderSupportAsync { public ConsoleSender(IContentFormatter formatter) { @@ -16,13 +16,20 @@ public ConsoleSender(IContentFormatter formatter) } public IContentFormatter Formatter { get; } - public bool IsSupport(string receiver) => true; + public string Key { get; set; } = "Console"; - public Task Send(string receiver, string bizFlag, string code, TimeSpan effectiveTime) + public bool IsSupport(string receiver) => true; + + public Task IsSupportAsync(string receiver) + { + return Task.FromResult(this.IsSupport(receiver)); + } + + public Task SendAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime) { - var content = this.Formatter.GetContent(receiver, bizFlag, code, effectiveTime); + var content = this.Formatter.GetContent(receiver, bizFlag, code, effectiveTime, this.Key); Console.WriteLine("发送内容:{0}", content); return Task.FromResult(true); - } - } + } + } } diff --git a/src/CheckCodeHelper.Samples/Program.cs b/src/CheckCodeHelper.Samples/Program.cs index d4745db..e2a56b1 100644 --- a/src/CheckCodeHelper.Samples/Program.cs +++ b/src/CheckCodeHelper.Samples/Program.cs @@ -1,160 +1,187 @@ -using CheckCodeHelper.Sender.EMail; -using CheckCodeHelper.Sender.Sms; -using CheckCodeHelper.Storage.MemoryCache; -using CheckCodeHelper.Storage.Redis; -using CheckCodeHelper.Storage.RedisCache; -using StackExchange.Redis; -using StackExchange.Redis.Extensions.Core.Abstractions; -using StackExchange.Redis.Extensions.Core.Configuration; -using StackExchange.Redis.Extensions.Core.Implementations; -using StackExchange.Redis.Extensions.Newtonsoft; -using StackExchange.Redis.Extensions.Protobuf; -using System; - -namespace CheckCodeHelper.Samples -{ - class Program - { - static readonly string bizFlag="forgetPassword"; - static readonly string receiver = "test";//根据不同的sender,输入不同的接收验证码账号 - static readonly TimeSpan effectiveTime = TimeSpan.FromMinutes(1); - static void Main(string[] args) - { - ICodeSender sender = null; - ICodeStorage storage; - - //storage = GetRedisCacheStorage(); //基于StackExchange.Redis.Extensions.Core - //storage = GetRedisStorage(); //仅基于StackExchange.Redis - storage = GetMemoryCacheStorage(); //基于MemoryCache - - //sender = new NoneSender(); //无需发送验证码场景 - //sender = GetSmsSender(); //通过短信发送验证码 - //sender = GetEMailSender(); //通过邮件发送验证码 - - CheckCodeHelperDemo(storage, sender); - Console.ReadLine(); - } - private static void CheckCodeHelperDemo(ICodeStorage storage, ICodeSender sender = null) - { - if (sender == null) - { - sender = new ConsoleSender(GetFormatter(bizFlag)); - } - - var helper = new CodeHelper(sender, storage); - var code = CodeHelper.GetRandomNumber(); //生成随机的验证码 - - Action getTimeAction = () => - { - //ICodeStorage.GetLastSetCodeTime用于获取最后一次发送校验码时间 - //用于比如手机验证码发送后,用户刷新页面时,页面上用于按钮倒计时计数的计算 - var time = storage.GetLastSetCodeTime(receiver, bizFlag).Result; - if (time.HasValue) - { - Console.WriteLine("上次发送时间:{0:yy-MM-dd HH:mm:ss.fff}", time.Value); - } - else - { - Console.WriteLine("未能获取到最后一次发送时间"); - } - }; - getTimeAction(); - - var sendResult = helper.SendCode(receiver, bizFlag, code, effectiveTime, new PeriodLimit - { - //设置周期为20分钟,然后在此段时间内最多允许发送验证码5次 - MaxLimit = 5, - Period = TimeSpan.FromMinutes(20) - }).Result; - - Console.WriteLine("发送结果:{0} 发送时间:{1:yy-MM-dd HH:mm:ss}", sendResult, DateTime.Now); - if (sendResult == SendResult.Success) - { - Console.WriteLine("*****************************"); - while (true) - { - Console.WriteLine("请输入校验码:"); - var vCode = Console.ReadLine(); - if (string.IsNullOrWhiteSpace(vCode)) continue; - getTimeAction(); - var vResult = helper.VerifyCode(receiver, bizFlag, vCode, 3).Result; - Console.WriteLine("{2:yy-MM-dd HH:mm:ss }校验码 {0} 校验结果:{1}", vCode, vResult, DateTime.Now); - if (vResult != VerificationResult.VerificationFailed) - { - break; - } - } - } - } - #region ICodeSender - private static ICodeSender GetEMailSender() - { - var setting = new EMailSetting()//设置您的smtp信息 - { - Host = "smtp.exmail.qq.com", - Port = 465, - UseSsl = true, - UserName = "测试", - Password = "",//填入发送邮件的邮箱密码 - UserAddress = "",//填入发送邮件的邮箱地址 - }; - string func(string b) => "找回密码验证码测试邮件"; - var sender = new EMailSender(GetFormatter(bizFlag), setting, func) - { - TextFormat = MimeKit.Text.TextFormat.Plain//设置发送的邮件内容格式 - }; - return sender; - } - private static ICodeSender GetSmsSender() - { - //此处以亿美为例 - string host = "shmtn.b2m.cn:80"; - string appid = "11";//填入亿美appid - string secretKey = "22"; //填入亿美secretKey - ISms sms = new EmaySms(host, appid, secretKey); - var sender = new SmsSender(GetFormatter(bizFlag), sms); - return sender; - } - #endregion - #region IContentFormatter - private static IContentFormatter GetFormatter(string bizFlag) - { - var simpleFormatter = new ContentFormatter( - (r, b, c, e) => $"{r}您好,您的忘记密码验证码为{c},有效期为{(int)e.TotalSeconds}秒."); - //如果就一个业务场景,也可以直接返回simpleFormatter - var formatter = new ComplexContentFormatter(); - formatter.SetFormatter(bizFlag, simpleFormatter); - return formatter; - } - #endregion - #region ICodeStorage - private static ICodeStorage GetRedisCacheStorage() - { - var redisConfig = new RedisConfiguration - { - Hosts = new RedisHost[] { - new RedisHost{ - Host="127.0.0.1", - Port=6379 - } - } - }; - var redisManager = new RedisCacheConnectionPoolManager(redisConfig); - var redisClient = new RedisCacheClient(redisManager, - new NewtonsoftSerializer(), redisConfig);//new ProtobufSerializer(); - var storage = new RedisCacheStorage(redisClient); - return storage; - } - private static ICodeStorage GetRedisStorage() - { - var multiplexer = ConnectionMultiplexer.Connect("127.0.0.1:6379"); - var storage = new RedisStorage(multiplexer); - return storage; - } - private static ICodeStorage GetMemoryCacheStorage() - { - return new MemoryCacheStorage(); - } - #endregion - } -} +using CheckCodeHelper.Sender.EMail; +using CheckCodeHelper.Sender.Sms; +using CheckCodeHelper.Storage.Memory; +using CheckCodeHelper.Storage.Redis; +using CheckCodeHelper.Storage.RedisCache; +using Microsoft.Extensions.Options; +using StackExchange.Redis; +using StackExchange.Redis.Extensions.Core.Abstractions; +using StackExchange.Redis.Extensions.Core.Configuration; +using StackExchange.Redis.Extensions.Core.Implementations; +using StackExchange.Redis.Extensions.Newtonsoft; +using StackExchange.Redis.Extensions.Protobuf; +using System; +using System.Collections.Generic; +using static CheckCodeHelper.Sender.EMail.EMailMimeMessageSetting; + +namespace CheckCodeHelper.Samples +{ + class Program + { + static readonly string BizFlag = "ForgetAndResetPassword"; + static readonly string Receiver = "test";//根据不同的sender,输入不同的接收验证码账号 + static readonly TimeSpan effectiveTime = TimeSpan.FromMinutes(1); + static void Main(string[] args) + { + ComplexHelperTest.Start(); + + //PrevDemo(); + + Console.ReadLine(); + + } + + private static void PrevDemo() + { + ICodeSender sender = null; + ICodeStorage storage; + + //storage = GetRedisCacheStorage(); //基于StackExchange.Redis.Extensions.Core + //storage = GetRedisStorage(); //仅基于StackExchange.Redis + storage = GetMemoryCacheStorage(); //基于MemoryCache + + //sender = new NoneSender(); //无需发送验证码场景 + //sender = GetSmsSender(); //通过短信发送验证码 + //sender = GetEMailSender(); //通过邮件发送验证码 + + CheckCodeHelperDemo(storage, sender); + } + + private static void CheckCodeHelperDemo(ICodeStorage storage, ICodeSender sender = null) + { + if (sender == null) + { + var senderKey = "CONSOLE"; + sender = new ConsoleSender(GetFormatter(BizFlag, senderKey)) + { + Key = senderKey, + }; + } + + var helper = new CodeHelper(sender, storage); + var code = CodeHelper.GetRandomNumber(); //生成随机的验证码 + + Action getTimeAction = () => + { + //ICodeStorage.GetLastSetCodeTime用于获取最后一次发送校验码时间 + //用于比如手机验证码发送后,用户刷新页面时,页面上用于按钮倒计时计数的计算 + var time = storage.GetLastSetCodeTimeAsync(Receiver, BizFlag).Result; + if (time.HasValue) + { + Console.WriteLine("上次发送时间:{0:yy-MM-dd HH:mm:ss.fff}", time.Value); + } + else + { + Console.WriteLine("未能获取到最后一次发送时间"); + } + }; + getTimeAction(); + + var sendResult = helper.SendCodeAsync(Receiver, BizFlag, code, effectiveTime, new PeriodLimit + { + //设置周期为20分钟,然后在此段时间内最多允许发送验证码5次 + MaxLimit = 5, + Period = TimeSpan.FromMinutes(20) + }).Result; + + Console.WriteLine("发送结果:{0} 发送时间:{1:yy-MM-dd HH:mm:ss}", sendResult, DateTime.Now); + if (sendResult == SendResult.Success) + { + Console.WriteLine("*****************************"); + while (true) + { + Console.WriteLine("请输入校验码:"); + var vCode = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(vCode)) continue; + getTimeAction(); + var vResult = helper.VerifyCodeAsync(Receiver, BizFlag, vCode, 3).Result; + Console.WriteLine("{2:yy-MM-dd HH:mm:ss }校验码 {0} 校验结果:{1}", vCode, vResult, DateTime.Now); + if (vResult != VerificationResult.Failed) + { + break; + } + } + } + } + #region ICodeSender + private static ICodeSender GetEMailSender() + { + var emailSetting = new EMailSetting()//设置您的smtp信息 + { + Host = "smtp.exmail.qq.com", + Port = 465, + UseSsl = true, + UserName = "测试", + Password = "",//填入发送邮件的邮箱密码 + UserAddress = "",//填入发送邮件的邮箱地址 + }; + var subjectSetting = new EMailMimeMessageSetting + { + DefaultTextFormat = MimeKit.Text.TextFormat.Plain, + Parameters = new Dictionary + { + { BizFlag , new MimeMessageParameter{ Subject="找回密码验证码测试邮件"}} + } + }; + var helper = new EMailHelper(Options.Create(emailSetting)); + var sender = new EMailSender(GetFormatter(BizFlag, EMailSender.DefaultKey), helper, Options.Create(subjectSetting)); + return sender; + } + private static ICodeSender GetSmsSender() + { + //此处以亿美为例 + string host = "http://shmtn.b2m.cn:80"; + string appid = "11";//填入亿美appid + string secretKey = "22"; //填入亿美secretKey + ISms sms = new EmaySms(Options.Create(new EmaySetting + { + Host = host, + AppId = appid, + SecretKey = secretKey + })); + var sender = new SmsSender(GetFormatter(BizFlag, SmsSender.DefaultKey), sms); + return sender; + } + #endregion + #region IContentFormatter + private static IContentFormatter GetFormatter(string bizFlag, string senderKey) + { + var simpleFormatter = new ContentFormatter( + (r, b, c, e, s) => $"{r}您好,您的忘记密码验证码为{c},有效期为{(int)e.TotalSeconds}秒."); + //如果就一个业务场景,也可以直接返回simpleFormatter + var formatter = new ComplexContentFormatter(); + formatter.SetFormatter(bizFlag, senderKey, simpleFormatter); + return formatter; + } + #endregion + #region ICodeStorage + private static ICodeStorage GetRedisCacheStorage() + { + var redisConfig = new RedisConfiguration + { + Hosts = new RedisHost[] { + new RedisHost{ + Host="127.0.0.1", + Port=6379 + } + } + }; + var redisManager = new RedisCacheConnectionPoolManager(redisConfig); + var redisClient = new RedisCacheClient(redisManager, + new NewtonsoftSerializer(), redisConfig);//new ProtobufSerializer(); + var storage = new RedisCacheStorage(redisClient); + return storage; + } + private static ICodeStorage GetRedisStorage() + { + var multiplexer = ConnectionMultiplexer.Connect("127.0.0.1:6379"); + var storage = new RedisStorage(multiplexer); + return storage; + } + private static ICodeStorage GetMemoryCacheStorage() + { + return new MemoryCacheStorage(); + } + #endregion + } +} diff --git a/src/CheckCodeHelper.Samples/appsettings.json b/src/CheckCodeHelper.Samples/appsettings.json new file mode 100644 index 0000000..c5139fd --- /dev/null +++ b/src/CheckCodeHelper.Samples/appsettings.json @@ -0,0 +1,116 @@ +{ + "Redis": { //Redis配置 + "Configuration": "127.0.0.1:6379" + }, + "EmaySetting": { //亿美短信配置 + "Host": "http://shmtn.b2m.cn:80", + "AppId": "111", + "SecretKey": "2222" + }, + "AlibabaConfig": { //阿里短信账号配置 + "AccessKeyId": "", + "AccessKeySecret": "", + "Endpoint": "dysmsapi.aliyuncs.com" + }, + "AlibabaSmsParameterSetting": { //阿里短信请求参数配置 + "DefaultSignName": "阿里云短信测试专用", + "Parameters": { + "ForgetAndResetPassword": { + "SignName": null, + "TemplateCode": "SMS_207948308" + } + } + }, + "EMailMimeMessageSetting": { //邮件Mime Message配置 + "DefaultTextFormat": "Plain", + "Parameters": { + "ForgetAndResetPassword": { + "Subject": "找回密码", + "TextFormat": null + } + } + }, + "EMailSetting": { //邮箱配置 + "Host": "smtp.mxhichina.com", + "Port": 465, + "UseSsl": true, + "UserName": "测试邮箱", + "UserAddress": "阿里邮箱地址", + "Password": "邮箱密码" + }, + "ComplexSetting": { //所有字典类属性Key值的组成方式均为 ICodeSender.Key + 下划线 + 业务标志bizFlag + "EffectiveTimeDisplayed": "Auto", //设置为Auto会导致ContentFormatters有效期部分的展示发生变化 + "ContentFormatters": { //内容模板 + "NONE_ForgetAndResetPassword": "", + + "Console_ForgetAndResetPassword": "{0}的{1}(找回密码)验证码为{2},有效期为{3}秒", + + "EMAIL_ForgetAndResetPassword": "您的找回密码验证码为{2},有效期为{3}分钟。", + + "SMS_ForgetAndResetPassword": "【亿美软通】找回密码验证码为{2},有效期为{3}分钟,请尽快使用。", + + "ALIBABASMS_ForgetAndResetPassword": "{{\"code\":\"{2}\",\"time\":\"{3}\"}}" //阿里短信此处为Json格式,注意非表示占位的花括号需要用两个表示 {{ }} + }, + "PeriodMaxLimits": { //周期次数限制 + "NONE_LoginValidError": 1, //登录错误校验比较特殊,需要设置为1,即一个周期内只允许设置一次值 + + "NONE_ForgetAndResetPassword": 10, + + "EMAIL_ForgetAndResetPassword": 5, + + "SMS_ForgetAndResetPassword": 5, + + "ALIBABASMS_ForgetAndResetPassword": 5 + }, + "PeriodLimitSeconds": { //周期时间限制(秒) + "NONE_LoginValidError": 3600, //登录错误校验的周期限制,一般为24小时,Demo设置为1小时 + + "NONE_ForgetAndResetPassword": 120, + + "EMAIL_ForgetAndResetPassword": 3600, + + "SMS_ForgetAndResetPassword": 3600, + + "ALIBABASMS_ForgetAndResetPassword": 3600 + }, + "PeriodLimitIntervalSeconds": { //周期内校验码发送间隔限制(秒) + //"NONE_LoginValidError": 30, //因为登录错误一个周期只允许发一次,所以这里可以不用设置 + + "NONE_ForgetAndResetPassword": 40, //对应验证码有效时间配置为30,超出部分无效且会造成CD读取到的时间在最后10秒突然没了 + + "SMS_ForgetAndResetPassword": 60, + + "ALIBABASMS_ForgetAndResetPassword": 60 + }, + "CodeEffectiveSeconds": { //验证码有效时间(秒) + "NONE_LoginValidError": 3600, //登录错误校验的验证码有效期需与周期时间限制一致 + + "NONE_ForgetAndResetPassword": 30, + + "Console_ForgetAndResetPassword": 40, + + "EMAIL_ForgetAndResetPassword": 600, + + "SMS_ForgetAndResetPassword": 120, + + "ALIBABASMS_ForgetAndResetPassword": 120 + }, + "CodeMaxErrorLimits": { //校验错误次数 + "NONE_LoginValidError": 5, //设置一个周期内,密码错误允许连续错误几次 + + "NONE_ForgetAndResetPassword": 3, + + "Console_ForgetAndResetPassword": 5, + + "EMAIL_ForgetAndResetPassword": 5, + + "SMS_ForgetAndResetPassword": 5, + + "ALIBABASMS_ForgetAndResetPassword": 5 + } + }, + //以下为业务测试数据,实际不应该走配置 + "CurrentSenderKey": "Console", + "BizFlag": "ForgetAndResetPassword", //LoginValidError + "Receiver": "test" +} diff --git a/src/CheckCodeHelper.Sender.AlibabaSms/AlibabaSmsExtensions.cs b/src/CheckCodeHelper.Sender.AlibabaSms/AlibabaSmsExtensions.cs new file mode 100644 index 0000000..36b51e6 --- /dev/null +++ b/src/CheckCodeHelper.Sender.AlibabaSms/AlibabaSmsExtensions.cs @@ -0,0 +1,35 @@ +#if NETSTANDARD2_0_OR_GREATER +using AlibabaCloud.OpenApiClient.Models; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CheckCodeHelper.Sender.AlibabaSms +{ + /// + /// 仅用于Net Core的注册方法 + /// + public static class AlibabaSmsExtensions + { + /// + /// 注册阿里短信发送相关的服务,注意此方法仅适用于全系统就一种的情况 + /// 注意此方法不会注册依赖的 + /// + /// + /// 仅包含的配置节点 + /// 仅包含的配置节点 + /// + public static IServiceCollection AddSingletonForAlibabaSms(this IServiceCollection services, IConfiguration config, IConfiguration setting) + { + services.Configure(config); + services.Configure(setting); + services.AddSingleton(); + return services; + } + } +} +#endif diff --git a/src/CheckCodeHelper.Sender.AlibabaSms/AlibabaSmsParameterSetting.cs b/src/CheckCodeHelper.Sender.AlibabaSms/AlibabaSmsParameterSetting.cs new file mode 100644 index 0000000..f8dfac6 --- /dev/null +++ b/src/CheckCodeHelper.Sender.AlibabaSms/AlibabaSmsParameterSetting.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CheckCodeHelper.Sender.AlibabaSms +{ + /// + /// 阿里短信请求参数配置 + /// + public class AlibabaSmsParameterSetting + { + /// + /// 默认的短信签名,如果未设置则以此为短信签名 + /// + public string DefaultSignName { get; set; } + + /// + /// 参数字典 Key为bizFlag + /// + public IDictionary Parameters { get; set; } + /// + /// 阿里短信请求参数 https://next.api.aliyun.com/document/Dysmsapi/2017-05-25/SendSms + /// + public class AlibabaSmsRequestParameter + { + /// + /// 短信签名名称 + /// + public string SignName { get; set; } + /// + /// 短信模板ID + /// + public string TemplateCode { get; set; } + } + } +} diff --git a/src/CheckCodeHelper.Sender.AlibabaSms/AlibabaSmsSender.cs b/src/CheckCodeHelper.Sender.AlibabaSms/AlibabaSmsSender.cs new file mode 100644 index 0000000..d838387 --- /dev/null +++ b/src/CheckCodeHelper.Sender.AlibabaSms/AlibabaSmsSender.cs @@ -0,0 +1,132 @@ +#if NETSTANDARD2_0_OR_GREATER +using Microsoft.Extensions.Options; +#endif +using AlibabaCloud.OpenApiClient.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using static CheckCodeHelper.Sender.AlibabaSms.AlibabaSmsParameterSetting; +using AlibabaCloud.SDK.Dysmsapi20170525; +using AlibabaCloud.SDK.Dysmsapi20170525.Models; + +namespace CheckCodeHelper.Sender.AlibabaSms +{ + /// + /// 通过阿里短信发送校验码 + /// + public class AlibabaSmsSender : ICodeSender + { + /// + /// 默认设置的 + /// + public const string DefaultKey = "ALIBABASMS"; + private readonly AlibabaSmsParameterSetting setting; + private readonly Config config; + + /// + /// 通过阿里短信发送校验码 + /// + /// + /// + /// + public AlibabaSmsSender( + IContentFormatter formatter, +#if NETSTANDARD2_0_OR_GREATER + IOptions setting, + IOptions config +#else + AlibabaSmsParameterSetting setting, + Config config +#endif + ) + { + this.Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); +#if NETSTANDARD2_0_OR_GREATER + this.setting = setting.Value; + this.config = config.Value; +#else + this.setting = setting ?? throw new ArgumentNullException(nameof(setting)); + this.config = config; +#endif + if (this.setting.Parameters == null || this.setting.Parameters.Count == 0) + { + throw new ArgumentException(nameof(this.setting.Parameters)); + } + } + + /// + /// 发送验证码Json模板 + /// + public IContentFormatter Formatter { get; } + + /// + /// 用于标志当前sender的唯一Key + /// + public string Key { get; set; } = DefaultKey; + + /// + /// 判断接收者是否符合发送条件,目前是宽松判断,即正则 ^1\d{10}$ + /// + /// + /// + public virtual bool IsSupport(string receiver) + { + return !string.IsNullOrWhiteSpace(receiver) + && Regex.IsMatch(receiver, @"^1\d{10}$"); + } + + /// + /// 发送校验码信息 + /// + /// 接收方 + /// 业务标志 + /// 校验码 + /// 校验码有效时间范围 + /// + public async Task SendAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime) + { + var param = this.GetRequestParameter(bizFlag); + var request = new SendSmsRequest + { + PhoneNumbers = receiver, + SignName = this.GetSignName(param, bizFlag), + TemplateCode = param.TemplateCode, + TemplateParam = this.Formatter.GetContent(receiver, bizFlag, code, effectiveTime, this.Key), + }; + var client = this.CreateClient(); + var response = await client.SendSmsAsync(request).ConfigureAwait(false); + return string.Equals(response.Body.Code, "OK", StringComparison.OrdinalIgnoreCase); + } + + private Client CreateClient() + { + return new Client(this.config); + } + + private AlibabaSmsRequestParameter GetRequestParameter(string bizFlag) + { + if (!this.setting.Parameters.ContainsKey(bizFlag)) + { + throw new KeyNotFoundException($"The request parameter for '{bizFlag}' is not found"); + } + return this.setting.Parameters[bizFlag]; + } + + private string GetSignName(AlibabaSmsRequestParameter parameter, string bizFlag) + { + var signName = parameter.SignName; + if (string.IsNullOrWhiteSpace(signName)) + { + signName = this.setting.DefaultSignName; + if (string.IsNullOrWhiteSpace(signName)) + { + throw new ArgumentException($"The sign name for '{bizFlag}' is not correct"); + } + } + return signName; + } + } +} diff --git a/src/CheckCodeHelper.Sender.AlibabaSms/CheckCodeHelper.Sender.AlibabaSms.csproj b/src/CheckCodeHelper.Sender.AlibabaSms/CheckCodeHelper.Sender.AlibabaSms.csproj new file mode 100644 index 0000000..6764824 --- /dev/null +++ b/src/CheckCodeHelper.Sender.AlibabaSms/CheckCodeHelper.Sender.AlibabaSms.csproj @@ -0,0 +1,27 @@ + + + + net45;netstandard2.0 + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + dong fang + 通用验证码发送及校验类库--通过阿里短信发送校验信息 + https://github.com/fdstar/CheckCodeHelper + https://mit-license.org/ + false + 1.0.0 + MIT + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/CheckCodeHelper.Sender.EMail/AttachmentInfo.cs b/src/CheckCodeHelper.Sender.EMail/AttachmentInfo.cs index 5f4cb42..aaf6bca 100644 --- a/src/CheckCodeHelper.Sender.EMail/AttachmentInfo.cs +++ b/src/CheckCodeHelper.Sender.EMail/AttachmentInfo.cs @@ -11,8 +11,16 @@ namespace CheckCodeHelper.Sender.EMail /// /// 邮件附件信息 /// - public class AttachmentInfo : IDisposable + public sealed class AttachmentInfo : IDisposable { + /// + /// 释放 + /// + ~AttachmentInfo() + { + this.Dispose(); + } + /// /// 附件类型 /// diff --git a/src/CheckCodeHelper.Sender.EMail/CheckCodeHelper.Sender.EMail.csproj b/src/CheckCodeHelper.Sender.EMail/CheckCodeHelper.Sender.EMail.csproj index 7679534..4b8dd97 100644 --- a/src/CheckCodeHelper.Sender.EMail/CheckCodeHelper.Sender.EMail.csproj +++ b/src/CheckCodeHelper.Sender.EMail/CheckCodeHelper.Sender.EMail.csproj @@ -1,8 +1,15 @@  - net461;netstandard2.0 + net45;netstandard2.0 bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + dong fang + 通用验证码发送及校验类库--通过邮件发送校验信息 + https://github.com/fdstar/CheckCodeHelper + https://mit-license.org/ + false + 1.0.3 + MIT @@ -13,4 +20,8 @@ + + + + diff --git a/src/CheckCodeHelper.Sender.EMail/EMailExtensions.cs b/src/CheckCodeHelper.Sender.EMail/EMailExtensions.cs new file mode 100644 index 0000000..1c44e2d --- /dev/null +++ b/src/CheckCodeHelper.Sender.EMail/EMailExtensions.cs @@ -0,0 +1,48 @@ +#if NETSTANDARD2_0_OR_GREATER +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CheckCodeHelper.Sender.EMail +{ + /// + /// 仅用于Net Core的注册方法 + /// + public static class EMailExtensions + { + /// + /// 注册邮件发送相关的服务,注意此方法仅适用于全系统就一种的情况 + /// 注意此方法不会注册依赖的 + /// + /// + /// 仅包含的配置节点 + /// 仅包含的配置节点 + /// + public static IServiceCollection AddSingletonForEMailSender(this IServiceCollection services, IConfiguration emailSetting, IConfiguration emailMimeMessageSetting) + { + services.Configure(emailMimeMessageSetting); + services.AddSingletonForEMailHelper(emailSetting); + services.AddSingleton(); + return services; + } + + /// + /// 注册邮件发送辅助类的相关实现,注意此方法不注册 + /// + /// + /// 仅包含的配置节点 + /// + public static IServiceCollection AddSingletonForEMailHelper(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration); + services.AddSingleton(); + return services; + } + } +} +#endif diff --git a/src/CheckCodeHelper.Sender.EMail/EMailHelper.cs b/src/CheckCodeHelper.Sender.EMail/EMailHelper.cs index fafc314..da1b2f6 100644 --- a/src/CheckCodeHelper.Sender.EMail/EMailHelper.cs +++ b/src/CheckCodeHelper.Sender.EMail/EMailHelper.cs @@ -1,4 +1,4 @@ -using MailKit.Net.Smtp; +using MailKit.Net.Smtp; using MimeKit; using MimeKit.Text; using System; @@ -8,7 +8,10 @@ using System.Threading.Tasks; namespace CheckCodeHelper.Sender.EMail -{ +{ +#if NETSTANDARD2_0_OR_GREATER + using Microsoft.Extensions.Options; +#endif /// /// 发送邮件辅助类 /// @@ -17,12 +20,29 @@ public class EMailHelper /// /// 邮箱配置 /// - public EMailSetting Setting { get; } + public EMailSetting Setting { get; } + +#if NETSTANDARD2_0_OR_GREATER + /// + /// 邮件发送设置 + /// + /// + public EMailHelper(IOptions option) + : this(option.Value) + { + } +#endif + /// /// 邮件发送设置 /// /// - public EMailHelper(EMailSetting setting) +#if NETSTANDARD2_0_OR_GREATER + private +#else + public +#endif + EMailHelper(EMailSetting setting) { this.Setting = setting ?? throw new ArgumentNullException(nameof(setting)); } @@ -74,46 +94,25 @@ public async Task SendEMailAsync(string subject, string content, MailboxAddress /// 密送方信息 /// 是否自动释放附件所用Stream /// - public async Task SendEMailAsync(string subject, string content, IEnumerable fromAddress, IEnumerable toAddress, TextFormat textFormat = TextFormat.Text, IEnumerable attachments = null, IEnumerable ccAddress=null, IEnumerable bccAddress = null, bool dispose = true) + public async Task SendEMailAsync(string subject, string content, IEnumerable fromAddress, IEnumerable toAddress, TextFormat textFormat = TextFormat.Text, IEnumerable attachments = null, IEnumerable ccAddress = null, IEnumerable bccAddress = null, bool dispose = true) { var message = new MimeMessage(); message.From.AddRange(fromAddress); message.To.AddRange(toAddress); - if (ccAddress != null && ccAddress.Any()) + if (!this.IsEmpty(ccAddress)) { - message.Cc.AddRange(ccAddress); + message.Cc.AddRange(ccAddress); } - if (bccAddress != null && bccAddress.Any()) + if (!this.IsEmpty(bccAddress)) { - message.Bcc.AddRange(bccAddress); + message.Bcc.AddRange(bccAddress); } message.Subject = subject; var body = new TextPart(textFormat) { Text = content - }; - MimeEntity entity = body; - if (attachments != null) - { - var mult = new Multipart("mixed") - { - body - }; - foreach (var att in attachments) - { - if (att.Stream != null) - { - var attachment = string.IsNullOrWhiteSpace(att.ContentType) ? new MimePart() : new MimePart(att.ContentType); - attachment.Content = new MimeContent(att.Stream); - attachment.ContentDisposition = new ContentDisposition(ContentDisposition.Attachment); - attachment.ContentTransferEncoding = att.ContentTransferEncoding; - attachment.FileName = ConvertHeaderToBase64(att.FileName, Encoding.UTF8);//解决附件中文名问题 - mult.Add(attachment); - } - } - entity = mult; - } - message.Body = entity; + }; + message.Body = this.GetMimeEntity(body, attachments); message.Date = DateTime.Now; using (var client = new SmtpClient()) { @@ -153,5 +152,34 @@ private string ConvertHeaderToBase64(string inputStr, Encoding encoding) } return inputStr; } - } + private bool IsEmpty(IEnumerable source) + { + return source == null || !source.Any(); + } + private MimeEntity GetMimeEntity(MimePart body, IEnumerable attachments) + { + MimeEntity entity = body; + if (!this.IsEmpty(attachments)) + { + var mult = new Multipart("mixed") + { + body + }; + foreach (var att in attachments) + { + if (att.Stream != null) + { + var attachment = string.IsNullOrWhiteSpace(att.ContentType) ? new MimePart() : new MimePart(att.ContentType); + attachment.Content = new MimeContent(att.Stream); + attachment.ContentDisposition = new ContentDisposition(ContentDisposition.Attachment); + attachment.ContentTransferEncoding = att.ContentTransferEncoding; + attachment.FileName = ConvertHeaderToBase64(att.FileName, Encoding.UTF8);//解决附件中文名问题 + mult.Add(attachment); + } + } + entity = mult; + } + return entity; + } + } } \ No newline at end of file diff --git a/src/CheckCodeHelper.Sender.EMail/EMailMimeMessageSetting.cs b/src/CheckCodeHelper.Sender.EMail/EMailMimeMessageSetting.cs new file mode 100644 index 0000000..25824df --- /dev/null +++ b/src/CheckCodeHelper.Sender.EMail/EMailMimeMessageSetting.cs @@ -0,0 +1,38 @@ +using MimeKit.Text; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CheckCodeHelper.Sender.EMail +{ + /// + /// 邮件MIME Message配置 + /// + public class EMailMimeMessageSetting + { + /// + /// 默认的邮件内容格式,如果不设置默认为 + /// + public TextFormat DefaultTextFormat { get; set; } + /// + /// 邮件配置 Key为bizFlag + /// + public IDictionary Parameters { get; set; } + /// + /// 邮件参数 + /// + public class MimeMessageParameter + { + /// + /// 邮件主题 + /// + public string Subject { get; set; } + /// + /// 邮件内容的格式 + /// + public TextFormat? TextFormat { get; set; } + } + } +} diff --git a/src/CheckCodeHelper.Sender.EMail/EMailSender.cs b/src/CheckCodeHelper.Sender.EMail/EMailSender.cs index b556800..da8dbcf 100644 --- a/src/CheckCodeHelper.Sender.EMail/EMailSender.cs +++ b/src/CheckCodeHelper.Sender.EMail/EMailSender.cs @@ -1,4 +1,7 @@ -using MimeKit; +#if NETSTANDARD2_0_OR_GREATER +using Microsoft.Extensions.Options; +#endif +using MimeKit; using MimeKit.Text; using System; using System.Collections.Generic; @@ -6,42 +9,68 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; - +using static CheckCodeHelper.Sender.EMail.EMailMimeMessageSetting; + namespace CheckCodeHelper.Sender.EMail { /// /// 通过邮件发送验证码 /// public class EMailSender : ICodeSender - { + { + /// + /// 默认设置的 + /// + public const string DefaultKey = "EMAIL"; + private readonly EMailMimeMessageSetting mimeMessageSetting; + +#if NETSTANDARD2_0_OR_GREATER + /// + /// 通过邮件发送验证码 + /// + /// 验证码内容模板 + /// 邮件发送者 + /// 邮件主题配置 + public EMailSender(IContentFormatter formatter, EMailHelper helper, IOptions mimeMessageSetting) + { + this.Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + this.EMailHelper = helper ?? throw new ArgumentNullException(nameof(helper)); + this.mimeMessageSetting = mimeMessageSetting.Value ?? throw new ArgumentNullException(nameof(mimeMessageSetting)); + if (this.mimeMessageSetting.Parameters == null || this.mimeMessageSetting.Parameters.Count == 0) + { + throw new ArgumentException(nameof(this.mimeMessageSetting.Parameters)); + } + } +#else /// - /// 通过短信发送验证码 + /// 通过邮件发送验证码 /// /// 验证码内容模板 - /// 邮箱配置 - /// 根据业务标志返回对应的邮件主题 - public EMailSender(IContentFormatter formatter, EMailSetting setting, Func subjectFunc) + /// 邮箱配置 + /// 邮件主题配置 + public EMailSender(IContentFormatter formatter, EMailSetting emailSetting, EMailMimeMessageSetting subjectSetting) { this.Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); - this.SubjectFunc = subjectFunc ?? throw new ArgumentNullException(nameof(subjectFunc)); - this.EMailHelper = new EMailHelper(setting ?? throw new ArgumentNullException(nameof(setting))); + this.mimeMessageSetting = subjectSetting ?? throw new ArgumentNullException(nameof(subjectSetting)); + this.EMailHelper = new EMailHelper(emailSetting ?? throw new ArgumentNullException(nameof(emailSetting))); + if (this.mimeMessageSetting.Parameters == null || this.mimeMessageSetting.Parameters.Count == 0) + { + throw new ArgumentException(nameof(this.mimeMessageSetting.Parameters)); + } } +#endif /// /// 发送验证码内容模板 /// - public IContentFormatter Formatter { get; } + public IContentFormatter Formatter { get; } /// - /// 邮件发送者 + /// 用于标志当前sender的唯一Key /// - public EMailHelper EMailHelper { get; } + public string Key { get; set; } = DefaultKey; /// - /// 用于生成邮件主题的委托 - /// - public Func SubjectFunc { get; } - /// - /// 发送的邮件内容格式 + /// 邮件发送者 /// - public TextFormat TextFormat { get; set; } + public EMailHelper EMailHelper { get; } /// /// 判断接收者是否符合发送条件 /// @@ -60,14 +89,33 @@ public virtual bool IsSupport(string receiver) /// 校验码 /// 校验码有效时间范围 /// - public virtual async Task Send(string receiver, string bizFlag, string code, TimeSpan effectiveTime) + public virtual async Task SendAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime) { - var subject = this.SubjectFunc(bizFlag); - var content = this.Formatter.GetContent(receiver, bizFlag, code, effectiveTime); - await this.EMailHelper.SendEMailAsync(subject, content, new List { + var parameter = this.GetParameter(bizFlag); + var content = this.Formatter.GetContent(receiver, bizFlag, code, effectiveTime, this.Key); + await this.EMailHelper.SendEMailAsync(parameter.Subject, content, new List { new MailboxAddress(receiver) - }, this.TextFormat).ConfigureAwait(false); - return true; + }, this.GetTextFormat(parameter)).ConfigureAwait(false); + return true; } + + private MimeMessageParameter GetParameter(string bizFlag) + { + if (!this.mimeMessageSetting.Parameters.ContainsKey(bizFlag)) + { + throw new KeyNotFoundException($"The parameter for '{bizFlag}' is not found"); + } + return this.mimeMessageSetting.Parameters[bizFlag]; + } + + private TextFormat GetTextFormat(MimeMessageParameter parameter) + { + var textFormat = parameter.TextFormat; + if (!textFormat.HasValue) + { + textFormat = this.mimeMessageSetting.DefaultTextFormat; + } + return textFormat.Value; + } } } diff --git a/src/CheckCodeHelper.Sender.EMail/EMailSetting.cs b/src/CheckCodeHelper.Sender.EMail/EMailSetting.cs index 2bb8750..f2d7d06 100644 --- a/src/CheckCodeHelper.Sender.EMail/EMailSetting.cs +++ b/src/CheckCodeHelper.Sender.EMail/EMailSetting.cs @@ -32,7 +32,7 @@ public class EMailSetting /// public string UserAddress { get; set; } /// - /// 发现邮件所需的账号密码 + /// 发送邮件所需的账号密码 /// public string Password { get; set; } } diff --git a/src/CheckCodeHelper.Sender.Sms/CheckCodeHelper.Sender.Sms.csproj b/src/CheckCodeHelper.Sender.Sms/CheckCodeHelper.Sender.Sms.csproj index 25072c7..7cacdfe 100644 --- a/src/CheckCodeHelper.Sender.Sms/CheckCodeHelper.Sender.Sms.csproj +++ b/src/CheckCodeHelper.Sender.Sms/CheckCodeHelper.Sender.Sms.csproj @@ -1,17 +1,28 @@ - - - - net461;netstandard2.0 - bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml - - - - - - - - - - - - + + + + net452;netstandard2.0 + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + dong fang + 通用验证码发送及校验类库--通过短信发送校验信息 + https://github.com/fdstar/CheckCodeHelper + https://mit-license.org/ + false + 1.0.1 + MIT + + + + + + + + + + + + + + + + diff --git a/src/CheckCodeHelper.Sender.Sms/EmaySms.cs b/src/CheckCodeHelper.Sender.Sms/EmaySms.cs index f5f7337..a9f5851 100644 --- a/src/CheckCodeHelper.Sender.Sms/EmaySms.cs +++ b/src/CheckCodeHelper.Sender.Sms/EmaySms.cs @@ -1,198 +1,249 @@ -using CheckCodeHelper.Sender.Sms.Utils; -using Newtonsoft.Json; -using RestSharp; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Threading.Tasks; - -namespace CheckCodeHelper.Sender.Sms -{ - /// - /// 亿美短信 http://www.emay.cn/ - /// - public class EmaySms : ISms - { - private readonly string _appId; - private readonly byte[] _secretKey; - private const string SendSingleSmsUrl = "/inter/sendSingleSMS"; - private const string SendBatchSmsUrl = "/inter/sendBatchSMS"; - /// - /// IRestClient - /// - public IRestClient Client { get; } - /// - /// 亿美短信 http://www.emay.cn/ - /// - /// 亿美短信服务Host - /// 亿美短信应用Id - /// 亿美短信应用秘钥 - /// 传输协议 - public EmaySms(string host, string appId, string secretKey, string scheme = "http") - { - if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(appId) || string.IsNullOrWhiteSpace(secretKey)) - { - throw new ArgumentNullException(); - } - this._appId = appId; - var key = new byte[16]; - Array.Copy(Encoding.UTF8.GetBytes(secretKey.PadRight(key.Length)), key, key.Length); - this._secretKey = key; - this.Client = new RestClient(string.Format("{0}://{1}", scheme, host)); - } - /// - /// 请求有效期(秒),默认60 - /// - public int ValidPeriod { get; set; } = 60; - /// - /// 请求时是否需要Gzip压缩,默认true - /// - public bool UseGzip { get; set; } = true; - private IRestRequest GetRestRequest(object data, string url,bool useGZip) - { - var str = JsonConvert.SerializeObject(data); - var request = new RestRequest(url, Method.POST); - request.AddHeader("appId", this._appId); - var rawData = Encoding.UTF8.GetBytes(str); - if (useGZip) - { - request.AddHeader("gzip", "on"); - rawData = GZipHelper.Compress(rawData); - } - var encryptData = AESHelper.Encrypt(rawData, this._secretKey, null, CipherMode.ECB, PaddingMode.PKCS7); - request.AddParameter(new Parameter - { - Type = ParameterType.RequestBody, - DataFormat = DataFormat.None, - Value = encryptData, - Name = "" - }); - return request; - } - private object GetSingleSmsObj(string mobile, string content, string bizId, DateTime? sendTime) - { - var data = new - { - mobile, - content, - timerTime = sendTime.HasValue ? sendTime.Value.ToString("yyyy-MM-dd HH:mm:ss") : string.Empty, - customSmsId = bizId, - requestTime = DateTime.Now.Ticks, - requestValidPeriod = this.ValidPeriod - }; - return data; - } - private bool IsResponseSuccess(IRestResponse response, bool useGZip) - { - bool isSuccess = response.Headers.FirstOrDefault(p => p.Name == "result")?.Value.ToString() == "SUCCESS"; -#if DEBUG - var responseStr = this.GetResponseContent(response, useGZip); -#endif - return isSuccess; - } - private string GetResponseContent(IRestResponse response, bool useGZip) - { - var data = AESHelper.Decrypt(response.RawBytes, this._secretKey, null, CipherMode.ECB, PaddingMode.PKCS7); - if (useGZip) - { - data = GZipHelper.Decompress(data); - } - return Encoding.UTF8.GetString(data); - } - /// - /// 发送单条短信 - /// - /// 手机号 - /// 短信内容 - /// 业务Id - /// 定时发送时间(是否支持看实际短信供应商) - /// - public bool SendMessage(string mobile, string content, string bizId = null, DateTime? sendTime = null) - { - var data = this.GetSingleSmsObj(mobile, content, bizId, sendTime); - var useGZip = this.UseGzip; - var request = this.GetRestRequest(data, SendSingleSmsUrl, useGZip); - var response = this.Client.Execute(request); - return this.IsResponseSuccess(response, useGZip); - } - /// - /// 发送单条短信 - /// - /// 手机号 - /// 短信内容 - /// 业务Id - /// 定时发送时间(是否支持看实际短信供应商) - /// - public async Task SendMessageAsync(string mobile, string content, string bizId = null, DateTime? sendTime = null) - { - var data = this.GetSingleSmsObj(mobile, content, bizId, sendTime); - var useGZip = this.UseGzip; - var request = this.GetRestRequest(data, SendSingleSmsUrl, useGZip); - var response = await this.Client.ExecuteTaskAsync(request).ConfigureAwait(false); - return this.IsResponseSuccess(response, useGZip); - } - /// - /// 批量发送短信 - /// - /// 短信内容 - /// 手机号码集合 - /// 该值要么为null,要么Count要与mobiles的Count一致,否则会引发异常 - /// 定时发送时间(是否支持看实际短信供应商) - /// - public bool SendMessageBatch(string content, IList mobiles, IList bizIds = null, DateTime? sendTime = null) - { - var data = this.GetBatchSMSObj(content, mobiles, bizIds, sendTime); - var useGZip = this.UseGzip; - var request = this.GetRestRequest(data, SendBatchSmsUrl, useGZip); - var response = this.Client.Execute(request); - return this.IsResponseSuccess(response, useGZip); - } - private object GetBatchSMSObj(string content, IList mobiles, IList bizIds, DateTime? sendTime) - { - if (bizIds != null && bizIds.Count> 0 && mobiles.Count != bizIds.Count) - { - throw new ArgumentException($"{nameof(mobiles)}.Count not equals {nameof(bizIds)}.Count"); - } - if (bizIds != null && bizIds.Count == 0) - { - bizIds = null; - } - var items = new System.Collections.ArrayList(); - for (var i = 0; i < mobiles.Count; i++) - { - items.Add(new - { - mobile = mobiles[i], - customSmsId = bizIds?[i] - }); - } - var obj = new - { - smses = items, - content, - timerTime = sendTime.HasValue ? sendTime.Value.ToString("yyyy-MM-dd HH:mm:ss") : string.Empty, - requestTime = DateTime.Now.Ticks, - requestValidPeriod = this.ValidPeriod - }; - return obj; - } - /// - /// 批量发送短信 - /// - /// 短信内容 - /// 手机号码集合 - /// 该值要么为null,要么Count要与mobiles的Count一致,否则会引发异常 - /// 定时发送时间(是否支持看实际短信供应商) - /// - public async Task SendMessageBatchAsync(string content, IList mobiles, IList bizIds = null, DateTime? sendTime = null) - { - var data = this.GetBatchSMSObj(content, mobiles, bizIds, sendTime); - var useGZip = this.UseGzip; - var request = this.GetRestRequest(data, SendBatchSmsUrl, useGZip); - var response = await this.Client.ExecuteTaskAsync(request).ConfigureAwait(false); - return this.IsResponseSuccess(response, useGZip); - } - } -} +using CheckCodeHelper.Sender.Sms.Utils; +using Newtonsoft.Json; +using RestSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace CheckCodeHelper.Sender.Sms +{ +#if NETSTANDARD2_0_OR_GREATER + using Microsoft.Extensions.Options; +#endif + /// + /// 亿美短信 http://www.emay.cn/ + /// + public class EmaySms : ISms + { + private readonly string _appId; + private readonly byte[] _secretKey; + private const string SendSingleSmsUrl = "/inter/sendSingleSMS"; + private const string SendBatchSmsUrl = "/inter/sendBatchSMS"; + /// + /// 默认Key值 + /// + public const string DefaultKey = "Emay"; + +#if NETSTANDARD2_0_OR_GREATER + /// + /// 亿美短信 http://www.emay.cn/ + /// + /// + public EmaySms(IOptions option) + : this(option.Value) + { + } +#endif + + /// + /// 亿美短信 http://www.emay.cn/ + /// + /// +#if NET45_OR_GREATER + public +#else + private +#endif + EmaySms(EmaySetting setting) + { + if (setting == null || string.IsNullOrWhiteSpace(setting.Host) || string.IsNullOrWhiteSpace(setting.AppId) || string.IsNullOrWhiteSpace(setting.SecretKey)) + { + throw new ArgumentNullException(nameof(setting)); + } + this._appId = setting.AppId; + var key = new byte[16]; + Array.Copy(Encoding.UTF8.GetBytes(setting.SecretKey.PadRight(key.Length)), key, key.Length); + this._secretKey = key; + this.Client = new RestClient(setting.Host); + } + /// + /// IRestClient + /// + public IRestClient Client { get; } + /// + /// 请求有效期(秒),默认60 + /// + public int ValidPeriod { get; set; } = 60; + /// + /// 请求时是否需要Gzip压缩,默认true + /// + public bool UseGzip { get; set; } = true; + /// + /// 用于区分唯一Key值,默认 + /// + public string Key { get; set; } = DefaultKey; + + private IRestRequest GetRestRequest(object data, string url,bool useGZip) + { + var str = JsonConvert.SerializeObject(data); + var request = new RestRequest(url, Method.POST); + request.AddHeader("appId", this._appId); + var rawData = Encoding.UTF8.GetBytes(str); + if (useGZip) + { + request.AddHeader("gzip", "on"); + rawData = GZipHelper.Compress(rawData); + } + var encryptData = AesHelper.Encrypt(rawData, this._secretKey, null, CipherMode.ECB, PaddingMode.PKCS7); + request.AddParameter("", encryptData, ParameterType.RequestBody); + return request; + } + private object GetSingleSmsObj(string mobile, string content, string bizId, DateTime? sendTime) + { + var data = new + { + mobile, + content, + timerTime = sendTime.HasValue ? sendTime.Value.ToString("yyyy-MM-dd HH:mm:ss") : string.Empty, + customSmsId = bizId, + requestTime = DateTime.Now.Ticks, + requestValidPeriod = this.ValidPeriod + }; + return data; + } + private bool IsResponseSuccess(IRestResponse response, bool useGZip) + { + bool isSuccess = response.Headers.FirstOrDefault(p => p.Name == "result")?.Value.ToString() == "SUCCESS"; +#if DEBUG + var responseStr = this.GetResponseContent(response, useGZip); +#endif + return isSuccess; + } + private string GetResponseContent(IRestResponse response, bool useGZip) + { + var data = AesHelper.Decrypt(response.RawBytes, this._secretKey, null, CipherMode.ECB, PaddingMode.PKCS7); + if (useGZip) + { + data = GZipHelper.Decompress(data); + } + return Encoding.UTF8.GetString(data); + } + /// + /// 调用亿美Api + /// + /// + /// + /// + protected bool CallApi(string url, Func dataFunc) + { + var data = dataFunc(); + var useGZip = this.UseGzip; + var request = this.GetRestRequest(data, url, useGZip); + var response = this.Client.Execute(request); + return this.IsResponseSuccess(response, useGZip); + } + /// + /// 调用亿美Api + /// + /// + /// + /// + protected async Task CallApiAsync(string url, Func dataFunc) + { + var data = dataFunc(); + var useGZip = this.UseGzip; + var request = this.GetRestRequest(data, url, useGZip); + var response = await this.Client.ExecuteAsync(request).ConfigureAwait(false); + return this.IsResponseSuccess(response, useGZip); + } + /// + /// 发送单条短信 + /// + /// 手机号 + /// 短信内容 + /// 业务Id + /// 定时发送时间(是否支持看实际短信供应商) + /// + public bool SendMessage(string mobile, string content, string bizId = null, DateTime? sendTime = null) + { + return this.CallApi(SendSingleSmsUrl, () => this.GetSingleSmsObj(mobile, content, bizId, sendTime)); + } + /// + /// 发送单条短信 + /// + /// 手机号 + /// 短信内容 + /// 业务Id + /// 定时发送时间(是否支持看实际短信供应商) + /// + public async Task SendMessageAsync(string mobile, string content, string bizId = null, DateTime? sendTime = null) + { + return await this.CallApiAsync(SendSingleSmsUrl, () => this.GetSingleSmsObj(mobile, content, bizId, sendTime)); + } + /// + /// 批量发送短信 + /// + /// 短信内容 + /// 手机号码集合 + /// 该值要么为null,要么Count要与mobiles的Count一致,否则会引发异常 + /// 定时发送时间(是否支持看实际短信供应商) + /// + public bool SendMessageBatch(string content, IList mobiles, IList bizIds = null, DateTime? sendTime = null) + { + return this.CallApi(SendBatchSmsUrl, () => this.GetBatchSMSObj(content, mobiles, bizIds, sendTime)); + } + private object GetBatchSMSObj(string content, IList mobiles, IList bizIds, DateTime? sendTime) + { + if (bizIds != null && bizIds.Count> 0 && mobiles.Count != bizIds.Count) + { + throw new ArgumentException($"{nameof(mobiles)}.Count not equals {nameof(bizIds)}.Count"); + } + if (bizIds != null && bizIds.Count == 0) + { + bizIds = null; + } + var items = new System.Collections.ArrayList(); + for (var i = 0; i < mobiles.Count; i++) + { + items.Add(new + { + mobile = mobiles[i], + customSmsId = bizIds?[i] + }); + } + var obj = new + { + smses = items, + content, + timerTime = sendTime.HasValue ? sendTime.Value.ToString("yyyy-MM-dd HH:mm:ss") : string.Empty, + requestTime = DateTime.Now.Ticks, + requestValidPeriod = this.ValidPeriod + }; + return obj; + } + /// + /// 批量发送短信 + /// + /// 短信内容 + /// 手机号码集合 + /// 该值要么为null,要么Count要与mobiles的Count一致,否则会引发异常 + /// 定时发送时间(是否支持看实际短信供应商) + /// + public async Task SendMessageBatchAsync(string content, IList mobiles, IList bizIds = null, DateTime? sendTime = null) + { + return await this.CallApiAsync(SendBatchSmsUrl, () => this.GetBatchSMSObj(content, mobiles, bizIds, sendTime)); + } + } + + /// + /// 亿美短信配置 + /// + public class EmaySetting + { + /// + /// 亿美短信服务Host + /// + public string Host { get; set; } + /// + /// 亿美短信应用Id + /// + public string AppId { get; set; } + /// + /// 亿美短信应用秘钥 + /// + public string SecretKey { get; set; } + } +} diff --git a/src/CheckCodeHelper.Sender.Sms/ISms.cs b/src/CheckCodeHelper.Sender.Sms/ISms.cs index 31f69df..3ca20db 100644 --- a/src/CheckCodeHelper.Sender.Sms/ISms.cs +++ b/src/CheckCodeHelper.Sender.Sms/ISms.cs @@ -11,6 +11,10 @@ namespace CheckCodeHelper.Sender.Sms /// public interface ISms { + /// + /// 如果存在多供应商或多账号时,可用于区分唯一Key值 + /// + string Key { get; set; } /// /// 发送单条短信 /// diff --git a/src/CheckCodeHelper.Sender.Sms/SmsExtensions.cs b/src/CheckCodeHelper.Sender.Sms/SmsExtensions.cs new file mode 100644 index 0000000..93bb65e --- /dev/null +++ b/src/CheckCodeHelper.Sender.Sms/SmsExtensions.cs @@ -0,0 +1,45 @@ +#if NETSTANDARD2_0_OR_GREATER +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CheckCodeHelper.Sender.Sms +{ + /// + /// 仅用于Net Core的注册方法 + /// + public static class SmsExtensions + { + /// + /// 注册亿美短信发送相关的服务,注意此方法仅适用于全系统就一种实现的情况 + /// 注意此方法不会注册依赖的 + /// + /// + /// 仅包含的配置节点 + /// + public static IServiceCollection AddSingletonForSmsSenderWithEmay(this IServiceCollection services, IConfiguration configuration) + { + services.AddSingletonForEmay(configuration); + services.AddSingleton(); + return services; + } + + /// + /// 注册亿美发送短信的相关实现,注意此方法不注册 + /// + /// + /// 仅包含的配置节点 + /// + public static IServiceCollection AddSingletonForEmay(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration); + services.AddSingleton(); + return services; + } + } +} +#endif diff --git a/src/CheckCodeHelper.Sender.Sms/SmsSender.cs b/src/CheckCodeHelper.Sender.Sms/SmsSender.cs index 46d4b95..b2b020b 100644 --- a/src/CheckCodeHelper.Sender.Sms/SmsSender.cs +++ b/src/CheckCodeHelper.Sender.Sms/SmsSender.cs @@ -12,6 +12,10 @@ namespace CheckCodeHelper.Sender.Sms /// public class SmsSender : ICodeSender { + /// + /// 默认设置的 + /// + public const string DefaultKey = "SMS"; /// /// 通过短信发送验证码 /// @@ -27,6 +31,10 @@ public SmsSender(IContentFormatter formatter, ISms sms) /// public IContentFormatter Formatter { get; } /// + /// 用于标志当前sender的唯一Key + /// + public string Key { get; set; } = DefaultKey; + /// /// 短信发送接口 /// public ISms Sms { get; } @@ -48,9 +56,9 @@ public virtual bool IsSupport(string receiver) /// 校验码 /// 校验码有效时间范围 /// - public virtual async Task Send(string receiver, string bizFlag, string code, TimeSpan effectiveTime) + public virtual async Task SendAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime) { - var content = this.Formatter.GetContent(receiver, bizFlag, code, effectiveTime); + var content = this.Formatter.GetContent(receiver, bizFlag, code, effectiveTime, this.Key); var ret = await this.Sms.SendMessageAsync(receiver, content).ConfigureAwait(false); return ret; } diff --git a/src/CheckCodeHelper.Sender.Sms/Utils/GzipHelper.cs b/src/CheckCodeHelper.Sender.Sms/Utils/GzipHelper.cs index 7c70e40..bddd39b 100644 --- a/src/CheckCodeHelper.Sender.Sms/Utils/GzipHelper.cs +++ b/src/CheckCodeHelper.Sender.Sms/Utils/GzipHelper.cs @@ -11,7 +11,7 @@ namespace CheckCodeHelper.Sender.Sms.Utils /// /// GZip辅助类 /// - public class GZipHelper + public static class GZipHelper { /// /// Gzip压缩 diff --git a/src/CheckCodeHelper.Sender.Sms/Utils/KeyGenerator.cs b/src/CheckCodeHelper.Sender.Sms/Utils/KeyGenerator.cs index e6be959..7669d36 100644 --- a/src/CheckCodeHelper.Sender.Sms/Utils/KeyGenerator.cs +++ b/src/CheckCodeHelper.Sender.Sms/Utils/KeyGenerator.cs @@ -10,7 +10,7 @@ namespace CheckCodeHelper.Sender.Sms.Utils /// /// 秘钥生成 /// - public class KeyGenerator + public static class KeyGenerator { /// /// 随机生成秘钥(对称算法) diff --git a/src/CheckCodeHelper.Sender.Sms/Utils/SymmetricAlgorithmHelper.cs b/src/CheckCodeHelper.Sender.Sms/Utils/SymmetricAlgorithmHelper.cs index 278294e..9bbe6bd 100644 --- a/src/CheckCodeHelper.Sender.Sms/Utils/SymmetricAlgorithmHelper.cs +++ b/src/CheckCodeHelper.Sender.Sms/Utils/SymmetricAlgorithmHelper.cs @@ -172,7 +172,7 @@ public class TripleDESHelper : SymmetricAlgorithmHelper
/// AES加密辅助类 ///
- public class AESHelper : SymmetricAlgorithmHelper//RijndaelManaged + public class AesHelper : SymmetricAlgorithmHelper//RijndaelManaged { /// /// 生成秘钥 diff --git a/src/CheckCodeHelper.Storage.Memory/CheckCodeHelper.Storage.Memory.csproj b/src/CheckCodeHelper.Storage.Memory/CheckCodeHelper.Storage.Memory.csproj new file mode 100644 index 0000000..2259cf0 --- /dev/null +++ b/src/CheckCodeHelper.Storage.Memory/CheckCodeHelper.Storage.Memory.csproj @@ -0,0 +1,27 @@ + + + + net45;netstandard2.0 + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + dong fang + 通用验证码发送及校验类库--通过内存存储校验信息 + https://github.com/fdstar/CheckCodeHelper + https://mit-license.org/ + false + 1.0.1 + MIT + + + + + + + + + + + + + + + diff --git a/src/CheckCodeHelper.Storage.MemoryCache/MemoryCacheStorage.cs b/src/CheckCodeHelper.Storage.Memory/MemoryCacheStorage.cs similarity index 62% rename from src/CheckCodeHelper.Storage.MemoryCache/MemoryCacheStorage.cs rename to src/CheckCodeHelper.Storage.Memory/MemoryCacheStorage.cs index b94ff91..acbf50d 100644 --- a/src/CheckCodeHelper.Storage.MemoryCache/MemoryCacheStorage.cs +++ b/src/CheckCodeHelper.Storage.Memory/MemoryCacheStorage.cs @@ -1,12 +1,16 @@ -using Microsoft.Extensions.Caching.Memory; +#if NETSTANDARD2_0_OR_GREATER +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +#else +using System.Runtime.Caching; +#endif using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace CheckCodeHelper.Storage.MemoryCache +namespace CheckCodeHelper.Storage.Memory { /// /// 校验码信息存储到MemoryCache @@ -16,44 +20,76 @@ public class MemoryCacheStorage : ICodeStorage /// /// Code缓存Key值前缀 /// - public string CodeKeyPrefix { get; set; } = "CC"; + public string CodeKeyPrefix { get; set; } = "CK"; /// /// Period缓存Key值前缀 /// - public string PeriodKeyPrefix { get; set; } = "CCT"; + public string PeriodKeyPrefix { get; set; } = "PK"; + +#if NETSTANDARD2_0_OR_GREATER /// /// 基于内存的缓存 /// - public IMemoryCache Cache { get; } + public IMemoryCache Cache { get; } + /// /// 缓存优先级,默认 /// - public CacheItemPriority CacheItemPriority { get; set; } = CacheItemPriority.High; + public CacheItemPriority CacheItemPriority { get; set; } = CacheItemPriority.High; + /// /// 基于IMemoryCache的构造函数 /// /// public MemoryCacheStorage(IMemoryCache cache = null) { - this.Cache = cache ?? new Microsoft.Extensions.Caching.Memory.MemoryCache(Options.Create(new MemoryCacheOptions())); + this.Cache = cache ?? new MemoryCache(Options.Create(new MemoryCacheOptions())); + } +#else + /// + /// 基于内存的缓存 + /// + public MemoryCache Cache { get; } + + /// + /// 缓存逐出优先级别,默认 + /// + public CacheItemPriority Priority { get; set; } = CacheItemPriority.NotRemovable; + + /// + /// 基于的构造函数 + /// + /// 用于存储的Cache,如果不传则使用 + public MemoryCacheStorage(MemoryCache cache = null) + { + this.Cache = cache ?? MemoryCache.Default; + } +#endif + /// + /// 释放 + /// + ~MemoryCacheStorage() + { + this.Cache.Dispose(); } + /// /// 获取校验码周期内已发送次数,如果周期已到或未发送过任何验证码,则返回0 /// /// /// /// - public Task GetAreadySendTimes(string receiver, string bizFlag) + public Task GetAreadySendTimesAsync(string receiver, string bizFlag) { var key = this.GetPeriodKey(receiver, bizFlag); int times = 0; - this.Cache.TryGetValue(key, out CodeStorage storage); + var storage = this.GetFromCache(key); if (storage != null) { times = storage.Number; } #if DEBUG - Console.WriteLine("Method:{0} Result:{1}", nameof(GetAreadySendTimes), times); + Console.WriteLine("Method:{0} Result:{1}", nameof(GetAreadySendTimesAsync), times); #endif return Task.FromResult(times); } @@ -63,16 +99,16 @@ public Task GetAreadySendTimes(string receiver, string bizFlag) /// 接收方 /// 业务标志 /// - public Task> GetEffectiveCode(string receiver, string bizFlag) + public Task> GetEffectiveCodeAsync(string receiver, string bizFlag) { Tuple tuple = null; var key = this.GetCodeKey(receiver, bizFlag); - this.Cache.TryGetValue(key, out CodeStorage storage); + var storage = this.GetFromCache(key); if (storage != null) { tuple = Tuple.Create(storage.Code, storage.Number); #if DEBUG - Console.WriteLine("Method:{0} Result: Code {1} Errors {2} ", nameof(GetEffectiveCode), storage.Code, storage.Number); + Console.WriteLine("Method:{0} Result: Code {1} Errors {2} ", nameof(GetEffectiveCodeAsync), storage.Code, storage.Number); #endif } return Task.FromResult(tuple); @@ -83,15 +119,15 @@ public Task> GetEffectiveCode(string receiver, string bizFlag /// 接收方 /// 业务标志 /// - public Task IncreaseCodeErrors(string receiver, string bizFlag) + public Task IncreaseCodeErrorsAsync(string receiver, string bizFlag) { var key = this.GetCodeKey(receiver, bizFlag); - this.Cache.TryGetValue(key, out CodeStorage storage); + var storage = this.GetFromCache(key); if (storage != null) { storage.Number += 1; } - return Task.CompletedTask; + return Task.FromResult(0); } /// /// 校验码周期内发送次数+1,如果周期已到,则不进行任何操作 @@ -99,15 +135,15 @@ public Task IncreaseCodeErrors(string receiver, string bizFlag) /// 接收方 /// 业务标志 /// - public Task IncreaseSendTimes(string receiver, string bizFlag) + public Task IncreaseSendTimesAsync(string receiver, string bizFlag) { var key = this.GetPeriodKey(receiver, bizFlag); - this.Cache.TryGetValue(key, out CodeStorage storage); + var storage = this.GetFromCache(key); if (storage != null) { storage.Number += 1; } - return Task.CompletedTask; + return Task.FromResult(0); } /// /// 将校验码进行持久化,如果接收方和业务标志组合已经存在,则进行覆盖 @@ -117,13 +153,13 @@ public Task IncreaseSendTimes(string receiver, string bizFlag) /// 校验码 /// 校验码有效时间范围 /// - public Task SetCode(string receiver, string bizFlag, string code, TimeSpan effectiveTime) + public Task SetCodeAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime) { var storage = new CodeStorage { Code = code, Number = 0, - StorageTime = DateTime.Now + StorageTime = DateTimeOffset.Now }; var key = this.GetCodeKey(receiver, bizFlag); this.SetCache(key, storage, effectiveTime); @@ -136,7 +172,7 @@ public Task SetCode(string receiver, string bizFlag, string code, TimeSpan /// 业务标志 /// 周期时间范围 /// - public Task SetPeriod(string receiver, string bizFlag, TimeSpan? period) + public Task SetPeriodAsync(string receiver, string bizFlag, TimeSpan? period) { var storage = new CodeStorage { @@ -145,15 +181,50 @@ public Task SetPeriod(string receiver, string bizFlag, TimeSpan? period) var key = this.GetPeriodKey(receiver, bizFlag); this.SetCache(key, storage, period); return Task.FromResult(true); + } + /// + /// 移除周期限制以及错误次数(适用于登录成功后,错误次数限制重新开始计时的场景) + /// + /// 接收方 + /// 业务标志 + /// 执行结果 + public Task RemovePeriodAsync(string receiver, string bizFlag) + { + var periodKey = this.GetPeriodKey(receiver, bizFlag); + var codeKey = this.GetCodeKey(receiver, bizFlag); + this.Cache.Remove(periodKey); + this.Cache.Remove(codeKey); + return Task.FromResult(0); } private void SetCache(string key, CodeStorage storage, TimeSpan? absoluteToNow) - { + { +#if NETSTANDARD2_0_OR_GREATER var option = new MemoryCacheEntryOptions { Priority = this.CacheItemPriority, AbsoluteExpirationRelativeToNow = absoluteToNow }; this.Cache.Set(key, storage, option); +#else + var policy = new CacheItemPolicy + { + Priority = this.Priority, + }; + if (absoluteToNow.HasValue) + { + policy.AbsoluteExpiration = DateTimeOffset.Now.Add(absoluteToNow.Value); + } + this.Cache.Set(key, storage, policy); +#endif + } + private CodeStorage GetFromCache(string key) + { +#if NETSTANDARD2_0_OR_GREATER + this.Cache.TryGetValue(key, out CodeStorage storage); + return storage; +#else + return this.Cache.Get(key) as CodeStorage; +#endif } /// /// 组织IMemoryCache键值 @@ -180,24 +251,24 @@ private string GetCodeKey(string receiver, string bizFlag) /// /// /// - public Task GetLastSetCodeTime(string receiver, string bizFlag) + public Task GetLastSetCodeTimeAsync(string receiver, string bizFlag) { - DateTime? dt = null; + DateTimeOffset? dt = null; var key = this.GetCodeKey(receiver, bizFlag); - this.Cache.TryGetValue(key, out CodeStorage storage); + var storage = this.GetFromCache(key); if (storage != null) { dt = storage.StorageTime; } return Task.FromResult(dt); - } - + } + [Serializable] - private class CodeStorage + private sealed class CodeStorage { public string Code { get; set; } public int Number { get; set; } - public DateTime StorageTime { get; set; } + public DateTimeOffset StorageTime { get; set; } } } } diff --git a/src/CheckCodeHelper.Storage.Memory/MemoryExtensions.cs b/src/CheckCodeHelper.Storage.Memory/MemoryExtensions.cs new file mode 100644 index 0000000..5abbe99 --- /dev/null +++ b/src/CheckCodeHelper.Storage.Memory/MemoryExtensions.cs @@ -0,0 +1,38 @@ +#if NETSTANDARD2_0_OR_GREATER +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CheckCodeHelper.Storage.Memory +{ + /// + /// 仅用于Net Core的注册方法 + /// + public static class MemoryExtensions + { + /// + /// 注册基于内存的校验码信息存储 + /// + /// + /// The to configure the provided + /// + public static IServiceCollection AddSingletonForMemoryCacheStorage(this IServiceCollection services, Action setupAction = null) + { + if (setupAction == null) + { + services.AddMemoryCache(); + } + else + { + services.AddMemoryCache(setupAction); + } + services.AddSingleton(); + return services; + } + } +} +#endif diff --git a/src/CheckCodeHelper.Storage.MemoryCache/CheckCodeHelper.Storage.MemoryCache.csproj b/src/CheckCodeHelper.Storage.MemoryCache/CheckCodeHelper.Storage.MemoryCache.csproj deleted file mode 100644 index 84de4e5..0000000 --- a/src/CheckCodeHelper.Storage.MemoryCache/CheckCodeHelper.Storage.MemoryCache.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - net461;netstandard2.0 - bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml - - - - - - - - - - - diff --git a/src/CheckCodeHelper.Storage.Redis/CheckCodeHelper.Storage.Redis.csproj b/src/CheckCodeHelper.Storage.Redis/CheckCodeHelper.Storage.Redis.csproj index 7bd692b..fec6373 100644 --- a/src/CheckCodeHelper.Storage.Redis/CheckCodeHelper.Storage.Redis.csproj +++ b/src/CheckCodeHelper.Storage.Redis/CheckCodeHelper.Storage.Redis.csproj @@ -1,14 +1,26 @@  - net461;netstandard2.0 + net45;netstandard2.0 bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + dong fang + 通用验证码发送及校验类库--通过Redis存储校验信息 + https://github.com/fdstar/CheckCodeHelper + https://mit-license.org/ + false + 1.0.2 + MIT - - + + + + + + + diff --git a/src/CheckCodeHelper.Storage.Redis/DateTimeOffsetHelper.cs b/src/CheckCodeHelper.Storage.Redis/DateTimeOffsetHelper.cs new file mode 100644 index 0000000..bde7415 --- /dev/null +++ b/src/CheckCodeHelper.Storage.Redis/DateTimeOffsetHelper.cs @@ -0,0 +1,66 @@ +#if NET45 || DEBUG +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CheckCodeHelper.Storage +{ + /// + /// Unix时间戳转换 + /// + public static class DateTimeOffsetHelper + { + private const long TicksPerMillisecond = 10000; + private const long TicksPerSecond = TicksPerMillisecond * 1000; + private const long TicksPerMinute = TicksPerSecond * 60; + private const long TicksPerHour = TicksPerMinute * 60; + private const long TicksPerDay = TicksPerHour * 24; + private const long MinTicks = 0; + private const long MaxTicks = DaysTo10000 * TicksPerDay - 1; + private const int DaysTo10000 = DaysPer400Years * 25 - 366; // 3652059 + // Number of days in a non-leap year + private const int DaysPerYear = 365; + // Number of days in 4 years + private const int DaysPer4Years = DaysPerYear * 4 + 1; // 1461 + // Number of days in 100 years + private const int DaysPer100Years = DaysPer4Years * 25 - 1; // 36524 + // Number of days in 400 years + private const int DaysPer400Years = DaysPer100Years * 4 + 1; // 146097 + private const int DaysTo1970 = DaysPer400Years * 4 + DaysPer100Years * 3 + DaysPer4Years * 17 + DaysPerYear; // 719,162 + private const long UnixEpochTicks = TimeSpan.TicksPerDay * DaysTo1970; // 621,355,968,000,000,000 + //private const long UnixEpochSeconds = UnixEpochTicks / TimeSpan.TicksPerSecond; // 62,135,596,800 + private const long UnixEpochMilliseconds = UnixEpochTicks / TimeSpan.TicksPerMillisecond; // 62,135,596,800,000 + /// + /// 转换为毫秒时间戳 + /// + /// + /// + public static long ToUnixTimeMilliseconds(this DateTimeOffset dateTimeOffset) + { + long milliseconds = dateTimeOffset.UtcDateTime.Ticks / TimeSpan.TicksPerMillisecond; + return milliseconds - UnixEpochMilliseconds; + } + /// + /// 从毫秒进行转换 + /// + /// + /// + public static DateTimeOffset FromUnixTimeMilliseconds(this long milliseconds) + { + const long MinMilliseconds = MinTicks / TimeSpan.TicksPerMillisecond - UnixEpochMilliseconds; + const long MaxMilliseconds = MaxTicks / TimeSpan.TicksPerMillisecond - UnixEpochMilliseconds; + + if (milliseconds < MinMilliseconds || milliseconds> MaxMilliseconds) + { + throw new ArgumentOutOfRangeException("milliseconds", + string.Format("Min:{0} Max:{1}", MinMilliseconds, MaxMilliseconds)); + } + + long ticks = milliseconds * TimeSpan.TicksPerMillisecond + UnixEpochTicks; + return new DateTimeOffset(ticks, TimeSpan.Zero); + } + } +} +#endif diff --git a/src/CheckCodeHelper.Storage.Redis/RedisExtensions.cs b/src/CheckCodeHelper.Storage.Redis/RedisExtensions.cs new file mode 100644 index 0000000..e37356e --- /dev/null +++ b/src/CheckCodeHelper.Storage.Redis/RedisExtensions.cs @@ -0,0 +1,61 @@ +#if NETSTANDARD2_0_OR_GREATER +using Microsoft.Extensions.DependencyInjection; +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CheckCodeHelper.Storage.Redis +{ + /// + /// 仅用于Net Core的注册方法 + /// + public static class RedisExtensions + { + /// + /// 注册基于Redis的校验码信息存储,此方法默认认为已注册过 + /// + /// + /// + public static IServiceCollection AddSingletonForRedisStorage(this IServiceCollection services) + { + return services.AddSingletonForRedisStorage((IConnectionMultiplexer)null); + } + + /// + /// 注册基于Redis的校验码信息存储 + /// + /// + /// 用于存储的Redis,注意如果不为null,会将该对象 + /// + public static IServiceCollection AddSingletonForRedisStorage(this IServiceCollection services, IConnectionMultiplexer connectionMultiplexer) + { + if (connectionMultiplexer != null) + { + services.AddSingleton(connectionMultiplexer); + } + services.AddSingleton(); + return services; + } + + /// + /// 注册基于Redis的校验码信息存储 + /// + /// + /// redis配置字符串,通过该方式会默认注册为Singleton + /// + public static IServiceCollection AddSingletonForRedisStorage(this IServiceCollection services, string redisConfiguration) + { + if (string.IsNullOrWhiteSpace(redisConfiguration)) + { + throw new ArgumentException(nameof(redisConfiguration)); + } + var connectionMultiplexer = ConnectionMultiplexer.Connect(redisConfiguration); + services.AddSingletonForRedisStorage(connectionMultiplexer); + return services; + } + } +} +#endif diff --git a/src/CheckCodeHelper.Storage.Redis/RedisStorage.cs b/src/CheckCodeHelper.Storage.Redis/RedisStorage.cs index a8ca52d..3562005 100644 --- a/src/CheckCodeHelper.Storage.Redis/RedisStorage.cs +++ b/src/CheckCodeHelper.Storage.Redis/RedisStorage.cs @@ -15,14 +15,31 @@ public class RedisStorage : ICodeStorage private const string CodeErrorHashKey = "Error"; private const string CodeTimeHashKey = "Time"; private const string PeriodHashKey = "Number"; + + private const string SetCodeScript = @"redis.call('HMSET', KEYS[1], 'Code', ARGV[1], 'Error', ARGV[2], 'Time', ARGV[3]) + if ARGV[4] ~= '-1' then + return redis.call('PEXPIRE', KEYS[1], ARGV[4]) + end + return 1"; + private const string SetPeriodScript = @"redis.call('HMSET', KEYS[1], 'Number', ARGV[1]) + if ARGV[2] ~= '-1' then + return redis.call('PEXPIRE', KEYS[1], ARGV[2]) + end + return 1"; + private const string HashIncrementScript = @"local hasKey = redis.call('EXISTS', KEYS[1]) + if hasKey ~= '0' then + redis.call('HINCRBY', KEYS[1], KEYS[2], ARGV[1]) + end + return 1"; + /// /// Code缓存Key值前缀 /// - public string CodeKeyPrefix { get; set; } = "CC1"; + public string CodeKeyPrefix { get; set; } = "CK"; /// /// Period缓存Key值前缀 /// - public string PeriodKeyPrefix { get; set; } = "CCT1"; + public string PeriodKeyPrefix { get; set; } = "PK"; /// /// 缓存写入Redis哪个库 /// @@ -41,14 +58,14 @@ public RedisStorage(IConnectionMultiplexer multiplexer) /// /// /// - public async Task GetAreadySendTimes(string receiver, string bizFlag) + public async Task GetAreadySendTimesAsync(string receiver, string bizFlag) { var db = this.GetDatabase(); var key = this.GetPeriodKey(receiver, bizFlag); var value = await db.HashGetAsync(key, PeriodHashKey).ConfigureAwait(false); value.TryParse(out int times); #if DEBUG - Console.WriteLine("Method:{0} Result:{1}", nameof(GetAreadySendTimes), times); + Console.WriteLine("Method:{0} Result:{1}", nameof(GetAreadySendTimesAsync), times); #endif return times; } @@ -58,7 +75,7 @@ public async Task GetAreadySendTimes(string receiver, string bizFlag) /// 接收方 /// 业务标志 /// - public async Task> GetEffectiveCode(string receiver, string bizFlag) + public async Task> GetEffectiveCodeAsync(string receiver, string bizFlag) { var db = this.GetDatabase(); var key = this.GetCodeKey(receiver, bizFlag); @@ -71,7 +88,7 @@ public async Task> GetEffectiveCode(string receiver, string b var code = values[0].ToString(); values[1].TryParse(out int errors); #if DEBUG - Console.WriteLine("Method:{0} Result: Code {1} Errors {2} ", nameof(GetEffectiveCode), code, errors); + Console.WriteLine("Method:{0} Result: Code {1} Errors {2} ", nameof(GetEffectiveCodeAsync), code, errors); #endif return Tuple.Create(code, errors); } @@ -83,14 +100,12 @@ public async Task> GetEffectiveCode(string receiver, string b /// 接收方 /// 业务标志 /// - public async Task IncreaseCodeErrors(string receiver, string bizFlag) + public async Task IncreaseCodeErrorsAsync(string receiver, string bizFlag) { var db = this.GetDatabase(); var key = this.GetCodeKey(receiver, bizFlag); - if (await db.KeyExistsAsync(key).ConfigureAwait(false)) - { - await db.HashIncrementAsync(key, CodeErrorHashKey, 1).ConfigureAwait(false); - } + await db.ScriptEvaluateAsync(HashIncrementScript, new RedisKey[] { key, CodeErrorHashKey }, + new RedisValue[] { 1 }).ConfigureAwait(false); } /// /// 校验码周期内发送次数+1,如果周期已到,则不进行任何操作 @@ -98,14 +113,12 @@ public async Task IncreaseCodeErrors(string receiver, string bizFlag) /// 接收方 /// 业务标志 /// - public async Task IncreaseSendTimes(string receiver, string bizFlag) + public async Task IncreaseSendTimesAsync(string receiver, string bizFlag) { var db = this.GetDatabase(); - var key = this.GetPeriodKey(receiver, bizFlag); - if (await db.KeyExistsAsync(key).ConfigureAwait(false)) - { - await db.HashIncrementAsync(key, PeriodHashKey, 1).ConfigureAwait(false); - } + var key = this.GetPeriodKey(receiver, bizFlag); + await db.ScriptEvaluateAsync(HashIncrementScript, new RedisKey[] { key, PeriodHashKey }, + new RedisValue[] { 1 }).ConfigureAwait(false); } /// /// 将校验码进行持久化,如果接收方和业务标志组合已经存在,则进行覆盖 @@ -115,26 +128,22 @@ public async Task IncreaseSendTimes(string receiver, string bizFlag) /// 校验码 /// 校验码有效时间范围 /// - public async Task SetCode(string receiver, string bizFlag, string code, TimeSpan effectiveTime) + public async Task SetCodeAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime) { var db = this.GetDatabase(); - var key = this.GetCodeKey(receiver, bizFlag); - var ret = await await db.HashSetAsync(key, new HashEntry[] { - new HashEntry(CodeValueHashKey,code), - new HashEntry(CodeErrorHashKey,0), - new HashEntry(CodeTimeHashKey,DateTimeOffset.Now.ToUnixTimeMilliseconds()) - }).ContinueWith(async t => - { - if (t.IsCompleted) - { - return await db.KeyExpireAsync(key, effectiveTime).ConfigureAwait(false); - } - return false; - }).ConfigureAwait(false); + var key = this.GetCodeKey(receiver, bizFlag); +#if NETSTANDARD2_0_OR_GREATER + var timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); +#else + var timestamp = DateTimeOffsetHelper.ToUnixTimeMilliseconds(DateTimeOffset.Now); +#endif + var ms = this.GetMilliseconds(effectiveTime); + var ret = await db.ScriptEvaluateAsync(SetCodeScript, new RedisKey[] { key }, + new RedisValue[] { code, 0, timestamp, ms }).ConfigureAwait(false); #if DEBUG - Console.WriteLine("Method:{0} Result:{1}", nameof(SetCode), ret); + Console.WriteLine("Method:{0} Result:{1}", nameof(SetCodeAsync), ret); #endif - return ret; + return (int)ret == 1; } /// /// 校验码发送次数周期持久化,如果接收方和业务标志组合已经存在,则进行覆盖 @@ -143,20 +152,30 @@ public async Task SetCode(string receiver, string bizFlag, string code, Ti /// 业务标志 /// 周期时间范围 /// - public async Task SetPeriod(string receiver, string bizFlag, TimeSpan? period) + public async Task SetPeriodAsync(string receiver, string bizFlag, TimeSpan? period) { var db = this.GetDatabase(); var key = this.GetPeriodKey(receiver, bizFlag); - await db.HashSetAsync(key, PeriodHashKey, 1).ConfigureAwait(false); - var ret = true; - if (period.HasValue) - { - ret = await db.KeyExpireAsync(key, period.Value); - } + var ms = this.GetMilliseconds(period); + var ret = await db.ScriptEvaluateAsync(SetPeriodScript, new RedisKey[] { key }, + new RedisValue[] { 1 , ms }).ConfigureAwait(false); #if DEBUG - Console.WriteLine("Method:{0} Result:{1}", nameof(SetPeriod), ret); + Console.WriteLine("Method:{0} Result:{1}", nameof(SetPeriodAsync), ret); #endif - return ret; + return (int)ret == 1; + } + /// + /// 移除周期限制以及错误次数(适用于登录成功后,错误次数限制重新开始计时的场景) + /// + /// 接收方 + /// 业务标志 + /// 执行结果 + public async Task RemovePeriodAsync(string receiver, string bizFlag) + { + var db = this.GetDatabase(); + var periodKey = this.GetPeriodKey(receiver, bizFlag); + var codeKey = this.GetCodeKey(receiver, bizFlag); + await db.KeyDeleteAsync(new RedisKey[] { periodKey, codeKey }).ConfigureAwait(false); } /// /// 组织Redis键值 @@ -181,23 +200,36 @@ private IDatabase GetDatabase() { return this._multiplexer.GetDatabase(this.DbNumber); } + private long GetMilliseconds(TimeSpan? ts) + { + var ms = -1L; + if (ts.HasValue) + { + ms = (long)ts.Value.TotalMilliseconds; + } + return ms; + } /// /// 获取最后一次校验码持久化的时间 /// /// /// /// - public async Task GetLastSetCodeTime(string receiver, string bizFlag) + public async Task GetLastSetCodeTimeAsync(string receiver, string bizFlag) { - DateTime? dt = null; + DateTimeOffset? dt = null; var db = this.GetDatabase(); var key = this.GetCodeKey(receiver, bizFlag); var value = await db.HashGetAsync(key, CodeTimeHashKey).ConfigureAwait(false); if (value.HasValue && value.TryParse(out long ts)) - { - dt = DateTimeOffset.FromUnixTimeMilliseconds(ts).ToLocalTime().DateTime; + { +#if NETSTANDARD2_0_OR_GREATER + dt = DateTimeOffset.FromUnixTimeMilliseconds(ts); +#else + dt = DateTimeOffsetHelper.FromUnixTimeMilliseconds(ts); +#endif } return dt; } - } + } } diff --git a/src/CheckCodeHelper.Storage.RedisCache/RedisCacheStorage.cs b/src/CheckCodeHelper.Storage.RedisCache/RedisCacheStorage.cs index 6ecedfa..e18f6ce 100644 --- a/src/CheckCodeHelper.Storage.RedisCache/RedisCacheStorage.cs +++ b/src/CheckCodeHelper.Storage.RedisCache/RedisCacheStorage.cs @@ -1,195 +1,208 @@ -using StackExchange.Redis.Extensions.Core.Abstractions; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CheckCodeHelper.Storage.RedisCache -{ - /// - /// 校验码信息存储到Redis - /// - public class RedisCacheStorage : ICodeStorage - { - private readonly IRedisCacheClient _client; - private const string CodeValueHashKey = "Code"; - private const string CodeErrorHashKey = "Error"; - private const string CodeTimeHashKey = "Time"; - private const string PeriodHashKey = "Number"; - /// - /// Code缓存Key值前缀 - /// - public string CodeKeyPrefix { get; set; } = "CC"; - /// - /// Period缓存Key值前缀 - /// - public string PeriodKeyPrefix { get; set; } = "CCT"; - /// - /// 缓存写入Redis哪个库 - /// - public int DbNumber { get; set; } = 8; - /// - /// 基于RedisCacheClient的构造函数 - /// - /// - public RedisCacheStorage(IRedisCacheClient client) - { - this._client = client; - } - /// - /// 获取校验码周期内已发送次数,如果周期已到或未发送过任何验证码,则返回0 - /// - /// - /// - /// - public async Task GetAreadySendTimes(string receiver, string bizFlag) - { - var db = this.GetDatabase(); - var key = this.GetPeriodKey(receiver, bizFlag); - var times = await db.HashGetAsync(key, PeriodHashKey).ConfigureAwait(false); -#if DEBUG - Console.WriteLine("Method:{0} Result:{1}", nameof(GetAreadySendTimes), times); -#endif - return times; - } - /// - /// 获取校验码及已尝试错误次数,如果校验码不存在或已过期,则返回null - /// - /// 接收方 - /// 业务标志 - /// - public async Task> GetEffectiveCode(string receiver, string bizFlag) - { - var db = this.GetDatabase(); - var key = this.GetCodeKey(receiver, bizFlag); - var dic = await db.HashGetAsync(key, new string[] { - CodeValueHashKey,CodeErrorHashKey - }).ConfigureAwait(false); - if (dic != null && dic.Count == 2 && dic.Values.All(x => !string.IsNullOrWhiteSpace(x))) - { - var code = dic[CodeValueHashKey]; - int.TryParse(dic[CodeErrorHashKey], out int errors); -#if DEBUG - Console.WriteLine("Method:{0} Result: Code {1} Errors {2} ", nameof(GetEffectiveCode), code, errors); -#endif - return Tuple.Create(code, errors); - } - return null; - } - /// - /// 校验码错误次数+1,如果校验码已过期,则不进行任何操作 - /// - /// 接收方 - /// 业务标志 - /// - 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)) - { - await db.HashIncerementByAsync(key, CodeErrorHashKey, 1).ConfigureAwait(false); - } - } - /// - /// 校验码周期内发送次数+1,如果周期已到,则不进行任何操作 - /// - /// 接收方 - /// 业务标志 - /// - 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)) - { - await db.HashIncerementByAsync(key, PeriodHashKey, 1).ConfigureAwait(false); - } - } - /// - /// 将校验码进行持久化,如果接收方和业务标志组合已经存在,则进行覆盖 - /// - /// 接收方 - /// 业务标志 - /// 校验码 - /// 校验码有效时间范围 - /// - public async Task SetCode(string receiver, string bizFlag, string code, TimeSpan effectiveTime) - { - var db = this.GetDatabase(); - var key = this.GetCodeKey(receiver, bizFlag); - await db.HashSetAsync(key, CodeValueHashKey, code).ConfigureAwait(false); - await db.HashSetAsync(key, CodeErrorHashKey, 0).ConfigureAwait(false); - await db.HashSetAsync(key, CodeTimeHashKey, DateTimeOffset.Now.ToUnixTimeMilliseconds()).ConfigureAwait(false); - var ret = await db.UpdateExpiryAsync(key, effectiveTime).ConfigureAwait(false); -#if DEBUG - Console.WriteLine("Method:{0} Result:{1}", nameof(SetCode), ret); -#endif - return ret; - } - /// - /// 校验码发送次数周期持久化,如果接收方和业务标志组合已经存在,则进行覆盖 - /// - /// 接收方 - /// 业务标志 - /// 周期时间范围 - /// - public async Task SetPeriod(string receiver, string bizFlag, TimeSpan? period) - { - var db = this.GetDatabase(); - var key = this.GetPeriodKey(receiver, bizFlag); - await db.HashSetAsync(key, PeriodHashKey, 1).ConfigureAwait(false); - var ret = true; - if (period.HasValue) - { - ret = await db.UpdateExpiryAsync(key, period.Value); - } -#if DEBUG - Console.WriteLine("Method:{0} Result:{1}", nameof(SetPeriod), ret); -#endif - return ret; - } - /// - /// 组织Redis键值 - /// - /// - /// - /// - /// - 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); - } - /// - /// 获取最后一次校验码持久化的时间 - /// - /// - /// - /// - public async Task GetLastSetCodeTime(string receiver, string bizFlag) - { - DateTime? dt = null; - var db = this.GetDatabase(); - var key = this.GetCodeKey(receiver, bizFlag); - var ts = await db.HashGetAsync(key, CodeTimeHashKey).ConfigureAwait(false); - if (ts> 0) - { - dt = DateTimeOffset.FromUnixTimeMilliseconds(ts).ToLocalTime().DateTime; - } - return dt; - } - } -} +using StackExchange.Redis.Extensions.Core.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CheckCodeHelper.Storage.RedisCache +{ + /// + /// 校验码信息存储到Redis + /// + public class RedisCacheStorage : ICodeStorage + { + private readonly IRedisCacheClient _client; + private const string CodeValueHashKey = "Code"; + private const string CodeErrorHashKey = "Error"; + private const string CodeTimeHashKey = "Time"; + private const string PeriodHashKey = "Number"; + /// + /// Code缓存Key值前缀 + /// + public string CodeKeyPrefix { get; set; } = "CKC"; + /// + /// Period缓存Key值前缀 + /// + public string PeriodKeyPrefix { get; set; } = "PKC"; + /// + /// 缓存写入Redis哪个库 + /// + public int DbNumber { get; set; } = 8; + /// + /// 基于RedisCacheClient的构造函数 + /// + /// + public RedisCacheStorage(IRedisCacheClient client) + { + this._client = client; + } + /// + /// 获取校验码周期内已发送次数,如果周期已到或未发送过任何验证码,则返回0 + /// + /// + /// + /// + public async Task GetAreadySendTimesAsync(string receiver, string bizFlag) + { + var db = this.GetDatabase(); + var key = this.GetPeriodKey(receiver, bizFlag); + var times = await db.HashGetAsync(key, PeriodHashKey).ConfigureAwait(false); +#if DEBUG + Console.WriteLine("Method:{0} Result:{1}", nameof(GetAreadySendTimesAsync), times); +#endif + return times; + } + /// + /// 获取校验码及已尝试错误次数,如果校验码不存在或已过期,则返回null + /// + /// 接收方 + /// 业务标志 + /// + public async Task> GetEffectiveCodeAsync(string receiver, string bizFlag) + { + var db = this.GetDatabase(); + var key = this.GetCodeKey(receiver, bizFlag); + var dic = await db.HashGetAsync(key, new string[] { + CodeValueHashKey,CodeErrorHashKey + }).ConfigureAwait(false); + if (dic != null && dic.Count == 2 && dic.Values.All(x => !string.IsNullOrWhiteSpace(x))) + { + var code = dic[CodeValueHashKey]; + int.TryParse(dic[CodeErrorHashKey], out int errors); +#if DEBUG + Console.WriteLine("Method:{0} Result: Code {1} Errors {2} ", nameof(GetEffectiveCodeAsync), code, errors); +#endif + return Tuple.Create(code, errors); + } + return null; + } + /// + /// 校验码错误次数+1,如果校验码已过期,则不进行任何操作 + /// + /// 接收方 + /// 业务标志 + /// + public async Task IncreaseCodeErrorsAsync(string receiver, string bizFlag) + { + var db = this.GetDatabase(); + var key = this.GetCodeKey(receiver, bizFlag); + if (await db.ExistsAsync(key).ConfigureAwait(false)) + { + await db.HashIncerementByAsync(key, CodeErrorHashKey, 1).ConfigureAwait(false); + } + } + /// + /// 校验码周期内发送次数+1,如果周期已到,则不进行任何操作 + /// + /// 接收方 + /// 业务标志 + /// + public async Task IncreaseSendTimesAsync(string receiver, string bizFlag) + { + var db = this.GetDatabase(); + var key = this.GetPeriodKey(receiver, bizFlag); + if (await db.ExistsAsync(key).ConfigureAwait(false)) + { + await db.HashIncerementByAsync(key, PeriodHashKey, 1).ConfigureAwait(false); + } + } + /// + /// 将校验码进行持久化,如果接收方和业务标志组合已经存在,则进行覆盖 + /// + /// 接收方 + /// 业务标志 + /// 校验码 + /// 校验码有效时间范围 + /// + public async Task SetCodeAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime) + { + var db = this.GetDatabase(); + var key = this.GetCodeKey(receiver, bizFlag); + await db.HashSetAsync(key, CodeValueHashKey, code).ConfigureAwait(false); + await db.HashSetAsync(key, CodeErrorHashKey, 0).ConfigureAwait(false); + await db.HashSetAsync(key, CodeTimeHashKey, DateTimeOffset.Now.ToUnixTimeMilliseconds()).ConfigureAwait(false); + var ret = await db.UpdateExpiryAsync(key, effectiveTime).ConfigureAwait(false); +#if DEBUG + Console.WriteLine("Method:{0} Result:{1}", nameof(SetCodeAsync), ret); +#endif + return ret; + } + /// + /// 校验码发送次数周期持久化,如果接收方和业务标志组合已经存在,则进行覆盖 + /// + /// 接收方 + /// 业务标志 + /// 周期时间范围 + /// + public async Task SetPeriodAsync(string receiver, string bizFlag, TimeSpan? period) + { + var db = this.GetDatabase(); + var key = this.GetPeriodKey(receiver, bizFlag); + await db.HashSetAsync(key, PeriodHashKey, 1).ConfigureAwait(false); + var ret = true; + if (period.HasValue) + { + ret = await db.UpdateExpiryAsync(key, period.Value); + } +#if DEBUG + Console.WriteLine("Method:{0} Result:{1}", nameof(SetPeriodAsync), ret); +#endif + return ret; + } + /// + /// 移除周期限制以及错误次数(适用于登录成功后,错误次数限制重新开始计时的场景) + /// + /// 接收方 + /// 业务标志 + /// 执行结果 + public async Task RemovePeriodAsync(string receiver, string bizFlag) + { + var db = this.GetDatabase(); + var periodKey = this.GetPeriodKey(receiver, bizFlag); + var codeKey = this.GetCodeKey(receiver, bizFlag); + await db.RemoveAllAsync(new string[] { periodKey, codeKey }).ConfigureAwait(false); + } + /// + /// 组织Redis键值 + /// + /// + /// + /// + /// + 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); + } + /// + /// 获取最后一次校验码持久化的时间 + /// + /// + /// + /// + public async Task GetLastSetCodeTimeAsync(string receiver, string bizFlag) + { + DateTimeOffset? dt = null; + var db = this.GetDatabase(); + var key = this.GetCodeKey(receiver, bizFlag); + var ts = await db.HashGetAsync(key, CodeTimeHashKey).ConfigureAwait(false); + if (ts> 0) + { + dt = DateTimeOffset.FromUnixTimeMilliseconds(ts); + } + return dt; + } + } +} diff --git a/src/CheckCodeHelper.sln b/src/CheckCodeHelper.sln index 93af092..7e18eb9 100644 --- a/src/CheckCodeHelper.sln +++ b/src/CheckCodeHelper.sln @@ -1,74 +1,81 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.645 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper", "CheckCodeHelper\CheckCodeHelper.csproj", "{F56708BC-6655-4598-8FC4-87AB88D0335B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Storage.RedisCache", "CheckCodeHelper.Storage.RedisCache\CheckCodeHelper.Storage.RedisCache.csproj", "{766850FF-63A7-404A-8C43-3D8AC97C2498}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Samples", "CheckCodeHelper.Samples\CheckCodeHelper.Samples.csproj", "{9410A548-3E81-43CA-8D01-92EC00353038}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Storage.Redis", "CheckCodeHelper.Storage.Redis\CheckCodeHelper.Storage.Redis.csproj", "{228189CD-BDD0-4912-A61F-B62522928FF1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solutions", "Solutions", "{27744E1C-CEE0-41F7-988D-22734CA8787B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{027DDE37-0525-4118-A625-3F484BDA940A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Sender.Sms", "CheckCodeHelper.Sender.Sms\CheckCodeHelper.Sender.Sms.csproj", "{CB2BEB92-DB0A-4D28-99B0-9F3D961E6593}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Sender.EMail", "CheckCodeHelper.Sender.EMail\CheckCodeHelper.Sender.EMail.csproj", "{D33B0951-75DF-4470-B7C0-7F071149C4CA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CheckCodeHelper.Storage.MemoryCache", "CheckCodeHelper.Storage.MemoryCache\CheckCodeHelper.Storage.MemoryCache.csproj", "{7DF905AF-A63E-462E-9B22-BF5503912851}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F56708BC-6655-4598-8FC4-87AB88D0335B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F56708BC-6655-4598-8FC4-87AB88D0335B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F56708BC-6655-4598-8FC4-87AB88D0335B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F56708BC-6655-4598-8FC4-87AB88D0335B}.Release|Any CPU.Build.0 = Release|Any CPU - {766850FF-63A7-404A-8C43-3D8AC97C2498}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {766850FF-63A7-404A-8C43-3D8AC97C2498}.Debug|Any CPU.Build.0 = Debug|Any CPU - {766850FF-63A7-404A-8C43-3D8AC97C2498}.Release|Any CPU.ActiveCfg = Release|Any CPU - {766850FF-63A7-404A-8C43-3D8AC97C2498}.Release|Any CPU.Build.0 = Release|Any CPU - {9410A548-3E81-43CA-8D01-92EC00353038}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9410A548-3E81-43CA-8D01-92EC00353038}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9410A548-3E81-43CA-8D01-92EC00353038}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9410A548-3E81-43CA-8D01-92EC00353038}.Release|Any CPU.Build.0 = Release|Any CPU - {228189CD-BDD0-4912-A61F-B62522928FF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {228189CD-BDD0-4912-A61F-B62522928FF1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {228189CD-BDD0-4912-A61F-B62522928FF1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {228189CD-BDD0-4912-A61F-B62522928FF1}.Release|Any CPU.Build.0 = Release|Any CPU - {CB2BEB92-DB0A-4D28-99B0-9F3D961E6593}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CB2BEB92-DB0A-4D28-99B0-9F3D961E6593}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CB2BEB92-DB0A-4D28-99B0-9F3D961E6593}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CB2BEB92-DB0A-4D28-99B0-9F3D961E6593}.Release|Any CPU.Build.0 = Release|Any CPU - {D33B0951-75DF-4470-B7C0-7F071149C4CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D33B0951-75DF-4470-B7C0-7F071149C4CA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D33B0951-75DF-4470-B7C0-7F071149C4CA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D33B0951-75DF-4470-B7C0-7F071149C4CA}.Release|Any CPU.Build.0 = Release|Any CPU - {7DF905AF-A63E-462E-9B22-BF5503912851}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7DF905AF-A63E-462E-9B22-BF5503912851}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7DF905AF-A63E-462E-9B22-BF5503912851}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7DF905AF-A63E-462E-9B22-BF5503912851}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {F56708BC-6655-4598-8FC4-87AB88D0335B} = {27744E1C-CEE0-41F7-988D-22734CA8787B} - {766850FF-63A7-404A-8C43-3D8AC97C2498} = {27744E1C-CEE0-41F7-988D-22734CA8787B} - {9410A548-3E81-43CA-8D01-92EC00353038} = {027DDE37-0525-4118-A625-3F484BDA940A} - {228189CD-BDD0-4912-A61F-B62522928FF1} = {27744E1C-CEE0-41F7-988D-22734CA8787B} - {CB2BEB92-DB0A-4D28-99B0-9F3D961E6593} = {27744E1C-CEE0-41F7-988D-22734CA8787B} - {D33B0951-75DF-4470-B7C0-7F071149C4CA} = {27744E1C-CEE0-41F7-988D-22734CA8787B} - {7DF905AF-A63E-462E-9B22-BF5503912851} = {27744E1C-CEE0-41F7-988D-22734CA8787B} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {872232F8-C51F-4A37-B03D-E3108B2A519F} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31321.278 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper", "CheckCodeHelper\CheckCodeHelper.csproj", "{F56708BC-6655-4598-8FC4-87AB88D0335B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Storage.RedisCache", "CheckCodeHelper.Storage.RedisCache\CheckCodeHelper.Storage.RedisCache.csproj", "{766850FF-63A7-404A-8C43-3D8AC97C2498}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Samples", "CheckCodeHelper.Samples\CheckCodeHelper.Samples.csproj", "{9410A548-3E81-43CA-8D01-92EC00353038}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Storage.Redis", "CheckCodeHelper.Storage.Redis\CheckCodeHelper.Storage.Redis.csproj", "{228189CD-BDD0-4912-A61F-B62522928FF1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solutions", "Solutions", "{27744E1C-CEE0-41F7-988D-22734CA8787B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{027DDE37-0525-4118-A625-3F484BDA940A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Sender.Sms", "CheckCodeHelper.Sender.Sms\CheckCodeHelper.Sender.Sms.csproj", "{CB2BEB92-DB0A-4D28-99B0-9F3D961E6593}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Sender.EMail", "CheckCodeHelper.Sender.EMail\CheckCodeHelper.Sender.EMail.csproj", "{D33B0951-75DF-4470-B7C0-7F071149C4CA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Storage.Memory", "CheckCodeHelper.Storage.Memory\CheckCodeHelper.Storage.Memory.csproj", "{7DF905AF-A63E-462E-9B22-BF5503912851}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Sender.AlibabaSms", "CheckCodeHelper.Sender.AlibabaSms\CheckCodeHelper.Sender.AlibabaSms.csproj", "{A75EF12C-4D4D-430F-BE97-BC4C29EA171E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F56708BC-6655-4598-8FC4-87AB88D0335B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F56708BC-6655-4598-8FC4-87AB88D0335B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F56708BC-6655-4598-8FC4-87AB88D0335B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F56708BC-6655-4598-8FC4-87AB88D0335B}.Release|Any CPU.Build.0 = Release|Any CPU + {766850FF-63A7-404A-8C43-3D8AC97C2498}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {766850FF-63A7-404A-8C43-3D8AC97C2498}.Debug|Any CPU.Build.0 = Debug|Any CPU + {766850FF-63A7-404A-8C43-3D8AC97C2498}.Release|Any CPU.ActiveCfg = Release|Any CPU + {766850FF-63A7-404A-8C43-3D8AC97C2498}.Release|Any CPU.Build.0 = Release|Any CPU + {9410A548-3E81-43CA-8D01-92EC00353038}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9410A548-3E81-43CA-8D01-92EC00353038}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9410A548-3E81-43CA-8D01-92EC00353038}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9410A548-3E81-43CA-8D01-92EC00353038}.Release|Any CPU.Build.0 = Release|Any CPU + {228189CD-BDD0-4912-A61F-B62522928FF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {228189CD-BDD0-4912-A61F-B62522928FF1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {228189CD-BDD0-4912-A61F-B62522928FF1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {228189CD-BDD0-4912-A61F-B62522928FF1}.Release|Any CPU.Build.0 = Release|Any CPU + {CB2BEB92-DB0A-4D28-99B0-9F3D961E6593}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB2BEB92-DB0A-4D28-99B0-9F3D961E6593}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB2BEB92-DB0A-4D28-99B0-9F3D961E6593}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB2BEB92-DB0A-4D28-99B0-9F3D961E6593}.Release|Any CPU.Build.0 = Release|Any CPU + {D33B0951-75DF-4470-B7C0-7F071149C4CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D33B0951-75DF-4470-B7C0-7F071149C4CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D33B0951-75DF-4470-B7C0-7F071149C4CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D33B0951-75DF-4470-B7C0-7F071149C4CA}.Release|Any CPU.Build.0 = Release|Any CPU + {7DF905AF-A63E-462E-9B22-BF5503912851}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DF905AF-A63E-462E-9B22-BF5503912851}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DF905AF-A63E-462E-9B22-BF5503912851}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DF905AF-A63E-462E-9B22-BF5503912851}.Release|Any CPU.Build.0 = Release|Any CPU + {A75EF12C-4D4D-430F-BE97-BC4C29EA171E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A75EF12C-4D4D-430F-BE97-BC4C29EA171E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A75EF12C-4D4D-430F-BE97-BC4C29EA171E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A75EF12C-4D4D-430F-BE97-BC4C29EA171E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {F56708BC-6655-4598-8FC4-87AB88D0335B} = {27744E1C-CEE0-41F7-988D-22734CA8787B} + {766850FF-63A7-404A-8C43-3D8AC97C2498} = {27744E1C-CEE0-41F7-988D-22734CA8787B} + {9410A548-3E81-43CA-8D01-92EC00353038} = {027DDE37-0525-4118-A625-3F484BDA940A} + {228189CD-BDD0-4912-A61F-B62522928FF1} = {27744E1C-CEE0-41F7-988D-22734CA8787B} + {CB2BEB92-DB0A-4D28-99B0-9F3D961E6593} = {27744E1C-CEE0-41F7-988D-22734CA8787B} + {D33B0951-75DF-4470-B7C0-7F071149C4CA} = {27744E1C-CEE0-41F7-988D-22734CA8787B} + {7DF905AF-A63E-462E-9B22-BF5503912851} = {27744E1C-CEE0-41F7-988D-22734CA8787B} + {A75EF12C-4D4D-430F-BE97-BC4C29EA171E} = {27744E1C-CEE0-41F7-988D-22734CA8787B} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {872232F8-C51F-4A37-B03D-E3108B2A519F} + EndGlobalSection +EndGlobal diff --git a/src/CheckCodeHelper/CheckCodeHelper.csproj b/src/CheckCodeHelper/CheckCodeHelper.csproj index 1c93b47..3973880 100644 --- a/src/CheckCodeHelper/CheckCodeHelper.csproj +++ b/src/CheckCodeHelper/CheckCodeHelper.csproj @@ -1,8 +1,19 @@  - net461;netstandard2.0 + net45;netstandard2.0 bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + dong fang + 通用验证码发送及校验类库 + https://github.com/fdstar/CheckCodeHelper + https://mit-license.org/ + false + 1.0.6 + MIT + + + + diff --git a/src/CheckCodeHelper/CodeExtensions.cs b/src/CheckCodeHelper/CodeExtensions.cs new file mode 100644 index 0000000..3796765 --- /dev/null +++ b/src/CheckCodeHelper/CodeExtensions.cs @@ -0,0 +1,60 @@ +#if NETSTANDARD2_0_OR_GREATER +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CheckCodeHelper +{ + /// + /// 仅用于Net Core的注册方法 + /// + public static class CodeExtensions + { + /// + /// 注册,并注册其依赖的和用于获取 + /// 注意实际支持的需要自行注册 + /// + /// + /// 仅包含的配置节点 + /// + public static IServiceCollection AddSingletonForComplexHelper(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration); + services.AddSingleton();//当前仅注册了formater,还需要内部构造 + services.AddSingleton(p => p.GetService());//用ComplexContentFormatter注册 + services.AddSingleton(p =>//校验码发送者 + { + Func func = key => + { + var senders = p.GetServices(); + var sender = senders.FirstOrDefault(_ => _.Key == key); + if (sender == null) + { + throw new KeyNotFoundException($"There is no sender with key '{key}'"); + } + return sender; + }; + return func; + }); + services.AddSingleton(); + return services; + } + + /// + /// 注册 + /// + /// + /// + public static IServiceCollection AddSingletonForNoneSender(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } + } +} +#endif diff --git a/src/CheckCodeHelper/CodeHelper.cs b/src/CheckCodeHelper/CodeHelper.cs index 3124fae..1f2cecd 100644 --- a/src/CheckCodeHelper/CodeHelper.cs +++ b/src/CheckCodeHelper/CodeHelper.cs @@ -1,119 +1,168 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CheckCodeHelper -{ - /// - /// 业务校验码辅助接口实现 - /// - public class CodeHelper : ICodeHelper - { - /// - /// 基于接口实现,可依赖注入 - /// - /// - /// - public CodeHelper(ICodeSender sender, ICodeStorage storage) - { - this.Sender = sender ?? throw new ArgumentNullException(nameof(sender)); - this.Storage = storage ?? throw new ArgumentNullException(nameof(storage)); - } - /// - /// 校验码实际发送者 - /// - public ICodeSender Sender { get; } - /// - /// 校验码信息存储者 - /// - public ICodeStorage Storage { get; } - /// - /// 发送校验码 - /// - /// 接收方 - /// 业务标志 - /// 校验码 - /// 校验码有效时间范围 - /// 周期内最大允许发送配置,为null则表示无限制 - public async Task 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; - } - /// - /// 验证校验码是否正确 - /// - /// 接收方 - /// 业务标志 - /// 校验码 - /// 最大允许错误次数 - /// - public async Task 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; - } - /// - /// 获取由数字组成的校验码 - /// - /// 校验码长度 - /// - 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'); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CheckCodeHelper +{ + /// + /// 业务校验码辅助接口实现 + /// + public class CodeHelper : ICodeHelper + { + /// + /// 基于接口实现,可依赖注入 + /// + /// + /// + public CodeHelper(ICodeSender sender, ICodeStorage storage) + { + this.Sender = sender ?? throw new ArgumentNullException(nameof(sender)); + this.Storage = storage ?? throw new ArgumentNullException(nameof(storage)); + } + /// + /// 校验码实际发送者 + /// + public ICodeSender Sender { get; } + /// + /// 校验码信息存储者 + /// + public ICodeStorage Storage { get; } + /// + /// 发送校验码 + /// + /// 接收方 + /// 业务标志 + /// 校验码 + /// 校验码有效时间范围 + /// 周期内允许的发送配置,为null则表示无限制 + public async Task SendCodeAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime, PeriodLimit periodLimit) + { + var result = SendResult.NotSupport; + if (await this.IsSupportAsync(receiver).ConfigureAwait(false)) + { + result = SendResult.MaxSendLimit; + bool canSend = periodLimit == null || periodLimit.MaxLimit <= 0; + int sendCount = 0; + if (!canSend) + { + //校验最大次数 + sendCount = await this.Storage.GetAreadySendTimesAsync(receiver, bizFlag).ConfigureAwait(false); + canSend = sendCount < periodLimit.MaxLimit; + } + if (canSend) + { + //校验发送间隔 + result = SendResult.IntervalLimit; + canSend = TimeSpan.Zero == await this.GetSendCDAsync(receiver, bizFlag, periodLimit).ConfigureAwait(false); + } + if (canSend) + { + //校验发送结果 + result = await this.SendCodeAfterCheckedAsync(receiver, bizFlag, code, effectiveTime, periodLimit, sendCount); + } + } + return result; + } + private async Task IsSupportAsync(string receiver) + { + if (this.Sender is ICodeSenderSupportAsync senderAsync) + { + return await senderAsync.IsSupportAsync(receiver).ConfigureAwait(false); + } + return this.Sender.IsSupport(receiver); + } + private async Task SendCodeAfterCheckedAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime, PeriodLimit periodLimit, int sendCount) + { + var result = SendResult.FailInSend; + if (await this.Sender.SendAsync(receiver, bizFlag, code, effectiveTime).ConfigureAwait(false) + && await this.Storage.SetCodeAsync(receiver, bizFlag, code, effectiveTime).ConfigureAwait(false)) + { + result = SendResult.Success; + if (periodLimit != null) + { + if (sendCount == 0) + { + await this.Storage.SetPeriodAsync(receiver, bizFlag, periodLimit.Period).ConfigureAwait(false); + } + else + { + await this.Storage.IncreaseSendTimesAsync(receiver, bizFlag).ConfigureAwait(false); + } + } + } + return result; + } + /// + /// 验证校验码是否正确 + /// + /// 接收方 + /// 业务标志 + /// 校验码 + /// 最大允许错误次数 + /// 当验证通过时,是否重置周期次数限制,默认false + /// 验证结果 + public async Task VerifyCodeAsync(string receiver, string bizFlag, string code, int maxErrorLimit, bool resetWhileRight = false) + { + var result = VerificationResult.Expired; + var vCode = await this.Storage.GetEffectiveCodeAsync(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.Failed; + await this.Storage.IncreaseCodeErrorsAsync(receiver, bizFlag).ConfigureAwait(false); + } + else if(resetWhileRight) + { + await this.Storage.RemovePeriodAsync(receiver, bizFlag).ConfigureAwait(false); + } + } + } + return result; + } + /// + /// 获取校验码发送的CD时间,如果无CD时间,则返回 + /// + /// 接收方 + /// 业务标志 + /// 周期内允许的发送配置,为null则表示无限制 + /// + public async Task GetSendCDAsync(string receiver, string bizFlag, PeriodLimit periodLimit) + { + if (periodLimit != null && periodLimit.Interval> TimeSpan.Zero) + { + var lastSendTime = await this.Storage.GetLastSetCodeTimeAsync(receiver, bizFlag).ConfigureAwait(false); + if (lastSendTime.HasValue) + { + var ts = lastSendTime.Value.Add(periodLimit.Interval.Value) - DateTimeOffset.Now; + if (ts> TimeSpan.Zero) + { + return ts; + } + } + } + return TimeSpan.Zero; + } + /// + /// 获取由数字组成的校验码 + /// + /// 校验码长度 + /// + 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'); + } + } +} diff --git a/src/CheckCodeHelper/ComplexContentFormatter.cs b/src/CheckCodeHelper/ComplexContentFormatter.cs index 1a9d45e..724e063 100644 --- a/src/CheckCodeHelper/ComplexContentFormatter.cs +++ b/src/CheckCodeHelper/ComplexContentFormatter.cs @@ -1,64 +1,72 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CheckCodeHelper -{ - using System.Collections.Concurrent; - /// - /// 基于业务标志的多内容模板实现 - /// - public class ComplexContentFormatter : IComplexContentFormatter - { - private readonly ConcurrentDictionary _dic = new ConcurrentDictionary(); - /// - /// 设置指定业务对应的内容模板 - /// - /// 业务标志 - /// 内容模板 - public void SetFormatter(string bizFlag, IContentFormatter formatter) - { - if (!string.IsNullOrWhiteSpace(bizFlag) && formatter != null) - { - this._dic.AddOrUpdate(bizFlag, formatter, (k, v) => formatter); - } - } - /// - /// 移除指定业务对应的内容模板,如果没有,则返回null - /// - /// 业务标志 - /// - public IContentFormatter RemoveFormatter(string bizFlag) - { - if (!string.IsNullOrWhiteSpace(bizFlag) - && this._dic.TryRemove(bizFlag, out IContentFormatter formatter)) - { - return formatter; - } - return null; - } - /// - /// 将指定参数组织成待发送的文本内容 - /// - /// 接收方 - /// 业务标志 - /// 校验码 - /// 校验码有效时间范围 - /// - 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); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CheckCodeHelper +{ + using System.Collections.Concurrent; + /// + /// 基于业务标志的多内容模板实现 + /// + public class ComplexContentFormatter : IComplexContentFormatter + { + private readonly ConcurrentDictionary _dic = new ConcurrentDictionary(); + /// + /// 设置指定业务对应的内容模板 + /// + /// 业务标志 + /// + /// 内容模板 + public void SetFormatter(string bizFlag, string senderKey, IContentFormatter formatter) + { + if (!string.IsNullOrWhiteSpace(bizFlag) && formatter != null) + { + this._dic.AddOrUpdate(this.GetKey(bizFlag, senderKey), formatter, (k, v) => formatter); + } + } + private string GetKey(string bizFlag, string senderKey) + { + return $"{senderKey}_{bizFlag}"; + } + /// + /// 移除指定业务对应的内容模板,如果没有,则返回null + /// + /// 业务标志 + /// + /// + public IContentFormatter RemoveFormatter(string bizFlag, string senderKey) + { + if (!string.IsNullOrWhiteSpace(bizFlag) + && this._dic.TryRemove(this.GetKey(bizFlag, senderKey), out IContentFormatter formatter)) + { + return formatter; + } + return null; + } + /// + /// 将指定参数组织成待发送的文本内容 + /// + /// 接收方 + /// 业务标志 + /// 校验码 + /// 校验码有效时间范围 + /// + /// + public string GetContent(string receiver, string bizFlag, string code, TimeSpan effectiveTime, string senderKey = null) + { + if (string.IsNullOrWhiteSpace(bizFlag)) + { + throw new ArgumentException(nameof(bizFlag)); + } + var key = this.GetKey(bizFlag, senderKey); + this._dic.TryGetValue(key, out IContentFormatter formatter); + if (formatter == null) + { + throw new KeyNotFoundException($"There is no formatter with key '{key}'"); + } + return formatter.GetContent(receiver, bizFlag, code, effectiveTime, senderKey); + } + } +} diff --git a/src/CheckCodeHelper/ComplexHelper.cs b/src/CheckCodeHelper/ComplexHelper.cs new file mode 100644 index 0000000..84dd9da --- /dev/null +++ b/src/CheckCodeHelper/ComplexHelper.cs @@ -0,0 +1,237 @@ +#if NETSTANDARD2_0_OR_GREATER +using Microsoft.Extensions.Options; +#endif +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CheckCodeHelper +{ + /// + /// 组合 + /// + public class ComplexHelper + { + private readonly Func senderFunc; + + /// + /// 配置信息 + /// + public ComplexSetting ComplexSetting { get; } + /// + /// 基于业务标志的多内容模板 + /// + public IComplexContentFormatter ComplexContentFormatter { get; } + /// + /// 数据存储 + /// + public ICodeStorage CodeStorage { get; } + + /// + /// 构造函数 + /// + /// 配置信息 + /// 模板信息 + /// 要使用的存储方式 + /// 用于根据获取对应的 + public ComplexHelper( +#if NETSTANDARD2_0_OR_GREATER + IOptions setting, +#else + ComplexSetting setting, +#endif + IComplexContentFormatter complexContentFormatter, + ICodeStorage codeStorage, + Func senderFunc) + { +#if NETSTANDARD2_0_OR_GREATER + this.ComplexSetting = setting.Value; +#else + this.ComplexSetting = setting; +#endif + this.ComplexContentFormatter = complexContentFormatter; + this.CodeStorage = codeStorage; + this.senderFunc = senderFunc; + this.InitComplexContentFormatter(complexContentFormatter); + } + /// + /// 初始化模板信息,不为空时默认按参数顺序进行占位,TimeSpan转化为秒进行占位填充 + /// + /// + protected virtual void InitComplexContentFormatter(IComplexContentFormatter complexContentFormatter) + { + var dic = this.ComplexSetting.ContentFormatters; + if (dic == null || dic.Count == 0) + { + return; + } + foreach (var kv in dic) + { + var content = kv.Value; + var tuple = this.GetBizFlagAndSenderKey(kv.Key); + complexContentFormatter.SetFormatter(tuple.Item1, tuple.Item2, new ContentFormatter( + (r, b, c, e, s) => string.Format(content, r, b, c, ContentFormatter.GetNumberDisplayed(e, this.ComplexSetting.EffectiveTimeDisplayed), s) + )); + } + } + /// + /// 获取两者组合后的唯一标志 + /// + /// + /// + /// + protected virtual string GetUniqueKey(string senderKey, string bizFlag) + { + return $"{senderKey}_{bizFlag}"; + } + /// + /// 通过唯一标志获取相应的业务编号和 + /// + /// + /// + protected virtual Tuple GetBizFlagAndSenderKey(string uniqueKey) + { + var idx = uniqueKey.IndexOf('_'); + if (idx <= 1 || idx == uniqueKey.Length - 1) + { + throw new ArgumentException($"The key '{uniqueKey}' formats error"); + } + var bizFlag = uniqueKey.Substring(idx + 1); + var senderKey = uniqueKey.Substring(0, idx); + return Tuple.Create(bizFlag, senderKey); + } + /// + /// 获取周期限制设置,返回null表示为设置 + /// + /// + /// 业务标志 + /// + public PeriodLimit GetPeriodLimit(string senderKey, string bizFlag) + { + PeriodLimit period = null; + var uniqueKey = this.GetUniqueKey(senderKey, bizFlag); + var maxLimit = this.ComplexSetting.PeriodMaxLimits; + if (maxLimit != null && maxLimit.Count> 0 && maxLimit.ContainsKey(uniqueKey)) + { + InitPeriodLimit(); + period.MaxLimit = maxLimit[uniqueKey]; + if (this.ComplexSetting.PeriodLimitSeconds.ContainsKey(uniqueKey)) + { + period.Period = TimeSpan.FromSeconds(this.ComplexSetting.PeriodLimitSeconds[uniqueKey]); + } + } + if (this.ComplexSetting.PeriodLimitIntervalSeconds.ContainsKey(uniqueKey)) + { + InitPeriodLimit(); + period.Interval = TimeSpan.FromSeconds(this.ComplexSetting.PeriodLimitIntervalSeconds[uniqueKey]); + } + void InitPeriodLimit() + { + if (period == null) + { + period = new PeriodLimit(); + } + } + return period; + } + /// + /// 获取验证码的有效时间设置 + /// + /// + /// 业务标志 + /// + public TimeSpan GetCodeEffectiveTime(string senderKey, string bizFlag) + { + var dic = this.ComplexSetting.CodeEffectiveSeconds; + if (dic == null || dic.Count == 0) + { + throw new ArgumentException(nameof(this.ComplexSetting.CodeEffectiveSeconds)); + } + var uniqueKey = this.GetUniqueKey(senderKey, bizFlag); + if (!dic.ContainsKey(uniqueKey)) + { + throw new KeyNotFoundException($"The effective time for code with key '{uniqueKey}' is not found"); + } + return TimeSpan.FromSeconds(dic[uniqueKey]); + } + /// + /// 获取错误次数限制设置 + /// + /// + /// 业务标志 + /// + public int GetCodeErrorLimit(string senderKey, string bizFlag) + { + var dic = this.ComplexSetting.CodeMaxErrorLimits; + if (dic == null || dic.Count == 0) + { + throw new ArgumentException(nameof(this.ComplexSetting.CodeMaxErrorLimits)); + } + var uniqueKey = this.GetUniqueKey(senderKey, bizFlag); + if (!dic.ContainsKey(uniqueKey)) + { + throw new KeyNotFoundException($"The error limits for code with key '{uniqueKey}' is not found"); + } + return dic[uniqueKey]; + } + /// + /// 获取 + /// + /// + /// + public ICodeHelper GetCodeHelper(string senderKey) + { + var sender = this.senderFunc(senderKey); + var codeHelper = new CodeHelper(sender, this.CodeStorage); + return codeHelper; + } + + /// + /// 使用指定的发送校验码 + /// + /// + /// 接收方 + /// 业务标志 + /// 校验码 + /// + public async Task SendCodeAsync(string senderKey, string receiver, string bizFlag, string code) + { + var codeHelper = this.GetCodeHelper(senderKey); + var period = this.GetPeriodLimit(senderKey, bizFlag); + var effctiveTime = this.GetCodeEffectiveTime(senderKey, bizFlag); + return await codeHelper.SendCodeAsync(receiver, bizFlag, code, effctiveTime, period); + } + + /// + /// 使用指定的验证校验码是否正确 + /// + /// + /// 接收方 + /// 业务标志 + /// 校验码 + /// 当验证通过时,是否重置周期次数限制,默认false + /// + public async Task VerifyCodeAsync(string senderKey, string receiver, string bizFlag, string code, bool resetWhileRight = false) + { + var codeHelper = this.GetCodeHelper(senderKey); + var errorLimit = this.GetCodeErrorLimit(senderKey, bizFlag); + return await codeHelper.VerifyCodeAsync(receiver, bizFlag, code, errorLimit, resetWhileRight); + } + + /// + /// 获取校验码发送的CD时间,如果无CD时间,则返回 + /// + /// + /// 接收方 + /// 业务标志 + /// + public async Task GetSendCDAsync(string senderKey, string receiver, string bizFlag) + { + var codeHelper = this.GetCodeHelper(senderKey); + var period = this.GetPeriodLimit(senderKey, bizFlag); + return await codeHelper.GetSendCDAsync(receiver, bizFlag, period); + } + } +} diff --git a/src/CheckCodeHelper/ComplexSetting.cs b/src/CheckCodeHelper/ComplexSetting.cs new file mode 100644 index 0000000..14a36b5 --- /dev/null +++ b/src/CheckCodeHelper/ComplexSetting.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CheckCodeHelper +{ + /// + /// 的配置信息,所有字典类属性Key值的组成方式均为 + 下划线 + 业务标志bizFlag + /// + public class ComplexSetting + { + /// + /// 发送内容中验证码有效时间的显示方式,默认以秒显示 + /// + public EffectiveTimeDisplayedInContent EffectiveTimeDisplayed { get; set; } = EffectiveTimeDisplayedInContent.Seconds; + /// + /// 用于构造文本的内容模板 + /// + public IDictionary ContentFormatters { get; set; } + /// + /// 周期次数限制 + /// + public IDictionary PeriodMaxLimits { get; set; } + /// + /// 周期时长限制(秒) + /// + public IDictionary PeriodLimitSeconds { get; set; } + /// + /// 周期内校验码发送间隔限制(秒) + /// + public IDictionary PeriodLimitIntervalSeconds { get; set; } + /// + /// 验证码有效时间(秒) + /// + public IDictionary CodeEffectiveSeconds { get; set; } + /// + /// 最大校验错误次数 + /// + public IDictionary CodeMaxErrorLimits { get; set; } + } +} diff --git a/src/CheckCodeHelper/ContentFormatter.cs b/src/CheckCodeHelper/ContentFormatter.cs index e407160..9ce7f91 100644 --- a/src/CheckCodeHelper/ContentFormatter.cs +++ b/src/CheckCodeHelper/ContentFormatter.cs @@ -1,36 +1,95 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CheckCodeHelper -{ - /// - /// 通用的内容模板 - /// - public class ContentFormatter : IContentFormatter - { - private readonly Func _func; - /// - /// 通用实现,这样就无需每种业务类型都要实现 - /// - /// 传递的委托,参数顺序与一致 - public ContentFormatter(Func func) - { - this._func = func ?? throw new ArgumentNullException(nameof(func)); - } - /// - /// 将指定参数组织成待发送的文本内容 - /// - /// 接收方 - /// 业务标志 - /// 校验码 - /// 校验码有效时间范围 - /// - public string GetContent(string receiver, string bizFlag, string code, TimeSpan effectiveTime) - { - return this._func.Invoke(receiver, bizFlag, code, effectiveTime); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CheckCodeHelper +{ + /// + /// 通用的内容模板 + /// + public class ContentFormatter : IContentFormatter + { + private readonly Func _func; + /// + /// 通用实现,这样就无需每种业务类型都要实现 + /// + /// 传递的委托,参数顺序与一致 + public ContentFormatter(Func func) + { + this._func = func ?? throw new ArgumentNullException(nameof(func)); + } + /// + /// 将指定参数组织成待发送的文本内容 + /// + /// 接收方 + /// 业务标志 + /// 校验码 + /// 校验码有效时间范围 + /// + /// + public string GetContent(string receiver, string bizFlag, string code, TimeSpan effectiveTime, string senderKey = null) + { + return this._func.Invoke(receiver, bizFlag, code, effectiveTime, senderKey); + } + + /// + /// 获取用于显示的时间数字 + /// + /// + /// + /// + public static double GetNumberDisplayed(TimeSpan effectiveTime, EffectiveTimeDisplayedInContent displayed) + { + switch (displayed) + { + case EffectiveTimeDisplayedInContent.Seconds: + return (int)effectiveTime.TotalSeconds; + case EffectiveTimeDisplayedInContent.Minutes: + return effectiveTime.TotalMinutes; + case EffectiveTimeDisplayedInContent.Hours: + return effectiveTime.TotalHours; + default: + int seconds = (int)effectiveTime.TotalSeconds; + const int secondsPerMinute = 60; + const int secondsPerHour = 360; + if (seconds>= secondsPerHour && seconds % secondsPerHour == 0) + { + return seconds / secondsPerHour; + } + else if (seconds>= secondsPerMinute && seconds % secondsPerMinute == 0) + { + return seconds / secondsPerMinute; + } + return seconds; + } + } + } + + /// + /// 发送内容中验证码有效时间的显示方式 + /// + public enum EffectiveTimeDisplayedInContent + { + /// + /// 自动判断 + /// 当有效时间大于等于60秒,且可被60整除时,以分钟显示 + /// 当有效时间大于等于360秒,且可被360整除时,以小时显示 + /// 其它情况,以整数秒显示 + /// + Auto = 0, + /// + /// 强制以整数秒显示,如果所有验证码有效时间都较短时,可以强制指定该方式 + /// + Seconds = 1, + /// + /// 强制以分钟显示,注意此时会以进行显示 + /// + Minutes = 2, + /// + /// 强制以小时显示,注意此时会以进行显示 + /// + Hours = 3, + } +} diff --git a/src/CheckCodeHelper/ICodeHelper.cs b/src/CheckCodeHelper/ICodeHelper.cs index 2723680..0492856 100644 --- a/src/CheckCodeHelper/ICodeHelper.cs +++ b/src/CheckCodeHelper/ICodeHelper.cs @@ -26,9 +26,9 @@ public interface ICodeHelper /// 业务标志 /// 校验码 /// 校验码有效时间范围 - /// 周期内最大允许发送配置,为null则表示无限制 - /// - Task SendCode(string receiver, string bizFlag, string code, TimeSpan effectiveTime, PeriodLimit maxSendLimit); + /// 周期内最大允许发送配置,为null则表示无限制 + /// 校验码发送结果 + Task SendCodeAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime, PeriodLimit periodLimit); /// /// 验证校验码是否正确 /// @@ -36,7 +36,16 @@ public interface ICodeHelper /// 业务标志 /// 校验码 /// 最大允许错误次数 + /// 当验证通过时,是否重置周期次数限制,默认false + /// 验证结果 + Task VerifyCodeAsync(string receiver, string bizFlag, string code, int maxErrorLimit, bool resetWhileRight = false); + /// + /// 获取校验码发送的CD时间,如果无CD时间,则返回 + /// + /// 接收方 + /// 业务标志 + /// 周期内允许的发送配置,为null则表示无限制 /// - Task VerifyCode(string receiver, string bizFlag, string code, int maxErrorLimit); + Task GetSendCDAsync(string receiver, string bizFlag, PeriodLimit periodLimit); } } diff --git a/src/CheckCodeHelper/ICodeSender.cs b/src/CheckCodeHelper/ICodeSender.cs index cfab09b..25cdc78 100644 --- a/src/CheckCodeHelper/ICodeSender.cs +++ b/src/CheckCodeHelper/ICodeSender.cs @@ -15,6 +15,10 @@ public interface ICodeSender /// 发送校验码内容模板 /// IContentFormatter Formatter { get; } + /// + /// 用于标志当前sender的唯一Key + /// + string Key { get; set; } /// /// 判断接收者是否符合发送条件,例如当前发送者只支持邮箱,而接收方为手机号,则返回结果应当为false /// @@ -28,7 +32,20 @@ public interface ICodeSender /// 业务标志 /// 校验码 /// 校验码有效时间范围 - /// - Task Send(string receiver, string bizFlag, string code, TimeSpan effectiveTime); - } + /// 发送结果 + Task SendAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime); + } + + /// + /// 用于支持某些需要异步判断的场景 + /// + public interface ICodeSenderSupportAsync + { + /// + /// 判断接收者是否符合发送条件 异步版 + /// + /// + /// + Task IsSupportAsync(string receiver); + } } diff --git a/src/CheckCodeHelper/ICodeStorage.cs b/src/CheckCodeHelper/ICodeStorage.cs index 912d628..b8356e4 100644 --- a/src/CheckCodeHelper/ICodeStorage.cs +++ b/src/CheckCodeHelper/ICodeStorage.cs @@ -18,50 +18,57 @@ public interface ICodeStorage /// 业务标志 /// 校验码 /// 校验码有效时间范围 - /// - Task SetCode(string receiver, string bizFlag, string code, TimeSpan effectiveTime); + /// 执行结果 + Task SetCodeAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime); /// /// 校验码错误次数+1,如果校验码已过期,则不进行任何操作 /// /// 接收方 /// 业务标志 - /// - Task IncreaseCodeErrors(string receiver, string bizFlag); + /// 执行结果 + Task IncreaseCodeErrorsAsync(string receiver, string bizFlag); /// /// 校验码发送次数周期持久化,如果接收方和业务标志组合已经存在,则进行覆盖 /// /// 接收方 /// 业务标志 /// 周期时间范围 - /// - Task SetPeriod(string receiver, string bizFlag, TimeSpan? period); + /// 执行结果 + Task SetPeriodAsync(string receiver, string bizFlag, TimeSpan? period); + /// + /// 移除周期限制以及错误次数(适用于登录成功后,错误次数限制重新开始计时的场景) + /// + /// 接收方 + /// 业务标志 + /// 执行结果 + Task RemovePeriodAsync(string receiver, string bizFlag); /// /// 校验码周期内发送次数+1,如果周期已到,则不进行任何操作 /// /// 接收方 /// 业务标志 - /// - Task IncreaseSendTimes(string receiver, string bizFlag); + /// 执行结果 + Task IncreaseSendTimesAsync(string receiver, string bizFlag); /// /// 获取校验码及已尝试错误次数,如果校验码不存在或已过期,则返回null /// /// 接收方 /// 业务标志 - /// - Task> GetEffectiveCode(string receiver, string bizFlag); + /// 获取结果 + Task> GetEffectiveCodeAsync(string receiver, string bizFlag); /// /// 获取校验码周期内已发送次数,如果周期已到或未发送过任何验证码,则返回0 /// - /// - /// - /// - Task GetAreadySendTimes(string receiver, string bizFlag); + /// 接收方 + /// 业务标志 + /// 获取结果 + Task GetAreadySendTimesAsync(string receiver, string bizFlag); /// /// 获取最后一次校验码持久化的时间,如果未能获取到则返回null /// - /// - /// - /// - Task GetLastSetCodeTime(string receiver, string bizFlag); + /// 接收方 + /// 业务标志 + /// 获取结果 + Task GetLastSetCodeTimeAsync(string receiver, string bizFlag); } } diff --git a/src/CheckCodeHelper/IContentFormatter.cs b/src/CheckCodeHelper/IContentFormatter.cs index 6724848..19e9e9f 100644 --- a/src/CheckCodeHelper/IContentFormatter.cs +++ b/src/CheckCodeHelper/IContentFormatter.cs @@ -18,8 +18,9 @@ public interface IContentFormatter /// 业务标志 /// 校验码 /// 校验码有效时间范围 + /// /// - string GetContent(string receiver, string bizFlag, string code, TimeSpan effectiveTime); + string GetContent(string receiver, string bizFlag, string code, TimeSpan effectiveTime, string senderKey = null); } /// /// 基于业务标志的多内容模板 @@ -30,13 +31,15 @@ public interface IComplexContentFormatter : IContentFormatter /// 设置指定业务对应的内容模板 /// /// 业务标志 + /// /// 内容模板 - void SetFormatter(string bizFlag, IContentFormatter formatter); + void SetFormatter(string bizFlag, string senderKey, IContentFormatter formatter); /// /// 移除指定业务对应的内容模板,如果没有,则返回null /// /// 业务标志 + /// /// - IContentFormatter RemoveFormatter(string bizFlag); + IContentFormatter RemoveFormatter(string bizFlag, string senderKey); } } diff --git a/src/CheckCodeHelper/NoneSender.cs b/src/CheckCodeHelper/NoneSender.cs index 1be4690..ff99306 100644 --- a/src/CheckCodeHelper/NoneSender.cs +++ b/src/CheckCodeHelper/NoneSender.cs @@ -10,11 +10,19 @@ namespace CheckCodeHelper /// 检验码发送接口空实现,该实现适用于无需发送校验码的场景,比如通过图片验证码展示校验码 /// public class NoneSender : ICodeSender - { + { + /// + /// 默认设置的 + /// + public const string DefaultKey = "NONE"; /// /// 发送校验码内容模板 因为此实现适用场景无需发送验证码,所以此处会返回 /// - public IContentFormatter Formatter => throw new NotImplementedException(); + public IContentFormatter Formatter => throw new NotImplementedException(); + /// + /// 用于标志当前sender的唯一Key + /// + public string Key { get; set; } = DefaultKey; /// /// 判断接收者是否符合发送条件,当前实现永远返回true /// @@ -29,7 +37,7 @@ public class NoneSender : ICodeSender /// /// /// - public Task Send(string receiver, string bizFlag, string code, TimeSpan effectiveTime) + public Task SendAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime) => Task.FromResult(true); } } diff --git a/src/CheckCodeHelper/PeriodLimit.cs b/src/CheckCodeHelper/PeriodLimit.cs index a12075d..ebece3f 100644 --- a/src/CheckCodeHelper/PeriodLimit.cs +++ b/src/CheckCodeHelper/PeriodLimit.cs @@ -12,12 +12,17 @@ namespace CheckCodeHelper public class PeriodLimit { /// - /// 周期内允许的最大次数 + /// 周期内允许的最大次数,0表示无限制 /// public int MaxLimit { get; set; } /// /// 周期时间,如果不设置,则表示无周期,此时代表总共只允许发送多少次 /// public TimeSpan? Period { get; set; } + /// + /// 验证码发送间隔,如果不设置,表示可以无冷却重新发送验证码,注意该时间最大只能为验证码的有效时间,超出部分无效且容易造成提示错误 + /// 注意间隔不受周期时间影响,例如发送间隔是60秒,周期是12H,在单个周期的最后一秒发送验证码,在下个周期内,还是需要60-1秒之后才可以发送 + /// + public TimeSpan? Interval { get; set; } } } diff --git a/src/CheckCodeHelper/SendResult.cs b/src/CheckCodeHelper/SendResult.cs index 970cc0c..d5c45ad 100644 --- a/src/CheckCodeHelper/SendResult.cs +++ b/src/CheckCodeHelper/SendResult.cs @@ -31,6 +31,11 @@ public enum SendResult /// 无法发送,结果为false /// [Description("无法发送")] - NotSupprot = 13, - } + NotSupport = 13, + /// + /// 发送间隔时间过短 + /// + [Description("发送间隔时间过短")] + IntervalLimit = 14, + } } diff --git a/src/CheckCodeHelper/VerificationResult.cs b/src/CheckCodeHelper/VerificationResult.cs index 3aa6cf8..b741480 100644 --- a/src/CheckCodeHelper/VerificationResult.cs +++ b/src/CheckCodeHelper/VerificationResult.cs @@ -26,7 +26,7 @@ public enum VerificationResult /// 校验码不一致,校验失败 /// [Description("校验失败")] - VerificationFailed = 32, + Failed = 32, /// /// 已经达到了最大错误尝试次数,需重新发送新的校验码 ///

AltStyle によって変換されたページ (->オリジナル) /