├── .github └── workflows │ └── sonarcloud.yml ├── .gitignore ├── LICENSE ├── README.md └── src ├── CheckCodeHelper.Samples ├── CheckCodeHelper.Samples.csproj ├── ComplexHelperTest.cs ├── ConsoleSender.cs ├── Program.cs └── appsettings.json ├── CheckCodeHelper.Sender.AlibabaSms ├── AlibabaSmsExtensions.cs ├── AlibabaSmsParameterSetting.cs ├── AlibabaSmsSender.cs └── CheckCodeHelper.Sender.AlibabaSms.csproj ├── CheckCodeHelper.Sender.EMail ├── AttachmentInfo.cs ├── CheckCodeHelper.Sender.EMail.csproj ├── EMailExtensions.cs ├── EMailHelper.cs ├── EMailMimeMessageSetting.cs ├── EMailSender.cs └── EMailSetting.cs ├── CheckCodeHelper.Sender.Sms ├── CheckCodeHelper.Sender.Sms.csproj ├── EmaySms.cs ├── ISms.cs ├── SmsExtensions.cs ├── SmsSender.cs └── Utils │ ├── GzipHelper.cs │ ├── KeyGenerator.cs │ └── SymmetricAlgorithmHelper.cs ├── CheckCodeHelper.Storage.Memory ├── CheckCodeHelper.Storage.Memory.csproj ├── MemoryCacheStorage.cs └── MemoryExtensions.cs ├── CheckCodeHelper.Storage.Redis ├── CheckCodeHelper.Storage.Redis.csproj ├── DateTimeOffsetHelper.cs ├── RedisExtensions.cs └── RedisStorage.cs ├── CheckCodeHelper.Storage.RedisCache ├── CheckCodeHelper.Storage.RedisCache.csproj └── RedisCacheStorage.cs ├── CheckCodeHelper.sln └── CheckCodeHelper ├── CheckCodeHelper.csproj ├── CodeExtensions.cs ├── CodeHelper.cs ├── ComplexContentFormatter.cs ├── ComplexHelper.cs ├── ComplexSetting.cs ├── ContentFormatter.cs ├── ICodeHelper.cs ├── ICodeSender.cs ├── ICodeStorage.cs ├── IContentFormatter.cs ├── NoneSender.cs ├── PeriodLimit.cs ├── SendResult.cs └── VerificationResult.cs /.github/workflows/sonarcloud.yml: -------------------------------------------------------------------------------- 1 | name: sonarcloud 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: windows-2019 12 | steps: 13 | - name: Set up JDK 11 14 | uses: actions/setup-java@v1 15 | with: 16 | java-version: 1.11 17 | - uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 20 | - name: Setup .NET 21 | uses: actions/setup-dotnet@v1 22 | with: 23 | dotnet-version: 3.1.x 24 | - name: Restore dependencies 25 | run: dotnet restore .\src\CheckCodeHelper.sln 26 | - name: Cache SonarCloud packages 27 | uses: actions/cache@v1 28 | with: 29 | path: ~\sonar\cache 30 | key: ${{ runner.os }}-sonar 31 | restore-keys: ${{ runner.os }}-sonar 32 | - name: Cache SonarCloud scanner 33 | id: cache-sonar-scanner 34 | uses: actions/cache@v1 35 | with: 36 | path: .\.sonar\scanner 37 | key: ${{ runner.os }}-sonar-scanner 38 | restore-keys: ${{ runner.os }}-sonar-scanner 39 | - name: Install SonarCloud scanner 40 | if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' 41 | shell: powershell 42 | run: | 43 | New-Item -Path .\.sonar\scanner -ItemType Directory 44 | dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner 45 | - name: Build and analyze 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 48 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 49 | shell: powershell 50 | run: | 51 | .\.sonar\scanner\dotnet-sonarscanner begin /k:"fdstar_CheckCodeHelper" /o:"fdstar" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" 52 | dotnet build .\src\CheckCodeHelper.sln --no-restore 53 | .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}" 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /src/*/obj 2 | /src/*/bin 3 | /src/.vs 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 fdstar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CheckCodeHelper 2 | 适用于发送验证码及校验验证码的场景(比如找回密码功能) 3 | 支持周期内限制最大发送次数,支持单次发送后最大校验错误次数 4 | 存储方案默认提供了`Redis`和`MemoryCache`实现,你也可以自己实现`ICodeStorage`来支持其它存储方案。 5 | 6 | 7 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://mit-license.org/) 8 | | Lib | Version | Summary | .Net | 9 | |---|---|---|---| 10 | |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`| 11 | |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`| 12 | |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`| 13 | |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`| 14 | |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`| 15 | |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`| 16 | 17 | 18 | ## 如何使用 19 | 你可以在此处查看使用例子 https://github.com/fdstar/CheckCodeHelper/blob/master/src/CheckCodeHelper.Samples 20 | - `Program.PrevDemo()`为`new`显示声明方式实现的Demo 21 | - `ComplexHelperTest`为`.Net Core`依赖注入方式实现的Demo 22 | - `ConsoleSender`为自定义`ICodeSender`及`ICodeSenderSupportAsync`的例子 23 | 24 | ## Release History 25 | **2021-11-30 Release** 26 | |CheckCodeHelper v1.0.6| 27 | |:--| 28 | |鉴于`IComplexContentFormatter`在`ComplexHelper`中作为构造参数传入,其本身可能已包含如何构造要发送的文本内容,故调整`ComplexHelper.ComplexSetting.ContentFormatters`为空未设置时,不再抛出异常| 29 | 30 | **2021-11-22 Release** 31 | |CheckCodeHelper v1.0.5| 32 | |:--| 33 | |`ComplexHelper`公开读取配置的方法以支持某些特殊场景| 34 | |`ComplexContentFormatter`调整唯一标志的组成方式,使其与`ComplexHelper`保持一致以减少字符串碎片| 35 | |**CheckCodeHelper.Storage.Redis v1.0.2**| 36 | |`ICodeStorage.RemovePeriodAsync`同时移除周期限制及错误次数记录| 37 | |**CheckCodeHelper.Storage.Memory v1.0.1**| 38 | |`ICodeStorage.RemovePeriodAsync`同时移除周期限制及错误次数记录| 39 | 40 | **2021-11-03 Release** 41 | |CheckCodeHelper v1.0.4| 42 | |:--| 43 | |增加`ICodeSenderSupportAsync`以支持`ICodeSender.IsSupport`异步场景,如果`ICodeSender`同时实现了`ICodeSenderSupportAsync`,则`CodeHelper`会通过`ICodeSenderSupportAsync.IsSupportAsync`判断`SendResult.NotSupport`| 44 | |修正`ComplexHelper`获取`PeriodLimit`时,如果未配置`ComplexSetting.PeriodMaxLimits`会导致`ComplexSetting.PeriodLimitIntervalSeconds`无效的问题| 45 | 46 | **2021-10-20 Release** 47 | |
CheckCodeHelper.Storage.Redis v1.0.1
| 48 | |:--| 49 | |调整为通过`Lua`脚本合并执行`Redis`的多条更新指令| 50 | 51 | **2021-10-18 Release** 52 | |CheckCodeHelper v1.0.3| 53 | |:--| 54 | |增加`EffectiveTimeDisplayedInContent`以调整验证码有效期在发送内容中的展示方式,`ComplexHelper`已支持该枚举,`ComplexSetting.EffectiveTimeDisplayed`默认设置为`Seconds`,即在所有的发送内容中以秒对应的数字进行展示,设置为`Auto`时如果有效时间为整360秒或以上且可被360整除,则展示为对应的小时数,有效时间为整60秒或以上且可被60整除,则展示为对应的分钟数
55 | `SendResult.NotSupprot`修正拼写错误为`SendResult.NotSupport`;`VerificationResult.VerificationFailed`简化为`Failed`。注意对于`CheckCodeHelper`这是**破坏性调整**,会造成升级后相关判断产生错误| 56 | |**CheckCodeHelper.Sender.EMail v1.0.3**| 57 | |移除不必要的`Subject Func`,同步调整`TextFormat`及`Subject`到`EMailMimeMessageSetting`,注意对于`EMail`部分这是一个**破坏性调整** | 58 | |**CheckCodeHelper.Sender.AlibabaSms v1.0.0**| 59 | |基于阿里模板短信的`ICodeSender`实现| 60 | 61 | **2021-07-20 Release** 62 | |CheckCodeHelper v1.0.2| 63 | |:--| 64 | |增加`PeriodLimit.Interval`以支持限制发送检验码的时间间隔| 65 | |**CheckCodeHelper.Sender.Sms v1.0.1**| 66 | |升级依赖的`RestSharp`版本至`106.12.0`以解决潜在的安全问题| 67 | 68 | **2021-07-17 Release** 69 | |CheckCodeHelper v1.0.1| 70 | |:--| 71 | |增加`ComplexHelper`以统一在单个应用中校验码的发送与验证入口,支持按需调用指定的`ICodeSender`| 72 | |**CheckCodeHelper.Sender.EMail v1.0.2**| 73 | |增加`Subject Func`以支持`ComplexHelper`| 74 | 75 | **2021-07-15 Release** 76 | |CheckCodeHelper v1.0.0| 77 | |:--| 78 | |主体功能| 79 | |**CheckCodeHelper.Sender.Sms v1.0.0**| 80 | |基于非模板短信的`ICodeSender`实现| 81 | |**CheckCodeHelper.Sender.EMail v1.0.1**| 82 | |基于EMail的`ICodeSender`实现| 83 | |**CheckCodeHelper.Storage.Redis v1.0.0**| 84 | |基于`Redis`的`ICodeStorage`实现| 85 | |**CheckCodeHelper.Storage.Memory v1.0.0**| 86 | |基于`MemoryCache`的`ICodeStorage`实现| 87 | 88 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Samples/CheckCodeHelper.Samples.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Always 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Samples/ComplexHelperTest.cs: -------------------------------------------------------------------------------- 1 | using CheckCodeHelper.Sender.AlibabaSms; 2 | using CheckCodeHelper.Sender.EMail; 3 | using CheckCodeHelper.Sender.Sms; 4 | using CheckCodeHelper.Storage.Memory; 5 | using CheckCodeHelper.Storage.Redis; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using System.Text; 12 | 13 | namespace CheckCodeHelper.Samples 14 | { 15 | public static class ComplexHelperTest 16 | { 17 | static IServiceCollection services; 18 | static IConfiguration configuration; 19 | static ComplexHelperTest() 20 | { 21 | var basePath = Path.GetDirectoryName(typeof(ComplexHelperTest).Assembly.Location); 22 | services = new ServiceCollection(); 23 | configuration = new ConfigurationBuilder() 24 | .AddJsonFile(Path.Combine(basePath, "appsettings.json"), false) 25 | .Build(); 26 | 27 | Init(); 28 | } 29 | 30 | public static void Init() 31 | { 32 | services.AddOptions(); 33 | 34 | //注册ICodeSender 35 | services.AddSingletonForNoneSender(); 36 | services.AddSingletonForSmsSenderWithEmay(configuration.GetSection("EmaySetting")); 37 | services.AddSingletonForEMailSender(configuration.GetSection("EMailSetting"), configuration.GetSection("EMailMimeMessageSetting")); 38 | services.AddSingletonForAlibabaSms(configuration.GetSection("AlibabaConfig"), configuration.GetSection("AlibabaSmsParameterSetting")); 39 | 40 | //增加自定义ICodeSender例子,且该Sender未设置周期限制 41 | services.AddSingleton(); 42 | 43 | //注册ICodeStorage 44 | services.AddSingletonForRedisStorage(configuration.GetValue("Redis:Configuration")); 45 | //services.AddSingletonForMemoryCacheStorage(); 46 | 47 | //注册ComplexHelper 48 | services.AddSingletonForComplexHelper(configuration.GetSection("ComplexSetting")); 49 | } 50 | 51 | public static void Start() 52 | { 53 | var serviceProvider = services.BuildServiceProvider(); 54 | 55 | //获取Helper,如果默认的InitComplexContentFormatter不符合业务需求,可继承后重写 56 | //ComplexHelper依赖ICodeSender.Key来获取实际的验证码发送者,如果找不到,会产生异常 57 | var complexHelper = serviceProvider.GetRequiredService(); 58 | 59 | var bizFlag = configuration.GetValue("BizFlag"); 60 | if (bizFlag == "LoginValidError") 61 | { 62 | LoginError(complexHelper); 63 | return; 64 | } 65 | else 66 | { 67 | CodeValidator(complexHelper); 68 | } 69 | } 70 | 71 | private static void CodeValidator(ComplexHelper complexHelper) 72 | { 73 | Console.WriteLine("****** 您当前正在进行验证码校验Demo ******"); 74 | var bizFlag = configuration.GetValue("BizFlag"); 75 | var senderKey = configuration.GetValue("CurrentSenderKey"); 76 | var receiver = configuration.GetValue("Receiver"); 77 | var code = CodeHelper.GetRandomNumber(); //生成随机的验证码 78 | 79 | Action getTimeAction = () => 80 | { 81 | var time = complexHelper.CodeStorage.GetLastSetCodeTimeAsync(receiver, bizFlag).Result; 82 | if (time.HasValue) 83 | { 84 | Console.WriteLine("上次发送时间:{0:yy-MM-dd HH:mm:ss.fff}", time.Value.LocalDateTime); 85 | } 86 | else 87 | { 88 | Console.WriteLine("未能获取到最后一次发送时间"); 89 | } 90 | var ts = complexHelper.GetSendCDAsync(senderKey, receiver, bizFlag).Result; 91 | Console.WriteLine("校验码发送剩余CD时间:{0}秒", ts.TotalSeconds); 92 | }; 93 | 94 | getTimeAction(); 95 | 96 | var sendResult = complexHelper.SendCodeAsync(senderKey, receiver, bizFlag, code).Result; 97 | 98 | Console.WriteLine("发送结果:{0} 请求时间:{1:yy-MM-dd HH:mm:ss}", sendResult, DateTime.Now); 99 | if (sendResult == SendResult.Success || sendResult == SendResult.IntervalLimit) 100 | { 101 | Console.WriteLine("*****************************"); 102 | while (true) 103 | { 104 | Console.WriteLine("请输入校验码:"); 105 | var vCode = Console.ReadLine(); 106 | if (string.IsNullOrWhiteSpace(vCode)) continue; 107 | getTimeAction(); 108 | var vResult = complexHelper.VerifyCodeAsync(senderKey, receiver, bizFlag, vCode).Result; 109 | Console.WriteLine("{2:yy-MM-dd HH:mm:ss }校验码 {0} 校验结果:{1}", vCode, vResult, DateTime.Now); 110 | if (vResult != VerificationResult.Failed) 111 | { 112 | break; 113 | } 114 | } 115 | } 116 | } 117 | 118 | private static void LoginError(ComplexHelper complexHelper) 119 | { 120 | Console.WriteLine("****** 您当前正在进行密码校验错误Demo ******"); 121 | var senderKey = NoneSender.DefaultKey; //密码校验错误无需实际发送内容 122 | var bizFlag = configuration.GetValue("BizFlag"); 123 | var correct = "1"; //密码正确时,得到的code 124 | var error = "0"; //密码错误时,得到的code 125 | var correctPwd = "123456";//这里不管输入的是什么账号,密码假设为同一个密码 126 | Console.WriteLine("所有账户的正确密码为:" + correctPwd); 127 | do 128 | { 129 | var account = ReadLine("** 请输入您的账号 **", "账号不能为空"); 130 | var pwd = ReadLine("** 请输入您的密码 **", "密码不能为空"); 131 | //检验前进行一次发送,因为周期设置成只能发送一次,所以无需关心发送结果 132 | var sendResult = complexHelper.SendCodeAsync(senderKey, account, bizFlag, correct).Result; 133 | Console.WriteLine("周期设置结果:" + sendResult + " 注意周期设置结果不影响校验"); 134 | 135 | var code = GetCode(pwd); 136 | var verifyResult = complexHelper.VerifyCodeAsync(senderKey, account, bizFlag, code, true).Result;//密码校验正确时,重置允许密码连续错误的次数 137 | Console.WriteLine("您账号 {0} 的密码验证结果 {1}", account, verifyResult); 138 | } 139 | while (true); 140 | 141 | string GetCode(string pwd) 142 | { 143 | return pwd == correctPwd ? correct : error; 144 | } 145 | 146 | string ReadLine(string title, string errorNotice) 147 | { 148 | Console.WriteLine(title); 149 | do 150 | { 151 | var input = Console.ReadLine(); 152 | if (string.IsNullOrWhiteSpace(input)) 153 | { 154 | Console.WriteLine(errorNotice); 155 | } 156 | else 157 | { 158 | return input; 159 | } 160 | } 161 | while (true); 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Samples/ConsoleSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace CheckCodeHelper.Samples 7 | { 8 | /// 9 | /// 在控制台输出校验码 10 | /// 11 | public class ConsoleSender : ICodeSender, ICodeSenderSupportAsync 12 | { 13 | public ConsoleSender(IContentFormatter formatter) 14 | { 15 | this.Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); 16 | } 17 | public IContentFormatter Formatter { get; } 18 | 19 | public string Key { get; set; } = "Console"; 20 | 21 | public bool IsSupport(string receiver) => true; 22 | 23 | public Task IsSupportAsync(string receiver) 24 | { 25 | return Task.FromResult(this.IsSupport(receiver)); 26 | } 27 | 28 | public Task SendAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime) 29 | { 30 | var content = this.Formatter.GetContent(receiver, bizFlag, code, effectiveTime, this.Key); 31 | Console.WriteLine("发送内容:{0}", content); 32 | return Task.FromResult(true); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Samples/Program.cs: -------------------------------------------------------------------------------- 1 | using CheckCodeHelper.Sender.EMail; 2 | using CheckCodeHelper.Sender.Sms; 3 | using CheckCodeHelper.Storage.Memory; 4 | using CheckCodeHelper.Storage.Redis; 5 | using CheckCodeHelper.Storage.RedisCache; 6 | using Microsoft.Extensions.Options; 7 | using StackExchange.Redis; 8 | using StackExchange.Redis.Extensions.Core.Abstractions; 9 | using StackExchange.Redis.Extensions.Core.Configuration; 10 | using StackExchange.Redis.Extensions.Core.Implementations; 11 | using StackExchange.Redis.Extensions.Newtonsoft; 12 | using StackExchange.Redis.Extensions.Protobuf; 13 | using System; 14 | using System.Collections.Generic; 15 | using static CheckCodeHelper.Sender.EMail.EMailMimeMessageSetting; 16 | 17 | namespace CheckCodeHelper.Samples 18 | { 19 | class Program 20 | { 21 | static readonly string BizFlag = "ForgetAndResetPassword"; 22 | static readonly string Receiver = "test";//根据不同的sender,输入不同的接收验证码账号 23 | static readonly TimeSpan effectiveTime = TimeSpan.FromMinutes(1); 24 | static void Main(string[] args) 25 | { 26 | ComplexHelperTest.Start(); 27 | 28 | //PrevDemo(); 29 | 30 | Console.ReadLine(); 31 | 32 | } 33 | 34 | private static void PrevDemo() 35 | { 36 | ICodeSender sender = null; 37 | ICodeStorage storage; 38 | 39 | //storage = GetRedisCacheStorage(); //基于StackExchange.Redis.Extensions.Core 40 | //storage = GetRedisStorage(); //仅基于StackExchange.Redis 41 | storage = GetMemoryCacheStorage(); //基于MemoryCache 42 | 43 | //sender = new NoneSender(); //无需发送验证码场景 44 | //sender = GetSmsSender(); //通过短信发送验证码 45 | //sender = GetEMailSender(); //通过邮件发送验证码 46 | 47 | CheckCodeHelperDemo(storage, sender); 48 | } 49 | 50 | private static void CheckCodeHelperDemo(ICodeStorage storage, ICodeSender sender = null) 51 | { 52 | if (sender == null) 53 | { 54 | var senderKey = "CONSOLE"; 55 | sender = new ConsoleSender(GetFormatter(BizFlag, senderKey)) 56 | { 57 | Key = senderKey, 58 | }; 59 | } 60 | 61 | var helper = new CodeHelper(sender, storage); 62 | var code = CodeHelper.GetRandomNumber(); //生成随机的验证码 63 | 64 | Action getTimeAction = () => 65 | { 66 | //ICodeStorage.GetLastSetCodeTime用于获取最后一次发送校验码时间 67 | //用于比如手机验证码发送后,用户刷新页面时,页面上用于按钮倒计时计数的计算 68 | var time = storage.GetLastSetCodeTimeAsync(Receiver, BizFlag).Result; 69 | if (time.HasValue) 70 | { 71 | Console.WriteLine("上次发送时间:{0:yy-MM-dd HH:mm:ss.fff}", time.Value); 72 | } 73 | else 74 | { 75 | Console.WriteLine("未能获取到最后一次发送时间"); 76 | } 77 | }; 78 | getTimeAction(); 79 | 80 | var sendResult = helper.SendCodeAsync(Receiver, BizFlag, code, effectiveTime, new PeriodLimit 81 | { 82 | //设置周期为20分钟,然后在此段时间内最多允许发送验证码5次 83 | MaxLimit = 5, 84 | Period = TimeSpan.FromMinutes(20) 85 | }).Result; 86 | 87 | Console.WriteLine("发送结果:{0} 发送时间:{1:yy-MM-dd HH:mm:ss}", sendResult, DateTime.Now); 88 | if (sendResult == SendResult.Success) 89 | { 90 | Console.WriteLine("*****************************"); 91 | while (true) 92 | { 93 | Console.WriteLine("请输入校验码:"); 94 | var vCode = Console.ReadLine(); 95 | if (string.IsNullOrWhiteSpace(vCode)) continue; 96 | getTimeAction(); 97 | var vResult = helper.VerifyCodeAsync(Receiver, BizFlag, vCode, 3).Result; 98 | Console.WriteLine("{2:yy-MM-dd HH:mm:ss }校验码 {0} 校验结果:{1}", vCode, vResult, DateTime.Now); 99 | if (vResult != VerificationResult.Failed) 100 | { 101 | break; 102 | } 103 | } 104 | } 105 | } 106 | #region ICodeSender 107 | private static ICodeSender GetEMailSender() 108 | { 109 | var emailSetting = new EMailSetting()//设置您的smtp信息 110 | { 111 | Host = "smtp.exmail.qq.com", 112 | Port = 465, 113 | UseSsl = true, 114 | UserName = "测试", 115 | Password = "",//填入发送邮件的邮箱密码 116 | UserAddress = "",//填入发送邮件的邮箱地址 117 | }; 118 | var subjectSetting = new EMailMimeMessageSetting 119 | { 120 | DefaultTextFormat = MimeKit.Text.TextFormat.Plain, 121 | Parameters = new Dictionary 122 | { 123 | { BizFlag , new MimeMessageParameter{ Subject="找回密码验证码测试邮件"}} 124 | } 125 | }; 126 | var helper = new EMailHelper(Options.Create(emailSetting)); 127 | var sender = new EMailSender(GetFormatter(BizFlag, EMailSender.DefaultKey), helper, Options.Create(subjectSetting)); 128 | return sender; 129 | } 130 | private static ICodeSender GetSmsSender() 131 | { 132 | //此处以亿美为例 133 | string host = "http://shmtn.b2m.cn:80"; 134 | string appid = "11";//填入亿美appid 135 | string secretKey = "22"; //填入亿美secretKey 136 | ISms sms = new EmaySms(Options.Create(new EmaySetting 137 | { 138 | Host = host, 139 | AppId = appid, 140 | SecretKey = secretKey 141 | })); 142 | var sender = new SmsSender(GetFormatter(BizFlag, SmsSender.DefaultKey), sms); 143 | return sender; 144 | } 145 | #endregion 146 | #region IContentFormatter 147 | private static IContentFormatter GetFormatter(string bizFlag, string senderKey) 148 | { 149 | var simpleFormatter = new ContentFormatter( 150 | (r, b, c, e, s) => $"{r}您好,您的忘记密码验证码为{c},有效期为{(int)e.TotalSeconds}秒."); 151 | //如果就一个业务场景,也可以直接返回simpleFormatter 152 | var formatter = new ComplexContentFormatter(); 153 | formatter.SetFormatter(bizFlag, senderKey, simpleFormatter); 154 | return formatter; 155 | } 156 | #endregion 157 | #region ICodeStorage 158 | private static ICodeStorage GetRedisCacheStorage() 159 | { 160 | var redisConfig = new RedisConfiguration 161 | { 162 | Hosts = new RedisHost[] { 163 | new RedisHost{ 164 | Host="127.0.0.1", 165 | Port=6379 166 | } 167 | } 168 | }; 169 | var redisManager = new RedisCacheConnectionPoolManager(redisConfig); 170 | var redisClient = new RedisCacheClient(redisManager, 171 | new NewtonsoftSerializer(), redisConfig);//new ProtobufSerializer(); 172 | var storage = new RedisCacheStorage(redisClient); 173 | return storage; 174 | } 175 | private static ICodeStorage GetRedisStorage() 176 | { 177 | var multiplexer = ConnectionMultiplexer.Connect("127.0.0.1:6379"); 178 | var storage = new RedisStorage(multiplexer); 179 | return storage; 180 | } 181 | private static ICodeStorage GetMemoryCacheStorage() 182 | { 183 | return new MemoryCacheStorage(); 184 | } 185 | #endregion 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Samples/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Redis": { //Redis配置 3 | "Configuration": "127.0.0.1:6379" 4 | }, 5 | "EmaySetting": { //亿美短信配置 6 | "Host": "http://shmtn.b2m.cn:80", 7 | "AppId": "111", 8 | "SecretKey": "2222" 9 | }, 10 | "AlibabaConfig": { //阿里短信账号配置 11 | "AccessKeyId": "", 12 | "AccessKeySecret": "", 13 | "Endpoint": "dysmsapi.aliyuncs.com" 14 | }, 15 | "AlibabaSmsParameterSetting": { //阿里短信请求参数配置 16 | "DefaultSignName": "阿里云短信测试专用", 17 | "Parameters": { 18 | "ForgetAndResetPassword": { 19 | "SignName": null, 20 | "TemplateCode": "SMS_207948308" 21 | } 22 | } 23 | }, 24 | "EMailMimeMessageSetting": { //邮件Mime Message配置 25 | "DefaultTextFormat": "Plain", 26 | "Parameters": { 27 | "ForgetAndResetPassword": { 28 | "Subject": "找回密码", 29 | "TextFormat": null 30 | } 31 | } 32 | }, 33 | "EMailSetting": { //邮箱配置 34 | "Host": "smtp.mxhichina.com", 35 | "Port": 465, 36 | "UseSsl": true, 37 | "UserName": "测试邮箱", 38 | "UserAddress": "阿里邮箱地址", 39 | "Password": "邮箱密码" 40 | }, 41 | "ComplexSetting": { //所有字典类属性Key值的组成方式均为 ICodeSender.Key + 下划线 + 业务标志bizFlag 42 | "EffectiveTimeDisplayed": "Auto", //设置为Auto会导致ContentFormatters有效期部分的展示发生变化 43 | "ContentFormatters": { //内容模板 44 | "NONE_ForgetAndResetPassword": "", 45 | 46 | "Console_ForgetAndResetPassword": "{0}的{1}(找回密码)验证码为{2},有效期为{3}秒", 47 | 48 | "EMAIL_ForgetAndResetPassword": "您的找回密码验证码为{2},有效期为{3}分钟。", 49 | 50 | "SMS_ForgetAndResetPassword": "【亿美软通】找回密码验证码为{2},有效期为{3}分钟,请尽快使用。", 51 | 52 | "ALIBABASMS_ForgetAndResetPassword": "{{\"code\":\"{2}\",\"time\":\"{3}\"}}" //阿里短信此处为Json格式,注意非表示占位的花括号需要用两个表示 {{ }} 53 | }, 54 | "PeriodMaxLimits": { //周期次数限制 55 | "NONE_LoginValidError": 1, //登录错误校验比较特殊,需要设置为1,即一个周期内只允许设置一次值 56 | 57 | "NONE_ForgetAndResetPassword": 10, 58 | 59 | "EMAIL_ForgetAndResetPassword": 5, 60 | 61 | "SMS_ForgetAndResetPassword": 5, 62 | 63 | "ALIBABASMS_ForgetAndResetPassword": 5 64 | }, 65 | "PeriodLimitSeconds": { //周期时间限制(秒) 66 | "NONE_LoginValidError": 3600, //登录错误校验的周期限制,一般为24小时,Demo设置为1小时 67 | 68 | "NONE_ForgetAndResetPassword": 120, 69 | 70 | "EMAIL_ForgetAndResetPassword": 3600, 71 | 72 | "SMS_ForgetAndResetPassword": 3600, 73 | 74 | "ALIBABASMS_ForgetAndResetPassword": 3600 75 | }, 76 | "PeriodLimitIntervalSeconds": { //周期内校验码发送间隔限制(秒) 77 | //"NONE_LoginValidError": 30, //因为登录错误一个周期只允许发一次,所以这里可以不用设置 78 | 79 | "NONE_ForgetAndResetPassword": 40, //对应验证码有效时间配置为30,超出部分无效且会造成CD读取到的时间在最后10秒突然没了 80 | 81 | "SMS_ForgetAndResetPassword": 60, 82 | 83 | "ALIBABASMS_ForgetAndResetPassword": 60 84 | }, 85 | "CodeEffectiveSeconds": { //验证码有效时间(秒) 86 | "NONE_LoginValidError": 3600, //登录错误校验的验证码有效期需与周期时间限制一致 87 | 88 | "NONE_ForgetAndResetPassword": 30, 89 | 90 | "Console_ForgetAndResetPassword": 40, 91 | 92 | "EMAIL_ForgetAndResetPassword": 600, 93 | 94 | "SMS_ForgetAndResetPassword": 120, 95 | 96 | "ALIBABASMS_ForgetAndResetPassword": 120 97 | }, 98 | "CodeMaxErrorLimits": { //校验错误次数 99 | "NONE_LoginValidError": 5, //设置一个周期内,密码错误允许连续错误几次 100 | 101 | "NONE_ForgetAndResetPassword": 3, 102 | 103 | "Console_ForgetAndResetPassword": 5, 104 | 105 | "EMAIL_ForgetAndResetPassword": 5, 106 | 107 | "SMS_ForgetAndResetPassword": 5, 108 | 109 | "ALIBABASMS_ForgetAndResetPassword": 5 110 | } 111 | }, 112 | //以下为业务测试数据,实际不应该走配置 113 | "CurrentSenderKey": "Console", 114 | "BizFlag": "ForgetAndResetPassword", //LoginValidError 115 | "Receiver": "test" 116 | } 117 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.AlibabaSms/AlibabaSmsExtensions.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD2_0_OR_GREATER 2 | using AlibabaCloud.OpenApiClient.Models; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace CheckCodeHelper.Sender.AlibabaSms 12 | { 13 | /// 14 | /// 仅用于Net Core的注册方法 15 | /// 16 | public static class AlibabaSmsExtensions 17 | { 18 | /// 19 | /// 注册阿里短信发送相关的服务,注意此方法仅适用于全系统就一种的情况 20 | /// 注意此方法不会注册依赖的 21 | /// 22 | /// 23 | /// 仅包含的配置节点 24 | /// 仅包含的配置节点 25 | /// 26 | public static IServiceCollection AddSingletonForAlibabaSms(this IServiceCollection services, IConfiguration config, IConfiguration setting) 27 | { 28 | services.Configure(config); 29 | services.Configure(setting); 30 | services.AddSingleton(); 31 | return services; 32 | } 33 | } 34 | } 35 | #endif 36 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.AlibabaSms/AlibabaSmsParameterSetting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace CheckCodeHelper.Sender.AlibabaSms 8 | { 9 | /// 10 | /// 阿里短信请求参数配置 11 | /// 12 | public class AlibabaSmsParameterSetting 13 | { 14 | /// 15 | /// 默认的短信签名,如果未设置则以此为短信签名 16 | /// 17 | public string DefaultSignName { get; set; } 18 | 19 | /// 20 | /// 参数字典 Key为bizFlag 21 | /// 22 | public IDictionary Parameters { get; set; } 23 | /// 24 | /// 阿里短信请求参数 https://next.api.aliyun.com/document/Dysmsapi/2017-05-25/SendSms 25 | /// 26 | public class AlibabaSmsRequestParameter 27 | { 28 | /// 29 | /// 短信签名名称 30 | /// 31 | public string SignName { get; set; } 32 | /// 33 | /// 短信模板ID 34 | /// 35 | public string TemplateCode { get; set; } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.AlibabaSms/AlibabaSmsSender.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD2_0_OR_GREATER 2 | using Microsoft.Extensions.Options; 3 | #endif 4 | using AlibabaCloud.OpenApiClient.Models; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Text.RegularExpressions; 10 | using System.Threading.Tasks; 11 | using static CheckCodeHelper.Sender.AlibabaSms.AlibabaSmsParameterSetting; 12 | using AlibabaCloud.SDK.Dysmsapi20170525; 13 | using AlibabaCloud.SDK.Dysmsapi20170525.Models; 14 | 15 | namespace CheckCodeHelper.Sender.AlibabaSms 16 | { 17 | /// 18 | /// 通过阿里短信发送校验码 19 | /// 20 | public class AlibabaSmsSender : ICodeSender 21 | { 22 | /// 23 | /// 默认设置的 24 | /// 25 | public const string DefaultKey = "ALIBABASMS"; 26 | private readonly AlibabaSmsParameterSetting setting; 27 | private readonly Config config; 28 | 29 | /// 30 | /// 通过阿里短信发送校验码 31 | /// 32 | /// 33 | /// 34 | /// 35 | public AlibabaSmsSender( 36 | IContentFormatter formatter, 37 | #if NETSTANDARD2_0_OR_GREATER 38 | IOptions setting, 39 | IOptions config 40 | #else 41 | AlibabaSmsParameterSetting setting, 42 | Config config 43 | #endif 44 | ) 45 | { 46 | this.Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); 47 | #if NETSTANDARD2_0_OR_GREATER 48 | this.setting = setting.Value; 49 | this.config = config.Value; 50 | #else 51 | this.setting = setting ?? throw new ArgumentNullException(nameof(setting)); 52 | this.config = config; 53 | #endif 54 | if (this.setting.Parameters == null || this.setting.Parameters.Count == 0) 55 | { 56 | throw new ArgumentException(nameof(this.setting.Parameters)); 57 | } 58 | } 59 | 60 | /// 61 | /// 发送验证码Json模板 62 | /// 63 | public IContentFormatter Formatter { get; } 64 | 65 | /// 66 | /// 用于标志当前sender的唯一Key 67 | /// 68 | public string Key { get; set; } = DefaultKey; 69 | 70 | /// 71 | /// 判断接收者是否符合发送条件,目前是宽松判断,即正则 ^1\d{10}$ 72 | /// 73 | /// 74 | /// 75 | public virtual bool IsSupport(string receiver) 76 | { 77 | return !string.IsNullOrWhiteSpace(receiver) 78 | && Regex.IsMatch(receiver, @"^1\d{10}$"); 79 | } 80 | 81 | /// 82 | /// 发送校验码信息 83 | /// 84 | /// 接收方 85 | /// 业务标志 86 | /// 校验码 87 | /// 校验码有效时间范围 88 | /// 89 | public async Task SendAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime) 90 | { 91 | var param = this.GetRequestParameter(bizFlag); 92 | var request = new SendSmsRequest 93 | { 94 | PhoneNumbers = receiver, 95 | SignName = this.GetSignName(param, bizFlag), 96 | TemplateCode = param.TemplateCode, 97 | TemplateParam = this.Formatter.GetContent(receiver, bizFlag, code, effectiveTime, this.Key), 98 | }; 99 | var client = this.CreateClient(); 100 | var response = await client.SendSmsAsync(request).ConfigureAwait(false); 101 | return string.Equals(response.Body.Code, "OK", StringComparison.OrdinalIgnoreCase); 102 | } 103 | 104 | private Client CreateClient() 105 | { 106 | return new Client(this.config); 107 | } 108 | 109 | private AlibabaSmsRequestParameter GetRequestParameter(string bizFlag) 110 | { 111 | if (!this.setting.Parameters.ContainsKey(bizFlag)) 112 | { 113 | throw new KeyNotFoundException($"The request parameter for '{bizFlag}' is not found"); 114 | } 115 | return this.setting.Parameters[bizFlag]; 116 | } 117 | 118 | private string GetSignName(AlibabaSmsRequestParameter parameter, string bizFlag) 119 | { 120 | var signName = parameter.SignName; 121 | if (string.IsNullOrWhiteSpace(signName)) 122 | { 123 | signName = this.setting.DefaultSignName; 124 | if (string.IsNullOrWhiteSpace(signName)) 125 | { 126 | throw new ArgumentException($"The sign name for '{bizFlag}' is not correct"); 127 | } 128 | } 129 | return signName; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.AlibabaSms/CheckCodeHelper.Sender.AlibabaSms.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net45;netstandard2.0 5 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml 6 | dong fang 7 | 通用验证码发送及校验类库--通过阿里短信发送校验信息 8 | https://github.com/fdstar/CheckCodeHelper 9 | https://mit-license.org/ 10 | false 11 | 1.0.0 12 | MIT 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.EMail/AttachmentInfo.cs: -------------------------------------------------------------------------------- 1 | using MimeKit; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace CheckCodeHelper.Sender.EMail 10 | { 11 | /// 12 | /// 邮件附件信息 13 | /// 14 | public sealed class AttachmentInfo : IDisposable 15 | { 16 | /// 17 | /// 释放 18 | /// 19 | ~AttachmentInfo() 20 | { 21 | this.Dispose(); 22 | } 23 | 24 | /// 25 | /// 附件类型 26 | /// 27 | public string ContentType { get; set; } 28 | /// 29 | /// 文件名称 30 | /// 31 | public string FileName { get; set; } 32 | /// 33 | /// 文件传输编码方式 34 | /// 35 | public ContentEncoding ContentTransferEncoding { get; set; } = ContentEncoding.Default; 36 | /// 37 | /// 文件数组 38 | /// 39 | public byte[] Data { get; set; } 40 | private Stream _stream; 41 | /// 42 | /// 文件数据流 43 | /// 44 | public Stream Stream 45 | { 46 | get 47 | { 48 | if (this._stream == null && this.Data != null) 49 | { 50 | _stream = new MemoryStream(this.Data); 51 | } 52 | return this._stream; 53 | } 54 | set { this._stream = value; } 55 | } 56 | /// 57 | /// 释放Stream 58 | /// 59 | public void Dispose() 60 | { 61 | if (this._stream != null) 62 | { 63 | this._stream.Dispose(); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.EMail/CheckCodeHelper.Sender.EMail.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net45;netstandard2.0 5 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml 6 | dong fang 7 | 通用验证码发送及校验类库--通过邮件发送校验信息 8 | https://github.com/fdstar/CheckCodeHelper 9 | https://mit-license.org/ 10 | false 11 | 1.0.3 12 | MIT 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.EMail/EMailExtensions.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD2_0_OR_GREATER 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Options; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace CheckCodeHelper.Sender.EMail 12 | { 13 | /// 14 | /// 仅用于Net Core的注册方法 15 | /// 16 | public static class EMailExtensions 17 | { 18 | /// 19 | /// 注册邮件发送相关的服务,注意此方法仅适用于全系统就一种的情况 20 | /// 注意此方法不会注册依赖的 21 | /// 22 | /// 23 | /// 仅包含的配置节点 24 | /// 仅包含的配置节点 25 | /// 26 | public static IServiceCollection AddSingletonForEMailSender(this IServiceCollection services, IConfiguration emailSetting, IConfiguration emailMimeMessageSetting) 27 | { 28 | services.Configure(emailMimeMessageSetting); 29 | services.AddSingletonForEMailHelper(emailSetting); 30 | services.AddSingleton(); 31 | return services; 32 | } 33 | 34 | /// 35 | /// 注册邮件发送辅助类的相关实现,注意此方法不注册 36 | /// 37 | /// 38 | /// 仅包含的配置节点 39 | /// 40 | public static IServiceCollection AddSingletonForEMailHelper(this IServiceCollection services, IConfiguration configuration) 41 | { 42 | services.Configure(configuration); 43 | services.AddSingleton(); 44 | return services; 45 | } 46 | } 47 | } 48 | #endif 49 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.EMail/EMailHelper.cs: -------------------------------------------------------------------------------- 1 | using MailKit.Net.Smtp; 2 | using MimeKit; 3 | using MimeKit.Text; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace CheckCodeHelper.Sender.EMail 11 | { 12 | #if NETSTANDARD2_0_OR_GREATER 13 | using Microsoft.Extensions.Options; 14 | #endif 15 | /// 16 | /// 发送邮件辅助类 17 | /// 18 | public class EMailHelper 19 | { 20 | /// 21 | /// 邮箱配置 22 | /// 23 | public EMailSetting Setting { get; } 24 | 25 | #if NETSTANDARD2_0_OR_GREATER 26 | /// 27 | /// 邮件发送设置 28 | /// 29 | /// 30 | public EMailHelper(IOptions option) 31 | : this(option.Value) 32 | { 33 | } 34 | #endif 35 | 36 | /// 37 | /// 邮件发送设置 38 | /// 39 | /// 40 | #if NETSTANDARD2_0_OR_GREATER 41 | private 42 | #else 43 | public 44 | #endif 45 | EMailHelper(EMailSetting setting) 46 | { 47 | this.Setting = setting ?? throw new ArgumentNullException(nameof(setting)); 48 | } 49 | /// 50 | /// 发送电子邮件,默认发送方为 51 | /// 52 | /// 邮件主题 53 | /// 邮件内容主题 54 | /// 接收方信息 55 | /// 内容主题模式,默认TextFormat.Text 56 | /// 附件 57 | /// 抄送方信息 58 | /// 密送方信息 59 | /// 是否自动释放附件所用Stream 60 | /// 61 | public async Task SendEMailAsync(string subject, string content, IEnumerable toAddress, TextFormat textFormat = TextFormat.Text, IEnumerable attachments = null, IEnumerable ccAddress = null, IEnumerable bccAddress = null, bool dispose = true) 62 | { 63 | await SendEMailAsync(subject, content, new MailboxAddress[] { new MailboxAddress(this.Setting.UserName, this.Setting.UserAddress) }, toAddress, textFormat, attachments, ccAddress, bccAddress, dispose).ConfigureAwait(false); 64 | } 65 | 66 | /// 67 | /// 发送电子邮件 68 | /// 69 | /// 邮件主题 70 | /// 邮件内容主题 71 | /// 发送方信息 72 | /// 接收方信息 73 | /// 内容主题模式,默认TextFormat.Text 74 | /// 附件 75 | /// 抄送方信息 76 | /// 密送方信息 77 | /// 是否自动释放附件所用Stream 78 | /// 79 | public async Task SendEMailAsync(string subject, string content, MailboxAddress fromAddress, IEnumerable toAddress, TextFormat textFormat = TextFormat.Text, IEnumerable attachments = null, IEnumerable ccAddress = null, IEnumerable bccAddress = null, bool dispose = true) 80 | { 81 | await SendEMailAsync(subject, content, new MailboxAddress[] { fromAddress }, toAddress, textFormat, attachments, ccAddress, bccAddress, dispose).ConfigureAwait(false); 82 | } 83 | 84 | /// 85 | /// 发送电子邮件 86 | /// 87 | /// 邮件主题 88 | /// 邮件内容主题 89 | /// 发送方信息 90 | /// 接收方信息 91 | /// 内容主题模式,默认TextFormat.Text 92 | /// 附件 93 | /// 抄送方信息 94 | /// 密送方信息 95 | /// 是否自动释放附件所用Stream 96 | /// 97 | 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) 98 | { 99 | var message = new MimeMessage(); 100 | message.From.AddRange(fromAddress); 101 | message.To.AddRange(toAddress); 102 | if (!this.IsEmpty(ccAddress)) 103 | { 104 | message.Cc.AddRange(ccAddress); 105 | } 106 | if (!this.IsEmpty(bccAddress)) 107 | { 108 | message.Bcc.AddRange(bccAddress); 109 | } 110 | message.Subject = subject; 111 | var body = new TextPart(textFormat) 112 | { 113 | Text = content 114 | }; 115 | message.Body = this.GetMimeEntity(body, attachments); 116 | message.Date = DateTime.Now; 117 | using (var client = new SmtpClient()) 118 | { 119 | //创建连接 120 | await this.SmtpClientSetting(client).ConfigureAwait(false); 121 | await client.SendAsync(message).ConfigureAwait(false); 122 | await client.DisconnectAsync(true).ConfigureAwait(false); 123 | if (dispose && attachments != null) 124 | { 125 | foreach (var att in attachments) 126 | { 127 | att.Dispose(); 128 | } 129 | } 130 | } 131 | } 132 | /// 133 | /// SmtpClient连接配置 134 | /// 135 | /// 136 | /// 137 | protected virtual async Task SmtpClientSetting(SmtpClient client) 138 | { 139 | await client.ConnectAsync(this.Setting.Host, this.Setting.Port, this.Setting.UseSsl).ConfigureAwait(false); 140 | await client.AuthenticateAsync(this.Setting.UserAddress, this.Setting.Password).ConfigureAwait(false); 141 | } 142 | private string ConvertToBase64(string inputStr, Encoding encoding) 143 | { 144 | return Convert.ToBase64String(encoding.GetBytes(inputStr)); 145 | } 146 | private string ConvertHeaderToBase64(string inputStr, Encoding encoding) 147 | {//https://www.cnblogs.com/qingspace/p/3732677.html 148 | var encode = !string.IsNullOrEmpty(inputStr) && inputStr.Any(c => c > 127); 149 | if (encode) 150 | { 151 | return "=?" + encoding.WebName + "?B?" + ConvertToBase64(inputStr, encoding) + "?="; 152 | } 153 | return inputStr; 154 | } 155 | private bool IsEmpty(IEnumerable source) 156 | { 157 | return source == null || !source.Any(); 158 | } 159 | private MimeEntity GetMimeEntity(MimePart body, IEnumerable attachments) 160 | { 161 | MimeEntity entity = body; 162 | if (!this.IsEmpty(attachments)) 163 | { 164 | var mult = new Multipart("mixed") 165 | { 166 | body 167 | }; 168 | foreach (var att in attachments) 169 | { 170 | if (att.Stream != null) 171 | { 172 | var attachment = string.IsNullOrWhiteSpace(att.ContentType) ? new MimePart() : new MimePart(att.ContentType); 173 | attachment.Content = new MimeContent(att.Stream); 174 | attachment.ContentDisposition = new ContentDisposition(ContentDisposition.Attachment); 175 | attachment.ContentTransferEncoding = att.ContentTransferEncoding; 176 | attachment.FileName = ConvertHeaderToBase64(att.FileName, Encoding.UTF8);//解决附件中文名问题 177 | mult.Add(attachment); 178 | } 179 | } 180 | entity = mult; 181 | } 182 | return entity; 183 | } 184 | } 185 | } -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.EMail/EMailMimeMessageSetting.cs: -------------------------------------------------------------------------------- 1 | using MimeKit.Text; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace CheckCodeHelper.Sender.EMail 9 | { 10 | /// 11 | /// 邮件MIME Message配置 12 | /// 13 | public class EMailMimeMessageSetting 14 | { 15 | /// 16 | /// 默认的邮件内容格式,如果不设置默认为 17 | /// 18 | public TextFormat DefaultTextFormat { get; set; } 19 | /// 20 | /// 邮件配置 Key为bizFlag 21 | /// 22 | public IDictionary Parameters { get; set; } 23 | /// 24 | /// 邮件参数 25 | /// 26 | public class MimeMessageParameter 27 | { 28 | /// 29 | /// 邮件主题 30 | /// 31 | public string Subject { get; set; } 32 | /// 33 | /// 邮件内容的格式 34 | /// 35 | public TextFormat? TextFormat { get; set; } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.EMail/EMailSender.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD2_0_OR_GREATER 2 | using Microsoft.Extensions.Options; 3 | #endif 4 | using MimeKit; 5 | using MimeKit.Text; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Text.RegularExpressions; 11 | using System.Threading.Tasks; 12 | using static CheckCodeHelper.Sender.EMail.EMailMimeMessageSetting; 13 | 14 | namespace CheckCodeHelper.Sender.EMail 15 | { 16 | /// 17 | /// 通过邮件发送验证码 18 | /// 19 | public class EMailSender : ICodeSender 20 | { 21 | /// 22 | /// 默认设置的 23 | /// 24 | public const string DefaultKey = "EMAIL"; 25 | private readonly EMailMimeMessageSetting mimeMessageSetting; 26 | 27 | #if NETSTANDARD2_0_OR_GREATER 28 | /// 29 | /// 通过邮件发送验证码 30 | /// 31 | /// 验证码内容模板 32 | /// 邮件发送者 33 | /// 邮件主题配置 34 | public EMailSender(IContentFormatter formatter, EMailHelper helper, IOptions mimeMessageSetting) 35 | { 36 | this.Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); 37 | this.EMailHelper = helper ?? throw new ArgumentNullException(nameof(helper)); 38 | this.mimeMessageSetting = mimeMessageSetting.Value ?? throw new ArgumentNullException(nameof(mimeMessageSetting)); 39 | if (this.mimeMessageSetting.Parameters == null || this.mimeMessageSetting.Parameters.Count == 0) 40 | { 41 | throw new ArgumentException(nameof(this.mimeMessageSetting.Parameters)); 42 | } 43 | } 44 | #else 45 | /// 46 | /// 通过邮件发送验证码 47 | /// 48 | /// 验证码内容模板 49 | /// 邮箱配置 50 | /// 邮件主题配置 51 | public EMailSender(IContentFormatter formatter, EMailSetting emailSetting, EMailMimeMessageSetting subjectSetting) 52 | { 53 | this.Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); 54 | this.mimeMessageSetting = subjectSetting ?? throw new ArgumentNullException(nameof(subjectSetting)); 55 | this.EMailHelper = new EMailHelper(emailSetting ?? throw new ArgumentNullException(nameof(emailSetting))); 56 | if (this.mimeMessageSetting.Parameters == null || this.mimeMessageSetting.Parameters.Count == 0) 57 | { 58 | throw new ArgumentException(nameof(this.mimeMessageSetting.Parameters)); 59 | } 60 | } 61 | #endif 62 | /// 63 | /// 发送验证码内容模板 64 | /// 65 | public IContentFormatter Formatter { get; } 66 | /// 67 | /// 用于标志当前sender的唯一Key 68 | /// 69 | public string Key { get; set; } = DefaultKey; 70 | /// 71 | /// 邮件发送者 72 | /// 73 | public EMailHelper EMailHelper { get; } 74 | /// 75 | /// 判断接收者是否符合发送条件 76 | /// 77 | /// 78 | /// 79 | public virtual bool IsSupport(string receiver) 80 | { 81 | return !string.IsNullOrWhiteSpace(receiver) 82 | && Regex.IsMatch(receiver, @"^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$"); 83 | } 84 | /// 85 | /// 发送校验码信息 86 | /// 87 | /// 接收方 88 | /// 业务标志 89 | /// 校验码 90 | /// 校验码有效时间范围 91 | /// 92 | public virtual async Task SendAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime) 93 | { 94 | var parameter = this.GetParameter(bizFlag); 95 | var content = this.Formatter.GetContent(receiver, bizFlag, code, effectiveTime, this.Key); 96 | await this.EMailHelper.SendEMailAsync(parameter.Subject, content, new List { 97 | new MailboxAddress(receiver) 98 | }, this.GetTextFormat(parameter)).ConfigureAwait(false); 99 | return true; 100 | } 101 | 102 | private MimeMessageParameter GetParameter(string bizFlag) 103 | { 104 | if (!this.mimeMessageSetting.Parameters.ContainsKey(bizFlag)) 105 | { 106 | throw new KeyNotFoundException($"The parameter for '{bizFlag}' is not found"); 107 | } 108 | return this.mimeMessageSetting.Parameters[bizFlag]; 109 | } 110 | 111 | private TextFormat GetTextFormat(MimeMessageParameter parameter) 112 | { 113 | var textFormat = parameter.TextFormat; 114 | if (!textFormat.HasValue) 115 | { 116 | textFormat = this.mimeMessageSetting.DefaultTextFormat; 117 | } 118 | return textFormat.Value; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.EMail/EMailSetting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace CheckCodeHelper.Sender.EMail 8 | { 9 | /// 10 | /// 邮箱配置 11 | /// 12 | public class EMailSetting 13 | { 14 | /// 15 | /// 邮件服务器Host 16 | /// 17 | public string Host { get; set; } 18 | /// 19 | /// 邮件服务器Port 20 | /// 21 | public int Port { get; set; } 22 | /// 23 | /// 邮件服务器是否是ssl 24 | /// 25 | public bool UseSsl { get; set; } 26 | /// 27 | /// 发送邮件的账号友善名称 28 | /// 29 | public string UserName { get; set; } 30 | /// 31 | /// 发送邮件的账号地址 32 | /// 33 | public string UserAddress { get; set; } 34 | /// 35 | /// 发送邮件所需的账号密码 36 | /// 37 | public string Password { get; set; } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.Sms/CheckCodeHelper.Sender.Sms.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net452;netstandard2.0 5 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml 6 | dong fang 7 | 通用验证码发送及校验类库--通过短信发送校验信息 8 | https://github.com/fdstar/CheckCodeHelper 9 | https://mit-license.org/ 10 | false 11 | 1.0.1 12 | MIT 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.Sms/EmaySms.cs: -------------------------------------------------------------------------------- 1 | using CheckCodeHelper.Sender.Sms.Utils; 2 | using Newtonsoft.Json; 3 | using RestSharp; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Security.Cryptography; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace CheckCodeHelper.Sender.Sms 12 | { 13 | #if NETSTANDARD2_0_OR_GREATER 14 | using Microsoft.Extensions.Options; 15 | #endif 16 | /// 17 | /// 亿美短信 http://www.emay.cn/ 18 | /// 19 | public class EmaySms : ISms 20 | { 21 | private readonly string _appId; 22 | private readonly byte[] _secretKey; 23 | private const string SendSingleSmsUrl = "/inter/sendSingleSMS"; 24 | private const string SendBatchSmsUrl = "/inter/sendBatchSMS"; 25 | /// 26 | /// 默认Key值 27 | /// 28 | public const string DefaultKey = "Emay"; 29 | 30 | #if NETSTANDARD2_0_OR_GREATER 31 | /// 32 | /// 亿美短信 http://www.emay.cn/ 33 | /// 34 | /// 35 | public EmaySms(IOptions option) 36 | : this(option.Value) 37 | { 38 | } 39 | #endif 40 | 41 | /// 42 | /// 亿美短信 http://www.emay.cn/ 43 | /// 44 | /// 45 | #if NET45_OR_GREATER 46 | public 47 | #else 48 | private 49 | #endif 50 | EmaySms(EmaySetting setting) 51 | { 52 | if (setting == null || string.IsNullOrWhiteSpace(setting.Host) || string.IsNullOrWhiteSpace(setting.AppId) || string.IsNullOrWhiteSpace(setting.SecretKey)) 53 | { 54 | throw new ArgumentNullException(nameof(setting)); 55 | } 56 | this._appId = setting.AppId; 57 | var key = new byte[16]; 58 | Array.Copy(Encoding.UTF8.GetBytes(setting.SecretKey.PadRight(key.Length)), key, key.Length); 59 | this._secretKey = key; 60 | this.Client = new RestClient(setting.Host); 61 | } 62 | /// 63 | /// IRestClient 64 | /// 65 | public IRestClient Client { get; } 66 | /// 67 | /// 请求有效期(秒),默认60 68 | /// 69 | public int ValidPeriod { get; set; } = 60; 70 | /// 71 | /// 请求时是否需要Gzip压缩,默认true 72 | /// 73 | public bool UseGzip { get; set; } = true; 74 | /// 75 | /// 用于区分唯一Key值,默认 76 | /// 77 | public string Key { get; set; } = DefaultKey; 78 | 79 | private IRestRequest GetRestRequest(object data, string url,bool useGZip) 80 | { 81 | var str = JsonConvert.SerializeObject(data); 82 | var request = new RestRequest(url, Method.POST); 83 | request.AddHeader("appId", this._appId); 84 | var rawData = Encoding.UTF8.GetBytes(str); 85 | if (useGZip) 86 | { 87 | request.AddHeader("gzip", "on"); 88 | rawData = GZipHelper.Compress(rawData); 89 | } 90 | var encryptData = AesHelper.Encrypt(rawData, this._secretKey, null, CipherMode.ECB, PaddingMode.PKCS7); 91 | request.AddParameter("", encryptData, ParameterType.RequestBody); 92 | return request; 93 | } 94 | private object GetSingleSmsObj(string mobile, string content, string bizId, DateTime? sendTime) 95 | { 96 | var data = new 97 | { 98 | mobile, 99 | content, 100 | timerTime = sendTime.HasValue ? sendTime.Value.ToString("yyyy-MM-dd HH:mm:ss") : string.Empty, 101 | customSmsId = bizId, 102 | requestTime = DateTime.Now.Ticks, 103 | requestValidPeriod = this.ValidPeriod 104 | }; 105 | return data; 106 | } 107 | private bool IsResponseSuccess(IRestResponse response, bool useGZip) 108 | { 109 | bool isSuccess = response.Headers.FirstOrDefault(p => p.Name == "result")?.Value.ToString() == "SUCCESS"; 110 | #if DEBUG 111 | var responseStr = this.GetResponseContent(response, useGZip); 112 | #endif 113 | return isSuccess; 114 | } 115 | private string GetResponseContent(IRestResponse response, bool useGZip) 116 | { 117 | var data = AesHelper.Decrypt(response.RawBytes, this._secretKey, null, CipherMode.ECB, PaddingMode.PKCS7); 118 | if (useGZip) 119 | { 120 | data = GZipHelper.Decompress(data); 121 | } 122 | return Encoding.UTF8.GetString(data); 123 | } 124 | /// 125 | /// 调用亿美Api 126 | /// 127 | /// 128 | /// 129 | /// 130 | protected bool CallApi(string url, Func dataFunc) 131 | { 132 | var data = dataFunc(); 133 | var useGZip = this.UseGzip; 134 | var request = this.GetRestRequest(data, url, useGZip); 135 | var response = this.Client.Execute(request); 136 | return this.IsResponseSuccess(response, useGZip); 137 | } 138 | /// 139 | /// 调用亿美Api 140 | /// 141 | /// 142 | /// 143 | /// 144 | protected async Task CallApiAsync(string url, Func dataFunc) 145 | { 146 | var data = dataFunc(); 147 | var useGZip = this.UseGzip; 148 | var request = this.GetRestRequest(data, url, useGZip); 149 | var response = await this.Client.ExecuteAsync(request).ConfigureAwait(false); 150 | return this.IsResponseSuccess(response, useGZip); 151 | } 152 | /// 153 | /// 发送单条短信 154 | /// 155 | /// 手机号 156 | /// 短信内容 157 | /// 业务Id 158 | /// 定时发送时间(是否支持看实际短信供应商) 159 | /// 160 | public bool SendMessage(string mobile, string content, string bizId = null, DateTime? sendTime = null) 161 | { 162 | return this.CallApi(SendSingleSmsUrl, () => this.GetSingleSmsObj(mobile, content, bizId, sendTime)); 163 | } 164 | /// 165 | /// 发送单条短信 166 | /// 167 | /// 手机号 168 | /// 短信内容 169 | /// 业务Id 170 | /// 定时发送时间(是否支持看实际短信供应商) 171 | /// 172 | public async Task SendMessageAsync(string mobile, string content, string bizId = null, DateTime? sendTime = null) 173 | { 174 | return await this.CallApiAsync(SendSingleSmsUrl, () => this.GetSingleSmsObj(mobile, content, bizId, sendTime)); 175 | } 176 | /// 177 | /// 批量发送短信 178 | /// 179 | /// 短信内容 180 | /// 手机号码集合 181 | /// 该值要么为null,要么Count要与mobiles的Count一致,否则会引发异常 182 | /// 定时发送时间(是否支持看实际短信供应商) 183 | /// 184 | public bool SendMessageBatch(string content, IList mobiles, IList bizIds = null, DateTime? sendTime = null) 185 | { 186 | return this.CallApi(SendBatchSmsUrl, () => this.GetBatchSMSObj(content, mobiles, bizIds, sendTime)); 187 | } 188 | private object GetBatchSMSObj(string content, IList mobiles, IList bizIds, DateTime? sendTime) 189 | { 190 | if (bizIds != null && bizIds.Count > 0 && mobiles.Count != bizIds.Count) 191 | { 192 | throw new ArgumentException($"{nameof(mobiles)}.Count not equals {nameof(bizIds)}.Count"); 193 | } 194 | if (bizIds != null && bizIds.Count == 0) 195 | { 196 | bizIds = null; 197 | } 198 | var items = new System.Collections.ArrayList(); 199 | for (var i = 0; i < mobiles.Count; i++) 200 | { 201 | items.Add(new 202 | { 203 | mobile = mobiles[i], 204 | customSmsId = bizIds?[i] 205 | }); 206 | } 207 | var obj = new 208 | { 209 | smses = items, 210 | content, 211 | timerTime = sendTime.HasValue ? sendTime.Value.ToString("yyyy-MM-dd HH:mm:ss") : string.Empty, 212 | requestTime = DateTime.Now.Ticks, 213 | requestValidPeriod = this.ValidPeriod 214 | }; 215 | return obj; 216 | } 217 | /// 218 | /// 批量发送短信 219 | /// 220 | /// 短信内容 221 | /// 手机号码集合 222 | /// 该值要么为null,要么Count要与mobiles的Count一致,否则会引发异常 223 | /// 定时发送时间(是否支持看实际短信供应商) 224 | /// 225 | public async Task SendMessageBatchAsync(string content, IList mobiles, IList bizIds = null, DateTime? sendTime = null) 226 | { 227 | return await this.CallApiAsync(SendBatchSmsUrl, () => this.GetBatchSMSObj(content, mobiles, bizIds, sendTime)); 228 | } 229 | } 230 | 231 | /// 232 | /// 亿美短信配置 233 | /// 234 | public class EmaySetting 235 | { 236 | /// 237 | /// 亿美短信服务Host 238 | /// 239 | public string Host { get; set; } 240 | /// 241 | /// 亿美短信应用Id 242 | /// 243 | public string AppId { get; set; } 244 | /// 245 | /// 亿美短信应用秘钥 246 | /// 247 | public string SecretKey { get; set; } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.Sms/ISms.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace CheckCodeHelper.Sender.Sms 8 | { 9 | /// 10 | /// 短信发送接口 11 | /// 12 | public interface ISms 13 | { 14 | /// 15 | /// 如果存在多供应商或多账号时,可用于区分唯一Key值 16 | /// 17 | string Key { get; set; } 18 | /// 19 | /// 发送单条短信 20 | /// 21 | /// 手机号 22 | /// 短信内容 23 | /// 业务Id 24 | /// 定时发送时间(是否支持看实际短信供应商) 25 | /// 26 | bool SendMessage(string mobile, string content, string bizId = null, DateTime? sendTime = null); 27 | /// 28 | /// 发送单条短信 29 | /// 30 | /// 手机号 31 | /// 短信内容 32 | /// 业务Id 33 | /// 定时发送时间(是否支持看实际短信供应商) 34 | /// 35 | Task SendMessageAsync(string mobile, string content, string bizId = null, DateTime? sendTime = null); 36 | /// 37 | /// 批量发送短信 38 | /// 39 | /// 短信内容 40 | /// 手机号码集合 41 | /// 该值要么为null,要么Count要与mobiles的Count一致,否则会引发异常 42 | /// 定时发送时间(是否支持看实际短信供应商) 43 | /// 44 | bool SendMessageBatch(string content, IList mobiles, IList bizIds = null, DateTime? sendTime = null); 45 | /// 46 | /// 批量发送短信 47 | /// 48 | /// 短信内容 49 | /// 手机号码集合 50 | /// 该值要么为null,要么Count要与mobiles的Count一致,否则会引发异常 51 | /// 定时发送时间(是否支持看实际短信供应商) 52 | /// 53 | Task SendMessageBatchAsync(string content, IList mobiles, IList bizIds = null, DateTime? sendTime = null); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.Sms/SmsExtensions.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD2_0_OR_GREATER 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace CheckCodeHelper.Sender.Sms 11 | { 12 | /// 13 | /// 仅用于Net Core的注册方法 14 | /// 15 | public static class SmsExtensions 16 | { 17 | /// 18 | /// 注册亿美短信发送相关的服务,注意此方法仅适用于全系统就一种实现的情况 19 | /// 注意此方法不会注册依赖的 20 | /// 21 | /// 22 | /// 仅包含的配置节点 23 | /// 24 | public static IServiceCollection AddSingletonForSmsSenderWithEmay(this IServiceCollection services, IConfiguration configuration) 25 | { 26 | services.AddSingletonForEmay(configuration); 27 | services.AddSingleton(); 28 | return services; 29 | } 30 | 31 | /// 32 | /// 注册亿美发送短信的相关实现,注意此方法不注册 33 | /// 34 | /// 35 | /// 仅包含的配置节点 36 | /// 37 | public static IServiceCollection AddSingletonForEmay(this IServiceCollection services, IConfiguration configuration) 38 | { 39 | services.Configure(configuration); 40 | services.AddSingleton(); 41 | return services; 42 | } 43 | } 44 | } 45 | #endif 46 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.Sms/SmsSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.RegularExpressions; 6 | using System.Threading.Tasks; 7 | 8 | namespace CheckCodeHelper.Sender.Sms 9 | { 10 | /// 11 | /// 通过短信发送验证码 12 | /// 13 | public class SmsSender : ICodeSender 14 | { 15 | /// 16 | /// 默认设置的 17 | /// 18 | public const string DefaultKey = "SMS"; 19 | /// 20 | /// 通过短信发送验证码 21 | /// 22 | /// 验证码内容模板 23 | /// 短信发送接口 24 | public SmsSender(IContentFormatter formatter, ISms sms) 25 | { 26 | this.Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); 27 | this.Sms = sms ?? throw new ArgumentNullException(nameof(sms)); 28 | } 29 | /// 30 | /// 发送验证码内容模板 31 | /// 32 | public IContentFormatter Formatter { get; } 33 | /// 34 | /// 用于标志当前sender的唯一Key 35 | /// 36 | public string Key { get; set; } = DefaultKey; 37 | /// 38 | /// 短信发送接口 39 | /// 40 | public ISms Sms { get; } 41 | /// 42 | /// 判断接收者是否符合发送条件,目前是宽松判断,即正则 ^1\d{10}$ 43 | /// 44 | /// 45 | /// 46 | public virtual bool IsSupport(string receiver) 47 | { 48 | return !string.IsNullOrWhiteSpace(receiver) 49 | && Regex.IsMatch(receiver, @"^1\d{10}$"); 50 | } 51 | /// 52 | /// 发送校验码信息 53 | /// 54 | /// 接收方 55 | /// 业务标志 56 | /// 校验码 57 | /// 校验码有效时间范围 58 | /// 59 | public virtual async Task SendAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime) 60 | { 61 | var content = this.Formatter.GetContent(receiver, bizFlag, code, effectiveTime, this.Key); 62 | var ret = await this.Sms.SendMessageAsync(receiver, content).ConfigureAwait(false); 63 | return ret; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.Sms/Utils/GzipHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace CheckCodeHelper.Sender.Sms.Utils 10 | { 11 | /// 12 | /// GZip辅助类 13 | /// 14 | public static class GZipHelper 15 | { 16 | /// 17 | /// Gzip压缩 18 | /// 19 | /// 原始数据 20 | /// 21 | public static byte[] Compress(byte[] data) 22 | { 23 | using (MemoryStream ms = new MemoryStream()) 24 | { 25 | using (var stream = new GZipStream(ms, CompressionMode.Compress, true)) 26 | { 27 | stream.Write(data, 0, data.Length); 28 | stream.Close(); 29 | return ms.ToArray(); 30 | } 31 | } 32 | } 33 | /// 34 | /// Gzip解压 35 | /// 36 | /// 待解密的数据 37 | /// 38 | public static byte[] Decompress(byte[] data) 39 | { 40 | using (MemoryStream ms = new MemoryStream(data)) 41 | { 42 | using (Stream inStream = new GZipStream(ms, CompressionMode.Decompress)) 43 | using (MemoryStream outStream = new MemoryStream()) 44 | { 45 | byte[] buffer = new byte[4096]; 46 | while (true) 47 | { 48 | int bytesRead = inStream.Read(buffer, 0, buffer.Length); 49 | if (bytesRead <= 0) 50 | break; 51 | else 52 | outStream.Write(buffer, 0, bytesRead); 53 | } 54 | return outStream.ToArray(); 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.Sms/Utils/KeyGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace CheckCodeHelper.Sender.Sms.Utils 8 | { 9 | using System.Security.Cryptography; 10 | /// 11 | /// 秘钥生成 12 | /// 13 | public static class KeyGenerator 14 | { 15 | /// 16 | /// 随机生成秘钥(对称算法) 17 | /// 18 | /// 秘钥(base64格式) 19 | /// iv向量(base64格式) 20 | /// 要生成的KeySize,每8个byte是一个字节,注意每种算法支持的KeySize均有差异,实际可通过输出LegalKeySizes来得到支持的值 21 | public static void CreateSymmetricAlgorithmKey(out string key, out string iv, int keySize) 22 | where T : SymmetricAlgorithm, new() 23 | { 24 | using (T t = new T()) 25 | { 26 | #if DEBUG 27 | Console.WriteLine(string.Join("", t.LegalKeySizes.Select(k => string.Format("MinSize:{0} MaxSize:{1} SkipSize:{2}", k.MinSize, k.MaxSize, k.SkipSize)))); 28 | #endif 29 | t.KeySize = keySize; 30 | t.GenerateIV(); 31 | t.GenerateKey(); 32 | iv = Convert.ToBase64String(t.IV); 33 | key = Convert.ToBase64String(t.Key); 34 | } 35 | } 36 | /// 37 | /// 随机生成秘钥(非对称算法) 38 | /// 39 | /// 40 | /// 公钥(Xml格式) 41 | /// 私钥(Xml格式) 42 | /// 用于生成秘钥的非对称算法实现类,因为非对称算法长度需要在构造函数传入,所以这里只能传递算法类 43 | public static void CreateAsymmetricAlgorithmKey(out string publicKey, out string privateKey, T provider = null) 44 | where T : AsymmetricAlgorithm, new() 45 | { 46 | if (provider == null) 47 | { 48 | provider = new T(); 49 | } 50 | using (provider) 51 | { 52 | #if DEBUG 53 | Console.WriteLine(string.Join("", provider.LegalKeySizes.Select(k => string.Format("MinSize:{0} MaxSize:{1} SkipSize:{2}", k.MinSize, k.MaxSize, k.SkipSize)))); 54 | #endif 55 | publicKey = provider.ToXmlString(false); 56 | privateKey = provider.ToXmlString(true); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Sender.Sms/Utils/SymmetricAlgorithmHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace CheckCodeHelper.Sender.Sms.Utils 8 | { 9 | using System.Security.Cryptography; 10 | /// 11 | /// 对称加密辅助类 12 | /// 13 | /// 14 | public class SymmetricAlgorithmHelper 15 | where T : SymmetricAlgorithm, new() 16 | { 17 | /// 18 | /// 加密 ECB PKCS7模式 19 | /// 20 | /// 要加密的数据 21 | /// 秘钥(base64格式) 22 | /// 加密后的结果(base64格式) 23 | public static string EncryptWithECB(string str, string key) 24 | { 25 | return EncryptWithECB(str, Convert.FromBase64String(key)); 26 | } 27 | /// 28 | /// 加密 ECB PKCS7模式 29 | /// 30 | /// 要加密的数据 31 | /// 秘钥 32 | /// 加密后的结果(base64格式) 33 | public static string EncryptWithECB(string str, byte[] key) 34 | { 35 | var toEncryptArray = Encoding.UTF8.GetBytes(str); 36 | return Convert.ToBase64String(Encrypt(toEncryptArray, key, null, CipherMode.ECB, PaddingMode.PKCS7)); 37 | } 38 | /// 39 | /// 解密 ECB PKCS7模式 40 | /// 41 | /// 要解密的数据(base64格式) 42 | /// 秘钥(base64格式) 43 | /// utf-8编码返回解密后得数据 44 | public static string DecryptWithECB(string str, string key) 45 | { 46 | return DecryptWithECB(str, Convert.FromBase64String(key)); 47 | } 48 | /// 49 | /// 3DES 解密 ECB PKCS7模式 50 | /// 51 | /// 要解密的数据(base64格式) 52 | /// 秘钥 53 | /// utf-8编码返回解密后得数据 54 | public static string DecryptWithECB(string str, byte[] key) 55 | { 56 | var toDecryptArray = Convert.FromBase64String(str); 57 | return Encoding.UTF8.GetString(Decrypt(toDecryptArray, key, null, CipherMode.ECB, PaddingMode.PKCS7)); 58 | } 59 | /// 60 | /// 加密 CBC PKCS7模式 61 | /// 62 | /// 要加密的数据 63 | /// 秘钥(base64格式) 64 | /// 向量(base64格式) 65 | /// 加密后的结果(base64格式) 66 | public static string EncryptWithCBC(string str, string key, string iv) 67 | { 68 | return EncryptWithCBC(str, Convert.FromBase64String(key), Convert.FromBase64String(iv)); 69 | } 70 | /// 71 | /// 加密 CBC PKCS7模式 72 | /// 73 | /// 要加密的数据 74 | /// 秘钥 75 | /// 向量 76 | /// 加密后的结果(base64格式) 77 | public static string EncryptWithCBC(string str, byte[] key, byte[] iv) 78 | { 79 | var toEncryptArray = Encoding.UTF8.GetBytes(str); 80 | return Convert.ToBase64String(Encrypt(toEncryptArray, key, iv, CipherMode.CBC, PaddingMode.PKCS7)); 81 | } 82 | /// 83 | /// 解密 CBC PKCS7模式 84 | /// 85 | /// 要解密的数据(base64格式) 86 | /// 秘钥(base64格式) 87 | /// 向量(base64格式) 88 | /// utf-8编码返回解密后得数据 89 | public static string DecryptWithCBC(string str, string key, string iv) 90 | { 91 | return DecryptWithCBC(str, Convert.FromBase64String(key), Convert.FromBase64String(iv)); 92 | } 93 | /// 94 | /// 解密 CBC PKCS7模式 95 | /// 96 | /// 要解密的数据(base64格式) 97 | /// 秘钥 98 | /// 向量 99 | /// utf-8编码返回解密后得数据 100 | public static string DecryptWithCBC(string str, byte[] key, byte[] iv) 101 | { 102 | var toDecryptArray = Convert.FromBase64String(str); 103 | return Encoding.UTF8.GetString(Decrypt(toDecryptArray, key, iv, CipherMode.CBC, PaddingMode.PKCS7)); 104 | } 105 | /// 106 | /// 加密 107 | /// 108 | /// 要加密的数据 109 | /// 秘钥 110 | /// 向量 111 | /// 块模式 112 | /// 填充模式 113 | /// 加密后得到的数组 114 | public static byte[] Encrypt(byte[] data, byte[] key, byte[] iv, CipherMode cipherMode, PaddingMode paddingMode) 115 | { 116 | return Transform(data, cipherMode, paddingMode, algorithm => algorithm.CreateEncryptor(key, iv)); 117 | } 118 | /// 119 | /// 解密 120 | /// 121 | /// 要解密的数据 122 | /// 秘钥 123 | /// 向量 124 | /// 块模式 125 | /// 填充模式 126 | /// 解密后的到的数组 127 | public static byte[] Decrypt(byte[] data, byte[] key, byte[] iv, CipherMode cipherMode, PaddingMode paddingMode) 128 | { 129 | return Transform(data, cipherMode, paddingMode, algorithm => algorithm.CreateDecryptor(key, iv)); 130 | } 131 | private static byte[] Transform(byte[] data, CipherMode cipherMode, PaddingMode paddingMode, Func func) 132 | { 133 | using (T algorithm = new T 134 | { 135 | Mode = cipherMode, 136 | Padding = paddingMode 137 | }) 138 | { 139 | using (ICryptoTransform cTransform = func(algorithm)) 140 | { 141 | return cTransform.TransformFinalBlock(data, 0, data.Length); 142 | } 143 | } 144 | } 145 | /// 146 | /// 生成秘钥 147 | /// 148 | /// 秘钥(base64格式) 149 | /// iv向量(base64格式) 150 | /// 要生成的KeySize 151 | public static void Create(out string key, out string iv, int keySize) 152 | { 153 | KeyGenerator.CreateSymmetricAlgorithmKey(out key, out iv, keySize); 154 | } 155 | } 156 | /// 157 | /// 3DES加密辅助类 158 | /// 159 | public class TripleDESHelper : SymmetricAlgorithmHelper 160 | { 161 | /// 162 | /// 生成秘钥 163 | /// 164 | /// 秘钥(base64格式) 165 | /// iv向量(base64格式) 166 | /// 要生成的KeySize,只支持128、192 167 | public static new void Create(out string key, out string iv, int keySize = 192) 168 | { 169 | KeyGenerator.CreateSymmetricAlgorithmKey(out key, out iv, keySize); 170 | } 171 | } 172 | /// 173 | /// AES加密辅助类 174 | /// 175 | public class AesHelper : SymmetricAlgorithmHelper//RijndaelManaged 176 | { 177 | /// 178 | /// 生成秘钥 179 | /// 180 | /// 秘钥(base64格式) 181 | /// iv向量(base64格式) 182 | /// 要生成的KeySize,只支持128、192、256,java一般生成的秘钥长度为128,所以这里默认也采用128 183 | public static new void Create(out string key, out string iv, int keySize = 128) 184 | { 185 | KeyGenerator.CreateSymmetricAlgorithmKey(out key, out iv, keySize); 186 | } 187 | } 188 | /// 189 | /// DES加密辅助类 190 | /// 191 | public class DESHelper : SymmetricAlgorithmHelper 192 | { 193 | /// 194 | /// 生成秘钥 195 | /// 196 | /// 秘钥(base64格式) 197 | /// iv向量(base64格式) 198 | /// 要生成的KeySize,DES只支持64,所以此处切勿传其它值 199 | public static new void Create(out string key, out string iv, int keySize = 64) 200 | { 201 | KeyGenerator.CreateSymmetricAlgorithmKey(out key, out iv, keySize); 202 | } 203 | } 204 | /// 205 | /// RC2加密辅助类 206 | /// 207 | public class RC2Helper : SymmetricAlgorithmHelper 208 | { 209 | /// 210 | /// 生成秘钥 211 | /// 212 | /// 秘钥(base64格式) 213 | /// iv向量(base64格式) 214 | /// 要生成的KeySize,支持的MinSize:40 MaxSize:128 SkipSize:8 215 | public static new void Create(out string key, out string iv, int keySize = 96) 216 | { 217 | KeyGenerator.CreateSymmetricAlgorithmKey(out key, out iv, keySize); 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Storage.Memory/CheckCodeHelper.Storage.Memory.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net45;netstandard2.0 5 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml 6 | dong fang 7 | 通用验证码发送及校验类库--通过内存存储校验信息 8 | https://github.com/fdstar/CheckCodeHelper 9 | https://mit-license.org/ 10 | false 11 | 1.0.1 12 | MIT 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Storage.Memory/MemoryCacheStorage.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD2_0_OR_GREATER 2 | using Microsoft.Extensions.Caching.Memory; 3 | using Microsoft.Extensions.Options; 4 | #else 5 | using System.Runtime.Caching; 6 | #endif 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace CheckCodeHelper.Storage.Memory 14 | { 15 | /// 16 | /// 校验码信息存储到MemoryCache 17 | /// 18 | public class MemoryCacheStorage : ICodeStorage 19 | { 20 | /// 21 | /// Code缓存Key值前缀 22 | /// 23 | public string CodeKeyPrefix { get; set; } = "CK"; 24 | /// 25 | /// Period缓存Key值前缀 26 | /// 27 | public string PeriodKeyPrefix { get; set; } = "PK"; 28 | 29 | #if NETSTANDARD2_0_OR_GREATER 30 | /// 31 | /// 基于内存的缓存 32 | /// 33 | public IMemoryCache Cache { get; } 34 | 35 | /// 36 | /// 缓存优先级,默认 37 | /// 38 | public CacheItemPriority CacheItemPriority { get; set; } = CacheItemPriority.High; 39 | 40 | /// 41 | /// 基于IMemoryCache的构造函数 42 | /// 43 | /// 44 | public MemoryCacheStorage(IMemoryCache cache = null) 45 | { 46 | this.Cache = cache ?? new MemoryCache(Options.Create(new MemoryCacheOptions())); 47 | } 48 | #else 49 | /// 50 | /// 基于内存的缓存 51 | /// 52 | public MemoryCache Cache { get; } 53 | 54 | /// 55 | /// 缓存逐出优先级别,默认 56 | /// 57 | public CacheItemPriority Priority { get; set; } = CacheItemPriority.NotRemovable; 58 | 59 | /// 60 | /// 基于的构造函数 61 | /// 62 | /// 用于存储的Cache,如果不传则使用 63 | public MemoryCacheStorage(MemoryCache cache = null) 64 | { 65 | this.Cache = cache ?? MemoryCache.Default; 66 | } 67 | #endif 68 | /// 69 | /// 释放 70 | /// 71 | ~MemoryCacheStorage() 72 | { 73 | this.Cache.Dispose(); 74 | } 75 | 76 | /// 77 | /// 获取校验码周期内已发送次数,如果周期已到或未发送过任何验证码,则返回0 78 | /// 79 | /// 80 | /// 81 | /// 82 | public Task GetAreadySendTimesAsync(string receiver, string bizFlag) 83 | { 84 | var key = this.GetPeriodKey(receiver, bizFlag); 85 | int times = 0; 86 | var storage = this.GetFromCache(key); 87 | if (storage != null) 88 | { 89 | times = storage.Number; 90 | } 91 | #if DEBUG 92 | Console.WriteLine("Method:{0} Result:{1}", nameof(GetAreadySendTimesAsync), times); 93 | #endif 94 | return Task.FromResult(times); 95 | } 96 | /// 97 | /// 获取校验码及已尝试错误次数,如果校验码不存在或已过期,则返回null 98 | /// 99 | /// 接收方 100 | /// 业务标志 101 | /// 102 | public Task> GetEffectiveCodeAsync(string receiver, string bizFlag) 103 | { 104 | Tuple tuple = null; 105 | var key = this.GetCodeKey(receiver, bizFlag); 106 | var storage = this.GetFromCache(key); 107 | if (storage != null) 108 | { 109 | tuple = Tuple.Create(storage.Code, storage.Number); 110 | #if DEBUG 111 | Console.WriteLine("Method:{0} Result: Code {1} Errors {2} ", nameof(GetEffectiveCodeAsync), storage.Code, storage.Number); 112 | #endif 113 | } 114 | return Task.FromResult(tuple); 115 | } 116 | /// 117 | /// 校验码错误次数+1,如果校验码已过期,则不进行任何操作 118 | /// 119 | /// 接收方 120 | /// 业务标志 121 | /// 122 | public Task IncreaseCodeErrorsAsync(string receiver, string bizFlag) 123 | { 124 | var key = this.GetCodeKey(receiver, bizFlag); 125 | var storage = this.GetFromCache(key); 126 | if (storage != null) 127 | { 128 | storage.Number += 1; 129 | } 130 | return Task.FromResult(0); 131 | } 132 | /// 133 | /// 校验码周期内发送次数+1,如果周期已到,则不进行任何操作 134 | /// 135 | /// 接收方 136 | /// 业务标志 137 | /// 138 | public Task IncreaseSendTimesAsync(string receiver, string bizFlag) 139 | { 140 | var key = this.GetPeriodKey(receiver, bizFlag); 141 | var storage = this.GetFromCache(key); 142 | if (storage != null) 143 | { 144 | storage.Number += 1; 145 | } 146 | return Task.FromResult(0); 147 | } 148 | /// 149 | /// 将校验码进行持久化,如果接收方和业务标志组合已经存在,则进行覆盖 150 | /// 151 | /// 接收方 152 | /// 业务标志 153 | /// 校验码 154 | /// 校验码有效时间范围 155 | /// 156 | public Task SetCodeAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime) 157 | { 158 | var storage = new CodeStorage 159 | { 160 | Code = code, 161 | Number = 0, 162 | StorageTime = DateTimeOffset.Now 163 | }; 164 | var key = this.GetCodeKey(receiver, bizFlag); 165 | this.SetCache(key, storage, effectiveTime); 166 | return Task.FromResult(true); 167 | } 168 | /// 169 | /// 校验码发送次数周期持久化,如果接收方和业务标志组合已经存在,则进行覆盖 170 | /// 171 | /// 接收方 172 | /// 业务标志 173 | /// 周期时间范围 174 | /// 175 | public Task SetPeriodAsync(string receiver, string bizFlag, TimeSpan? period) 176 | { 177 | var storage = new CodeStorage 178 | { 179 | Number = 1, 180 | }; 181 | var key = this.GetPeriodKey(receiver, bizFlag); 182 | this.SetCache(key, storage, period); 183 | return Task.FromResult(true); 184 | } 185 | /// 186 | /// 移除周期限制以及错误次数(适用于登录成功后,错误次数限制重新开始计时的场景) 187 | /// 188 | /// 接收方 189 | /// 业务标志 190 | /// 执行结果 191 | public Task RemovePeriodAsync(string receiver, string bizFlag) 192 | { 193 | var periodKey = this.GetPeriodKey(receiver, bizFlag); 194 | var codeKey = this.GetCodeKey(receiver, bizFlag); 195 | this.Cache.Remove(periodKey); 196 | this.Cache.Remove(codeKey); 197 | return Task.FromResult(0); 198 | } 199 | private void SetCache(string key, CodeStorage storage, TimeSpan? absoluteToNow) 200 | { 201 | #if NETSTANDARD2_0_OR_GREATER 202 | var option = new MemoryCacheEntryOptions 203 | { 204 | Priority = this.CacheItemPriority, 205 | AbsoluteExpirationRelativeToNow = absoluteToNow 206 | }; 207 | this.Cache.Set(key, storage, option); 208 | #else 209 | var policy = new CacheItemPolicy 210 | { 211 | Priority = this.Priority, 212 | }; 213 | if (absoluteToNow.HasValue) 214 | { 215 | policy.AbsoluteExpiration = DateTimeOffset.Now.Add(absoluteToNow.Value); 216 | } 217 | this.Cache.Set(key, storage, policy); 218 | #endif 219 | } 220 | private CodeStorage GetFromCache(string key) 221 | { 222 | #if NETSTANDARD2_0_OR_GREATER 223 | this.Cache.TryGetValue(key, out CodeStorage storage); 224 | return storage; 225 | #else 226 | return this.Cache.Get(key) as CodeStorage; 227 | #endif 228 | } 229 | /// 230 | /// 组织IMemoryCache键值 231 | /// 232 | /// 233 | /// 234 | /// 235 | /// 236 | protected virtual string GetKey(string receiver, string bizFlag, string prefix) 237 | { 238 | return string.Format("{0}_{1}_{2}", prefix, bizFlag, receiver); 239 | } 240 | private string GetPeriodKey(string receiver, string bizFlag) 241 | { 242 | return this.GetKey(receiver, bizFlag, this.PeriodKeyPrefix); 243 | } 244 | private string GetCodeKey(string receiver, string bizFlag) 245 | { 246 | return this.GetKey(receiver, bizFlag, this.CodeKeyPrefix); 247 | } 248 | /// 249 | /// 获取最后一次校验码持久化的时间 250 | /// 251 | /// 252 | /// 253 | /// 254 | public Task GetLastSetCodeTimeAsync(string receiver, string bizFlag) 255 | { 256 | DateTimeOffset? dt = null; 257 | var key = this.GetCodeKey(receiver, bizFlag); 258 | var storage = this.GetFromCache(key); 259 | if (storage != null) 260 | { 261 | dt = storage.StorageTime; 262 | } 263 | return Task.FromResult(dt); 264 | } 265 | 266 | [Serializable] 267 | private sealed class CodeStorage 268 | { 269 | public string Code { get; set; } 270 | public int Number { get; set; } 271 | public DateTimeOffset StorageTime { get; set; } 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Storage.Memory/MemoryExtensions.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD2_0_OR_GREATER 2 | using Microsoft.Extensions.Caching.Memory; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace CheckCodeHelper.Storage.Memory 11 | { 12 | /// 13 | /// 仅用于Net Core的注册方法 14 | /// 15 | public static class MemoryExtensions 16 | { 17 | /// 18 | /// 注册基于内存的校验码信息存储 19 | /// 20 | /// 21 | /// The to configure the provided 22 | /// 23 | public static IServiceCollection AddSingletonForMemoryCacheStorage(this IServiceCollection services, Action setupAction = null) 24 | { 25 | if (setupAction == null) 26 | { 27 | services.AddMemoryCache(); 28 | } 29 | else 30 | { 31 | services.AddMemoryCache(setupAction); 32 | } 33 | services.AddSingleton(); 34 | return services; 35 | } 36 | } 37 | } 38 | #endif 39 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Storage.Redis/CheckCodeHelper.Storage.Redis.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net45;netstandard2.0 5 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml 6 | dong fang 7 | 通用验证码发送及校验类库--通过Redis存储校验信息 8 | https://github.com/fdstar/CheckCodeHelper 9 | https://mit-license.org/ 10 | false 11 | 1.0.2 12 | MIT 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Storage.Redis/DateTimeOffsetHelper.cs: -------------------------------------------------------------------------------- 1 | #if NET45 || DEBUG 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace CheckCodeHelper.Storage 9 | { 10 | /// 11 | /// Unix时间戳转换 12 | /// 13 | public static class DateTimeOffsetHelper 14 | { 15 | private const long TicksPerMillisecond = 10000; 16 | private const long TicksPerSecond = TicksPerMillisecond * 1000; 17 | private const long TicksPerMinute = TicksPerSecond * 60; 18 | private const long TicksPerHour = TicksPerMinute * 60; 19 | private const long TicksPerDay = TicksPerHour * 24; 20 | private const long MinTicks = 0; 21 | private const long MaxTicks = DaysTo10000 * TicksPerDay - 1; 22 | private const int DaysTo10000 = DaysPer400Years * 25 - 366; // 3652059 23 | // Number of days in a non-leap year 24 | private const int DaysPerYear = 365; 25 | // Number of days in 4 years 26 | private const int DaysPer4Years = DaysPerYear * 4 + 1; // 1461 27 | // Number of days in 100 years 28 | private const int DaysPer100Years = DaysPer4Years * 25 - 1; // 36524 29 | // Number of days in 400 years 30 | private const int DaysPer400Years = DaysPer100Years * 4 + 1; // 146097 31 | private const int DaysTo1970 = DaysPer400Years * 4 + DaysPer100Years * 3 + DaysPer4Years * 17 + DaysPerYear; // 719,162 32 | private const long UnixEpochTicks = TimeSpan.TicksPerDay * DaysTo1970; // 621,355,968,000,000,000 33 | //private const long UnixEpochSeconds = UnixEpochTicks / TimeSpan.TicksPerSecond; // 62,135,596,800 34 | private const long UnixEpochMilliseconds = UnixEpochTicks / TimeSpan.TicksPerMillisecond; // 62,135,596,800,000 35 | /// 36 | /// 转换为毫秒时间戳 37 | /// 38 | /// 39 | /// 40 | public static long ToUnixTimeMilliseconds(this DateTimeOffset dateTimeOffset) 41 | { 42 | long milliseconds = dateTimeOffset.UtcDateTime.Ticks / TimeSpan.TicksPerMillisecond; 43 | return milliseconds - UnixEpochMilliseconds; 44 | } 45 | /// 46 | /// 从毫秒进行转换 47 | /// 48 | /// 49 | /// 50 | public static DateTimeOffset FromUnixTimeMilliseconds(this long milliseconds) 51 | { 52 | const long MinMilliseconds = MinTicks / TimeSpan.TicksPerMillisecond - UnixEpochMilliseconds; 53 | const long MaxMilliseconds = MaxTicks / TimeSpan.TicksPerMillisecond - UnixEpochMilliseconds; 54 | 55 | if (milliseconds < MinMilliseconds || milliseconds > MaxMilliseconds) 56 | { 57 | throw new ArgumentOutOfRangeException("milliseconds", 58 | string.Format("Min:{0} Max:{1}", MinMilliseconds, MaxMilliseconds)); 59 | } 60 | 61 | long ticks = milliseconds * TimeSpan.TicksPerMillisecond + UnixEpochTicks; 62 | return new DateTimeOffset(ticks, TimeSpan.Zero); 63 | } 64 | } 65 | } 66 | #endif 67 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Storage.Redis/RedisExtensions.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD2_0_OR_GREATER 2 | using Microsoft.Extensions.DependencyInjection; 3 | using StackExchange.Redis; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace CheckCodeHelper.Storage.Redis 11 | { 12 | /// 13 | /// 仅用于Net Core的注册方法 14 | /// 15 | public static class RedisExtensions 16 | { 17 | /// 18 | /// 注册基于Redis的校验码信息存储,此方法默认认为已注册过 19 | /// 20 | /// 21 | /// 22 | public static IServiceCollection AddSingletonForRedisStorage(this IServiceCollection services) 23 | { 24 | return services.AddSingletonForRedisStorage((IConnectionMultiplexer)null); 25 | } 26 | 27 | /// 28 | /// 注册基于Redis的校验码信息存储 29 | /// 30 | /// 31 | /// 用于存储的Redis,注意如果不为null,会将该对象 32 | /// 33 | public static IServiceCollection AddSingletonForRedisStorage(this IServiceCollection services, IConnectionMultiplexer connectionMultiplexer) 34 | { 35 | if (connectionMultiplexer != null) 36 | { 37 | services.AddSingleton(connectionMultiplexer); 38 | } 39 | services.AddSingleton(); 40 | return services; 41 | } 42 | 43 | /// 44 | /// 注册基于Redis的校验码信息存储 45 | /// 46 | /// 47 | /// redis配置字符串,通过该方式会默认注册为Singleton 48 | /// 49 | public static IServiceCollection AddSingletonForRedisStorage(this IServiceCollection services, string redisConfiguration) 50 | { 51 | if (string.IsNullOrWhiteSpace(redisConfiguration)) 52 | { 53 | throw new ArgumentException(nameof(redisConfiguration)); 54 | } 55 | var connectionMultiplexer = ConnectionMultiplexer.Connect(redisConfiguration); 56 | services.AddSingletonForRedisStorage(connectionMultiplexer); 57 | return services; 58 | } 59 | } 60 | } 61 | #endif 62 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Storage.Redis/RedisStorage.cs: -------------------------------------------------------------------------------- 1 | using StackExchange.Redis; 2 | using System; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CheckCodeHelper.Storage.Redis 7 | { 8 | /// 9 | /// 校验码信息存储到Redis 10 | /// 11 | public class RedisStorage : ICodeStorage 12 | { 13 | private readonly IConnectionMultiplexer _multiplexer; 14 | private const string CodeValueHashKey = "Code"; 15 | private const string CodeErrorHashKey = "Error"; 16 | private const string CodeTimeHashKey = "Time"; 17 | private const string PeriodHashKey = "Number"; 18 | 19 | private const string SetCodeScript = @"redis.call('HMSET', KEYS[1], 'Code', ARGV[1], 'Error', ARGV[2], 'Time', ARGV[3]) 20 | if ARGV[4] ~= '-1' then 21 | return redis.call('PEXPIRE', KEYS[1], ARGV[4]) 22 | end 23 | return 1"; 24 | private const string SetPeriodScript = @"redis.call('HMSET', KEYS[1], 'Number', ARGV[1]) 25 | if ARGV[2] ~= '-1' then 26 | return redis.call('PEXPIRE', KEYS[1], ARGV[2]) 27 | end 28 | return 1"; 29 | private const string HashIncrementScript = @"local hasKey = redis.call('EXISTS', KEYS[1]) 30 | if hasKey ~= '0' then 31 | redis.call('HINCRBY', KEYS[1], KEYS[2], ARGV[1]) 32 | end 33 | return 1"; 34 | 35 | /// 36 | /// Code缓存Key值前缀 37 | /// 38 | public string CodeKeyPrefix { get; set; } = "CK"; 39 | /// 40 | /// Period缓存Key值前缀 41 | /// 42 | public string PeriodKeyPrefix { get; set; } = "PK"; 43 | /// 44 | /// 缓存写入Redis哪个库 45 | /// 46 | public int DbNumber { get; set; } = 8; 47 | /// 48 | /// 基于IConnectionMultiplexer的构造函数 49 | /// 50 | /// 51 | public RedisStorage(IConnectionMultiplexer multiplexer) 52 | { 53 | this._multiplexer = multiplexer; 54 | } 55 | /// 56 | /// 获取校验码周期内已发送次数,如果周期已到或未发送过任何验证码,则返回0 57 | /// 58 | /// 59 | /// 60 | /// 61 | public async Task GetAreadySendTimesAsync(string receiver, string bizFlag) 62 | { 63 | var db = this.GetDatabase(); 64 | var key = this.GetPeriodKey(receiver, bizFlag); 65 | var value = await db.HashGetAsync(key, PeriodHashKey).ConfigureAwait(false); 66 | value.TryParse(out int times); 67 | #if DEBUG 68 | Console.WriteLine("Method:{0} Result:{1}", nameof(GetAreadySendTimesAsync), times); 69 | #endif 70 | return times; 71 | } 72 | /// 73 | /// 获取校验码及已尝试错误次数,如果校验码不存在或已过期,则返回null 74 | /// 75 | /// 接收方 76 | /// 业务标志 77 | /// 78 | public async Task> GetEffectiveCodeAsync(string receiver, string bizFlag) 79 | { 80 | var db = this.GetDatabase(); 81 | var key = this.GetCodeKey(receiver, bizFlag); 82 | var values = await db.HashGetAsync(key, new RedisValue[] { 83 | CodeValueHashKey, 84 | CodeErrorHashKey, 85 | }).ConfigureAwait(false); 86 | if (values != null && values.Length == 2 && values.All(x => x.HasValue)) 87 | { 88 | var code = values[0].ToString(); 89 | values[1].TryParse(out int errors); 90 | #if DEBUG 91 | Console.WriteLine("Method:{0} Result: Code {1} Errors {2} ", nameof(GetEffectiveCodeAsync), code, errors); 92 | #endif 93 | return Tuple.Create(code, errors); 94 | } 95 | return null; 96 | } 97 | /// 98 | /// 校验码错误次数+1,如果校验码已过期,则不进行任何操作 99 | /// 100 | /// 接收方 101 | /// 业务标志 102 | /// 103 | public async Task IncreaseCodeErrorsAsync(string receiver, string bizFlag) 104 | { 105 | var db = this.GetDatabase(); 106 | var key = this.GetCodeKey(receiver, bizFlag); 107 | await db.ScriptEvaluateAsync(HashIncrementScript, new RedisKey[] { key, CodeErrorHashKey }, 108 | new RedisValue[] { 1 }).ConfigureAwait(false); 109 | } 110 | /// 111 | /// 校验码周期内发送次数+1,如果周期已到,则不进行任何操作 112 | /// 113 | /// 接收方 114 | /// 业务标志 115 | /// 116 | public async Task IncreaseSendTimesAsync(string receiver, string bizFlag) 117 | { 118 | var db = this.GetDatabase(); 119 | var key = this.GetPeriodKey(receiver, bizFlag); 120 | await db.ScriptEvaluateAsync(HashIncrementScript, new RedisKey[] { key, PeriodHashKey }, 121 | new RedisValue[] { 1 }).ConfigureAwait(false); 122 | } 123 | /// 124 | /// 将校验码进行持久化,如果接收方和业务标志组合已经存在,则进行覆盖 125 | /// 126 | /// 接收方 127 | /// 业务标志 128 | /// 校验码 129 | /// 校验码有效时间范围 130 | /// 131 | public async Task SetCodeAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime) 132 | { 133 | var db = this.GetDatabase(); 134 | var key = this.GetCodeKey(receiver, bizFlag); 135 | #if NETSTANDARD2_0_OR_GREATER 136 | var timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); 137 | #else 138 | var timestamp = DateTimeOffsetHelper.ToUnixTimeMilliseconds(DateTimeOffset.Now); 139 | #endif 140 | var ms = this.GetMilliseconds(effectiveTime); 141 | var ret = await db.ScriptEvaluateAsync(SetCodeScript, new RedisKey[] { key }, 142 | new RedisValue[] { code, 0, timestamp, ms }).ConfigureAwait(false); 143 | #if DEBUG 144 | Console.WriteLine("Method:{0} Result:{1}", nameof(SetCodeAsync), ret); 145 | #endif 146 | return (int)ret == 1; 147 | } 148 | /// 149 | /// 校验码发送次数周期持久化,如果接收方和业务标志组合已经存在,则进行覆盖 150 | /// 151 | /// 接收方 152 | /// 业务标志 153 | /// 周期时间范围 154 | /// 155 | public async Task SetPeriodAsync(string receiver, string bizFlag, TimeSpan? period) 156 | { 157 | var db = this.GetDatabase(); 158 | var key = this.GetPeriodKey(receiver, bizFlag); 159 | var ms = this.GetMilliseconds(period); 160 | var ret = await db.ScriptEvaluateAsync(SetPeriodScript, new RedisKey[] { key }, 161 | new RedisValue[] { 1 , ms }).ConfigureAwait(false); 162 | #if DEBUG 163 | Console.WriteLine("Method:{0} Result:{1}", nameof(SetPeriodAsync), ret); 164 | #endif 165 | return (int)ret == 1; 166 | } 167 | /// 168 | /// 移除周期限制以及错误次数(适用于登录成功后,错误次数限制重新开始计时的场景) 169 | /// 170 | /// 接收方 171 | /// 业务标志 172 | /// 执行结果 173 | public async Task RemovePeriodAsync(string receiver, string bizFlag) 174 | { 175 | var db = this.GetDatabase(); 176 | var periodKey = this.GetPeriodKey(receiver, bizFlag); 177 | var codeKey = this.GetCodeKey(receiver, bizFlag); 178 | await db.KeyDeleteAsync(new RedisKey[] { periodKey, codeKey }).ConfigureAwait(false); 179 | } 180 | /// 181 | /// 组织Redis键值 182 | /// 183 | /// 184 | /// 185 | /// 186 | /// 187 | protected virtual string GetKey(string receiver, string bizFlag, string prefix) 188 | { 189 | return string.Format("{0}:{1}:{2}", prefix, bizFlag, receiver); 190 | } 191 | private string GetPeriodKey(string receiver, string bizFlag) 192 | { 193 | return this.GetKey(receiver, bizFlag, this.PeriodKeyPrefix); 194 | } 195 | private string GetCodeKey(string receiver, string bizFlag) 196 | { 197 | return this.GetKey(receiver, bizFlag, this.CodeKeyPrefix); 198 | } 199 | private IDatabase GetDatabase() 200 | { 201 | return this._multiplexer.GetDatabase(this.DbNumber); 202 | } 203 | private long GetMilliseconds(TimeSpan? ts) 204 | { 205 | var ms = -1L; 206 | if (ts.HasValue) 207 | { 208 | ms = (long)ts.Value.TotalMilliseconds; 209 | } 210 | return ms; 211 | } 212 | /// 213 | /// 获取最后一次校验码持久化的时间 214 | /// 215 | /// 216 | /// 217 | /// 218 | public async Task GetLastSetCodeTimeAsync(string receiver, string bizFlag) 219 | { 220 | DateTimeOffset? dt = null; 221 | var db = this.GetDatabase(); 222 | var key = this.GetCodeKey(receiver, bizFlag); 223 | var value = await db.HashGetAsync(key, CodeTimeHashKey).ConfigureAwait(false); 224 | if (value.HasValue && value.TryParse(out long ts)) 225 | { 226 | #if NETSTANDARD2_0_OR_GREATER 227 | dt = DateTimeOffset.FromUnixTimeMilliseconds(ts); 228 | #else 229 | dt = DateTimeOffsetHelper.FromUnixTimeMilliseconds(ts); 230 | #endif 231 | } 232 | return dt; 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Storage.RedisCache/CheckCodeHelper.Storage.RedisCache.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net461;netstandard2.0 5 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.Storage.RedisCache/RedisCacheStorage.cs: -------------------------------------------------------------------------------- 1 | using StackExchange.Redis.Extensions.Core.Abstractions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace CheckCodeHelper.Storage.RedisCache 9 | { 10 | /// 11 | /// 校验码信息存储到Redis 12 | /// 13 | public class RedisCacheStorage : ICodeStorage 14 | { 15 | private readonly IRedisCacheClient _client; 16 | private const string CodeValueHashKey = "Code"; 17 | private const string CodeErrorHashKey = "Error"; 18 | private const string CodeTimeHashKey = "Time"; 19 | private const string PeriodHashKey = "Number"; 20 | /// 21 | /// Code缓存Key值前缀 22 | /// 23 | public string CodeKeyPrefix { get; set; } = "CKC"; 24 | /// 25 | /// Period缓存Key值前缀 26 | /// 27 | public string PeriodKeyPrefix { get; set; } = "PKC"; 28 | /// 29 | /// 缓存写入Redis哪个库 30 | /// 31 | public int DbNumber { get; set; } = 8; 32 | /// 33 | /// 基于RedisCacheClient的构造函数 34 | /// 35 | /// 36 | public RedisCacheStorage(IRedisCacheClient client) 37 | { 38 | this._client = client; 39 | } 40 | /// 41 | /// 获取校验码周期内已发送次数,如果周期已到或未发送过任何验证码,则返回0 42 | /// 43 | /// 44 | /// 45 | /// 46 | public async Task GetAreadySendTimesAsync(string receiver, string bizFlag) 47 | { 48 | var db = this.GetDatabase(); 49 | var key = this.GetPeriodKey(receiver, bizFlag); 50 | var times = await db.HashGetAsync(key, PeriodHashKey).ConfigureAwait(false); 51 | #if DEBUG 52 | Console.WriteLine("Method:{0} Result:{1}", nameof(GetAreadySendTimesAsync), times); 53 | #endif 54 | return times; 55 | } 56 | /// 57 | /// 获取校验码及已尝试错误次数,如果校验码不存在或已过期,则返回null 58 | /// 59 | /// 接收方 60 | /// 业务标志 61 | /// 62 | public async Task> GetEffectiveCodeAsync(string receiver, string bizFlag) 63 | { 64 | var db = this.GetDatabase(); 65 | var key = this.GetCodeKey(receiver, bizFlag); 66 | var dic = await db.HashGetAsync(key, new string[] { 67 | CodeValueHashKey,CodeErrorHashKey 68 | }).ConfigureAwait(false); 69 | if (dic != null && dic.Count == 2 && dic.Values.All(x => !string.IsNullOrWhiteSpace(x))) 70 | { 71 | var code = dic[CodeValueHashKey]; 72 | int.TryParse(dic[CodeErrorHashKey], out int errors); 73 | #if DEBUG 74 | Console.WriteLine("Method:{0} Result: Code {1} Errors {2} ", nameof(GetEffectiveCodeAsync), code, errors); 75 | #endif 76 | return Tuple.Create(code, errors); 77 | } 78 | return null; 79 | } 80 | /// 81 | /// 校验码错误次数+1,如果校验码已过期,则不进行任何操作 82 | /// 83 | /// 接收方 84 | /// 业务标志 85 | /// 86 | public async Task IncreaseCodeErrorsAsync(string receiver, string bizFlag) 87 | { 88 | var db = this.GetDatabase(); 89 | var key = this.GetCodeKey(receiver, bizFlag); 90 | if (await db.ExistsAsync(key).ConfigureAwait(false)) 91 | { 92 | await db.HashIncerementByAsync(key, CodeErrorHashKey, 1).ConfigureAwait(false); 93 | } 94 | } 95 | /// 96 | /// 校验码周期内发送次数+1,如果周期已到,则不进行任何操作 97 | /// 98 | /// 接收方 99 | /// 业务标志 100 | /// 101 | public async Task IncreaseSendTimesAsync(string receiver, string bizFlag) 102 | { 103 | var db = this.GetDatabase(); 104 | var key = this.GetPeriodKey(receiver, bizFlag); 105 | if (await db.ExistsAsync(key).ConfigureAwait(false)) 106 | { 107 | await db.HashIncerementByAsync(key, PeriodHashKey, 1).ConfigureAwait(false); 108 | } 109 | } 110 | /// 111 | /// 将校验码进行持久化,如果接收方和业务标志组合已经存在,则进行覆盖 112 | /// 113 | /// 接收方 114 | /// 业务标志 115 | /// 校验码 116 | /// 校验码有效时间范围 117 | /// 118 | public async Task SetCodeAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime) 119 | { 120 | var db = this.GetDatabase(); 121 | var key = this.GetCodeKey(receiver, bizFlag); 122 | await db.HashSetAsync(key, CodeValueHashKey, code).ConfigureAwait(false); 123 | await db.HashSetAsync(key, CodeErrorHashKey, 0).ConfigureAwait(false); 124 | await db.HashSetAsync(key, CodeTimeHashKey, DateTimeOffset.Now.ToUnixTimeMilliseconds()).ConfigureAwait(false); 125 | var ret = await db.UpdateExpiryAsync(key, effectiveTime).ConfigureAwait(false); 126 | #if DEBUG 127 | Console.WriteLine("Method:{0} Result:{1}", nameof(SetCodeAsync), ret); 128 | #endif 129 | return ret; 130 | } 131 | /// 132 | /// 校验码发送次数周期持久化,如果接收方和业务标志组合已经存在,则进行覆盖 133 | /// 134 | /// 接收方 135 | /// 业务标志 136 | /// 周期时间范围 137 | /// 138 | public async Task SetPeriodAsync(string receiver, string bizFlag, TimeSpan? period) 139 | { 140 | var db = this.GetDatabase(); 141 | var key = this.GetPeriodKey(receiver, bizFlag); 142 | await db.HashSetAsync(key, PeriodHashKey, 1).ConfigureAwait(false); 143 | var ret = true; 144 | if (period.HasValue) 145 | { 146 | ret = await db.UpdateExpiryAsync(key, period.Value); 147 | } 148 | #if DEBUG 149 | Console.WriteLine("Method:{0} Result:{1}", nameof(SetPeriodAsync), ret); 150 | #endif 151 | return ret; 152 | } 153 | /// 154 | /// 移除周期限制以及错误次数(适用于登录成功后,错误次数限制重新开始计时的场景) 155 | /// 156 | /// 接收方 157 | /// 业务标志 158 | /// 执行结果 159 | public async Task RemovePeriodAsync(string receiver, string bizFlag) 160 | { 161 | var db = this.GetDatabase(); 162 | var periodKey = this.GetPeriodKey(receiver, bizFlag); 163 | var codeKey = this.GetCodeKey(receiver, bizFlag); 164 | await db.RemoveAllAsync(new string[] { periodKey, codeKey }).ConfigureAwait(false); 165 | } 166 | /// 167 | /// 组织Redis键值 168 | /// 169 | /// 170 | /// 171 | /// 172 | /// 173 | protected virtual string GetKey(string receiver, string bizFlag, string prefix) 174 | { 175 | return string.Format("{0}:{1}:{2}", prefix, bizFlag, receiver); 176 | } 177 | private string GetPeriodKey(string receiver, string bizFlag) 178 | { 179 | return this.GetKey(receiver, bizFlag, this.PeriodKeyPrefix); 180 | } 181 | private string GetCodeKey(string receiver, string bizFlag) 182 | { 183 | return this.GetKey(receiver, bizFlag, this.CodeKeyPrefix); 184 | } 185 | private IRedisDatabase GetDatabase() 186 | { 187 | return this._client.GetDb(this.DbNumber); 188 | } 189 | /// 190 | /// 获取最后一次校验码持久化的时间 191 | /// 192 | /// 193 | /// 194 | /// 195 | public async Task GetLastSetCodeTimeAsync(string receiver, string bizFlag) 196 | { 197 | DateTimeOffset? dt = null; 198 | var db = this.GetDatabase(); 199 | var key = this.GetCodeKey(receiver, bizFlag); 200 | var ts = await db.HashGetAsync(key, CodeTimeHashKey).ConfigureAwait(false); 201 | if (ts > 0) 202 | { 203 | dt = DateTimeOffset.FromUnixTimeMilliseconds(ts); 204 | } 205 | return dt; 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/CheckCodeHelper.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31321.278 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper", "CheckCodeHelper\CheckCodeHelper.csproj", "{F56708BC-6655-4598-8FC4-87AB88D0335B}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Storage.RedisCache", "CheckCodeHelper.Storage.RedisCache\CheckCodeHelper.Storage.RedisCache.csproj", "{766850FF-63A7-404A-8C43-3D8AC97C2498}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Samples", "CheckCodeHelper.Samples\CheckCodeHelper.Samples.csproj", "{9410A548-3E81-43CA-8D01-92EC00353038}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Storage.Redis", "CheckCodeHelper.Storage.Redis\CheckCodeHelper.Storage.Redis.csproj", "{228189CD-BDD0-4912-A61F-B62522928FF1}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solutions", "Solutions", "{27744E1C-CEE0-41F7-988D-22734CA8787B}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{027DDE37-0525-4118-A625-3F484BDA940A}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Sender.Sms", "CheckCodeHelper.Sender.Sms\CheckCodeHelper.Sender.Sms.csproj", "{CB2BEB92-DB0A-4D28-99B0-9F3D961E6593}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Sender.EMail", "CheckCodeHelper.Sender.EMail\CheckCodeHelper.Sender.EMail.csproj", "{D33B0951-75DF-4470-B7C0-7F071149C4CA}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Storage.Memory", "CheckCodeHelper.Storage.Memory\CheckCodeHelper.Storage.Memory.csproj", "{7DF905AF-A63E-462E-9B22-BF5503912851}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckCodeHelper.Sender.AlibabaSms", "CheckCodeHelper.Sender.AlibabaSms\CheckCodeHelper.Sender.AlibabaSms.csproj", "{A75EF12C-4D4D-430F-BE97-BC4C29EA171E}" 25 | EndProject 26 | Global 27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 28 | Debug|Any CPU = Debug|Any CPU 29 | Release|Any CPU = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {F56708BC-6655-4598-8FC4-87AB88D0335B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {F56708BC-6655-4598-8FC4-87AB88D0335B}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {F56708BC-6655-4598-8FC4-87AB88D0335B}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {F56708BC-6655-4598-8FC4-87AB88D0335B}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {766850FF-63A7-404A-8C43-3D8AC97C2498}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {766850FF-63A7-404A-8C43-3D8AC97C2498}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {766850FF-63A7-404A-8C43-3D8AC97C2498}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {766850FF-63A7-404A-8C43-3D8AC97C2498}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {9410A548-3E81-43CA-8D01-92EC00353038}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {9410A548-3E81-43CA-8D01-92EC00353038}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {9410A548-3E81-43CA-8D01-92EC00353038}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {9410A548-3E81-43CA-8D01-92EC00353038}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {228189CD-BDD0-4912-A61F-B62522928FF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {228189CD-BDD0-4912-A61F-B62522928FF1}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {228189CD-BDD0-4912-A61F-B62522928FF1}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {228189CD-BDD0-4912-A61F-B62522928FF1}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {CB2BEB92-DB0A-4D28-99B0-9F3D961E6593}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {CB2BEB92-DB0A-4D28-99B0-9F3D961E6593}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {CB2BEB92-DB0A-4D28-99B0-9F3D961E6593}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {CB2BEB92-DB0A-4D28-99B0-9F3D961E6593}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {D33B0951-75DF-4470-B7C0-7F071149C4CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {D33B0951-75DF-4470-B7C0-7F071149C4CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {D33B0951-75DF-4470-B7C0-7F071149C4CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {D33B0951-75DF-4470-B7C0-7F071149C4CA}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {7DF905AF-A63E-462E-9B22-BF5503912851}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {7DF905AF-A63E-462E-9B22-BF5503912851}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {7DF905AF-A63E-462E-9B22-BF5503912851}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {7DF905AF-A63E-462E-9B22-BF5503912851}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {A75EF12C-4D4D-430F-BE97-BC4C29EA171E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {A75EF12C-4D4D-430F-BE97-BC4C29EA171E}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {A75EF12C-4D4D-430F-BE97-BC4C29EA171E}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {A75EF12C-4D4D-430F-BE97-BC4C29EA171E}.Release|Any CPU.Build.0 = Release|Any CPU 64 | EndGlobalSection 65 | GlobalSection(SolutionProperties) = preSolution 66 | HideSolutionNode = FALSE 67 | EndGlobalSection 68 | GlobalSection(NestedProjects) = preSolution 69 | {F56708BC-6655-4598-8FC4-87AB88D0335B} = {27744E1C-CEE0-41F7-988D-22734CA8787B} 70 | {766850FF-63A7-404A-8C43-3D8AC97C2498} = {27744E1C-CEE0-41F7-988D-22734CA8787B} 71 | {9410A548-3E81-43CA-8D01-92EC00353038} = {027DDE37-0525-4118-A625-3F484BDA940A} 72 | {228189CD-BDD0-4912-A61F-B62522928FF1} = {27744E1C-CEE0-41F7-988D-22734CA8787B} 73 | {CB2BEB92-DB0A-4D28-99B0-9F3D961E6593} = {27744E1C-CEE0-41F7-988D-22734CA8787B} 74 | {D33B0951-75DF-4470-B7C0-7F071149C4CA} = {27744E1C-CEE0-41F7-988D-22734CA8787B} 75 | {7DF905AF-A63E-462E-9B22-BF5503912851} = {27744E1C-CEE0-41F7-988D-22734CA8787B} 76 | {A75EF12C-4D4D-430F-BE97-BC4C29EA171E} = {27744E1C-CEE0-41F7-988D-22734CA8787B} 77 | EndGlobalSection 78 | GlobalSection(ExtensibilityGlobals) = postSolution 79 | SolutionGuid = {872232F8-C51F-4A37-B03D-E3108B2A519F} 80 | EndGlobalSection 81 | EndGlobal 82 | -------------------------------------------------------------------------------- /src/CheckCodeHelper/CheckCodeHelper.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net45;netstandard2.0 5 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml 6 | dong fang 7 | 通用验证码发送及校验类库 8 | https://github.com/fdstar/CheckCodeHelper 9 | https://mit-license.org/ 10 | false 11 | 1.0.6 12 | MIT 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/CheckCodeHelper/CodeExtensions.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD2_0_OR_GREATER 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Options; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace CheckCodeHelper 12 | { 13 | /// 14 | /// 仅用于Net Core的注册方法 15 | /// 16 | public static class CodeExtensions 17 | { 18 | /// 19 | /// 注册,并注册其依赖的和用于获取 20 | /// 注意实际支持的需要自行注册 21 | /// 22 | /// 23 | /// 仅包含的配置节点 24 | /// 25 | public static IServiceCollection AddSingletonForComplexHelper(this IServiceCollection services, IConfiguration configuration) 26 | { 27 | services.Configure(configuration); 28 | services.AddSingleton();//当前仅注册了formater,还需要内部构造 29 | services.AddSingleton(p => p.GetService());//用ComplexContentFormatter注册 30 | services.AddSingleton(p =>//校验码发送者 31 | { 32 | Func func = key => 33 | { 34 | var senders = p.GetServices(); 35 | var sender = senders.FirstOrDefault(_ => _.Key == key); 36 | if (sender == null) 37 | { 38 | throw new KeyNotFoundException($"There is no sender with key '{key}'"); 39 | } 40 | return sender; 41 | }; 42 | return func; 43 | }); 44 | services.AddSingleton(); 45 | return services; 46 | } 47 | 48 | /// 49 | /// 注册 50 | /// 51 | /// 52 | /// 53 | public static IServiceCollection AddSingletonForNoneSender(this IServiceCollection services) 54 | { 55 | services.AddSingleton(); 56 | return services; 57 | } 58 | } 59 | } 60 | #endif 61 | -------------------------------------------------------------------------------- /src/CheckCodeHelper/CodeHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace CheckCodeHelper 8 | { 9 | /// 10 | /// 业务校验码辅助接口实现 11 | /// 12 | public class CodeHelper : ICodeHelper 13 | { 14 | /// 15 | /// 基于接口实现,可依赖注入 16 | /// 17 | /// 18 | /// 19 | public CodeHelper(ICodeSender sender, ICodeStorage storage) 20 | { 21 | this.Sender = sender ?? throw new ArgumentNullException(nameof(sender)); 22 | this.Storage = storage ?? throw new ArgumentNullException(nameof(storage)); 23 | } 24 | /// 25 | /// 校验码实际发送者 26 | /// 27 | public ICodeSender Sender { get; } 28 | /// 29 | /// 校验码信息存储者 30 | /// 31 | public ICodeStorage Storage { get; } 32 | /// 33 | /// 发送校验码 34 | /// 35 | /// 接收方 36 | /// 业务标志 37 | /// 校验码 38 | /// 校验码有效时间范围 39 | /// 周期内允许的发送配置,为null则表示无限制 40 | public async Task SendCodeAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime, PeriodLimit periodLimit) 41 | { 42 | var result = SendResult.NotSupport; 43 | if (await this.IsSupportAsync(receiver).ConfigureAwait(false)) 44 | { 45 | result = SendResult.MaxSendLimit; 46 | bool canSend = periodLimit == null || periodLimit.MaxLimit <= 0; 47 | int sendCount = 0; 48 | if (!canSend) 49 | { 50 | //校验最大次数 51 | sendCount = await this.Storage.GetAreadySendTimesAsync(receiver, bizFlag).ConfigureAwait(false); 52 | canSend = sendCount < periodLimit.MaxLimit; 53 | } 54 | if (canSend) 55 | { 56 | //校验发送间隔 57 | result = SendResult.IntervalLimit; 58 | canSend = TimeSpan.Zero == await this.GetSendCDAsync(receiver, bizFlag, periodLimit).ConfigureAwait(false); 59 | } 60 | if (canSend) 61 | { 62 | //校验发送结果 63 | result = await this.SendCodeAfterCheckedAsync(receiver, bizFlag, code, effectiveTime, periodLimit, sendCount); 64 | } 65 | } 66 | return result; 67 | } 68 | private async Task IsSupportAsync(string receiver) 69 | { 70 | if (this.Sender is ICodeSenderSupportAsync senderAsync) 71 | { 72 | return await senderAsync.IsSupportAsync(receiver).ConfigureAwait(false); 73 | } 74 | return this.Sender.IsSupport(receiver); 75 | } 76 | private async Task SendCodeAfterCheckedAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime, PeriodLimit periodLimit, int sendCount) 77 | { 78 | var result = SendResult.FailInSend; 79 | if (await this.Sender.SendAsync(receiver, bizFlag, code, effectiveTime).ConfigureAwait(false) 80 | && await this.Storage.SetCodeAsync(receiver, bizFlag, code, effectiveTime).ConfigureAwait(false)) 81 | { 82 | result = SendResult.Success; 83 | if (periodLimit != null) 84 | { 85 | if (sendCount == 0) 86 | { 87 | await this.Storage.SetPeriodAsync(receiver, bizFlag, periodLimit.Period).ConfigureAwait(false); 88 | } 89 | else 90 | { 91 | await this.Storage.IncreaseSendTimesAsync(receiver, bizFlag).ConfigureAwait(false); 92 | } 93 | } 94 | } 95 | return result; 96 | } 97 | /// 98 | /// 验证校验码是否正确 99 | /// 100 | /// 接收方 101 | /// 业务标志 102 | /// 校验码 103 | /// 最大允许错误次数 104 | /// 当验证通过时,是否重置周期次数限制,默认false 105 | /// 验证结果 106 | public async Task VerifyCodeAsync(string receiver, string bizFlag, string code, int maxErrorLimit, bool resetWhileRight = false) 107 | { 108 | var result = VerificationResult.Expired; 109 | var vCode = await this.Storage.GetEffectiveCodeAsync(receiver, bizFlag).ConfigureAwait(false); 110 | if (vCode != null && !string.IsNullOrWhiteSpace(vCode.Item1)) 111 | { 112 | result = VerificationResult.MaxErrorLimit; 113 | if (vCode.Item2 < maxErrorLimit) 114 | { 115 | result = VerificationResult.Success; 116 | if (!string.Equals(vCode.Item1, code, StringComparison.OrdinalIgnoreCase)) 117 | { 118 | result = VerificationResult.Failed; 119 | await this.Storage.IncreaseCodeErrorsAsync(receiver, bizFlag).ConfigureAwait(false); 120 | } 121 | else if(resetWhileRight) 122 | { 123 | await this.Storage.RemovePeriodAsync(receiver, bizFlag).ConfigureAwait(false); 124 | } 125 | } 126 | } 127 | return result; 128 | } 129 | /// 130 | /// 获取校验码发送的CD时间,如果无CD时间,则返回 131 | /// 132 | /// 接收方 133 | /// 业务标志 134 | /// 周期内允许的发送配置,为null则表示无限制 135 | /// 136 | public async Task GetSendCDAsync(string receiver, string bizFlag, PeriodLimit periodLimit) 137 | { 138 | if (periodLimit != null && periodLimit.Interval > TimeSpan.Zero) 139 | { 140 | var lastSendTime = await this.Storage.GetLastSetCodeTimeAsync(receiver, bizFlag).ConfigureAwait(false); 141 | if (lastSendTime.HasValue) 142 | { 143 | var ts = lastSendTime.Value.Add(periodLimit.Interval.Value) - DateTimeOffset.Now; 144 | if (ts > TimeSpan.Zero) 145 | { 146 | return ts; 147 | } 148 | } 149 | } 150 | return TimeSpan.Zero; 151 | } 152 | /// 153 | /// 获取由数字组成的校验码 154 | /// 155 | /// 校验码长度 156 | /// 157 | public static string GetRandomNumber(int maxLength = 6) 158 | { 159 | if (maxLength <= 0 || maxLength >= 10) 160 | { 161 | throw new ArgumentOutOfRangeException($"{nameof(maxLength)} must between 1 and 9."); 162 | } 163 | var rd = Math.Abs(Guid.NewGuid().GetHashCode()); 164 | var tmpX = (int)Math.Pow(10, maxLength); 165 | return (rd % tmpX).ToString().PadLeft(maxLength, '0'); 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/CheckCodeHelper/ComplexContentFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace CheckCodeHelper 8 | { 9 | using System.Collections.Concurrent; 10 | /// 11 | /// 基于业务标志的多内容模板实现 12 | /// 13 | public class ComplexContentFormatter : IComplexContentFormatter 14 | { 15 | private readonly ConcurrentDictionary _dic = new ConcurrentDictionary(); 16 | /// 17 | /// 设置指定业务对应的内容模板 18 | /// 19 | /// 业务标志 20 | /// 21 | /// 内容模板 22 | public void SetFormatter(string bizFlag, string senderKey, IContentFormatter formatter) 23 | { 24 | if (!string.IsNullOrWhiteSpace(bizFlag) && formatter != null) 25 | { 26 | this._dic.AddOrUpdate(this.GetKey(bizFlag, senderKey), formatter, (k, v) => formatter); 27 | } 28 | } 29 | private string GetKey(string bizFlag, string senderKey) 30 | { 31 | return $"{senderKey}_{bizFlag}"; 32 | } 33 | /// 34 | /// 移除指定业务对应的内容模板,如果没有,则返回null 35 | /// 36 | /// 业务标志 37 | /// 38 | /// 39 | public IContentFormatter RemoveFormatter(string bizFlag, string senderKey) 40 | { 41 | if (!string.IsNullOrWhiteSpace(bizFlag) 42 | && this._dic.TryRemove(this.GetKey(bizFlag, senderKey), out IContentFormatter formatter)) 43 | { 44 | return formatter; 45 | } 46 | return null; 47 | } 48 | /// 49 | /// 将指定参数组织成待发送的文本内容 50 | /// 51 | /// 接收方 52 | /// 业务标志 53 | /// 校验码 54 | /// 校验码有效时间范围 55 | /// 56 | /// 57 | public string GetContent(string receiver, string bizFlag, string code, TimeSpan effectiveTime, string senderKey = null) 58 | { 59 | if (string.IsNullOrWhiteSpace(bizFlag)) 60 | { 61 | throw new ArgumentException(nameof(bizFlag)); 62 | } 63 | var key = this.GetKey(bizFlag, senderKey); 64 | this._dic.TryGetValue(key, out IContentFormatter formatter); 65 | if (formatter == null) 66 | { 67 | throw new KeyNotFoundException($"There is no formatter with key '{key}'"); 68 | } 69 | return formatter.GetContent(receiver, bizFlag, code, effectiveTime, senderKey); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/CheckCodeHelper/ComplexHelper.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD2_0_OR_GREATER 2 | using Microsoft.Extensions.Options; 3 | #endif 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace CheckCodeHelper 11 | { 12 | /// 13 | /// 组合 14 | /// 15 | public class ComplexHelper 16 | { 17 | private readonly Func senderFunc; 18 | 19 | /// 20 | /// 配置信息 21 | /// 22 | public ComplexSetting ComplexSetting { get; } 23 | /// 24 | /// 基于业务标志的多内容模板 25 | /// 26 | public IComplexContentFormatter ComplexContentFormatter { get; } 27 | /// 28 | /// 数据存储 29 | /// 30 | public ICodeStorage CodeStorage { get; } 31 | 32 | /// 33 | /// 构造函数 34 | /// 35 | /// 配置信息 36 | /// 模板信息 37 | /// 要使用的存储方式 38 | /// 用于根据获取对应的 39 | public ComplexHelper( 40 | #if NETSTANDARD2_0_OR_GREATER 41 | IOptions setting, 42 | #else 43 | ComplexSetting setting, 44 | #endif 45 | IComplexContentFormatter complexContentFormatter, 46 | ICodeStorage codeStorage, 47 | Func senderFunc) 48 | { 49 | #if NETSTANDARD2_0_OR_GREATER 50 | this.ComplexSetting = setting.Value; 51 | #else 52 | this.ComplexSetting = setting; 53 | #endif 54 | this.ComplexContentFormatter = complexContentFormatter; 55 | this.CodeStorage = codeStorage; 56 | this.senderFunc = senderFunc; 57 | this.InitComplexContentFormatter(complexContentFormatter); 58 | } 59 | /// 60 | /// 初始化模板信息,不为空时默认按参数顺序进行占位,TimeSpan转化为秒进行占位填充 61 | /// 62 | /// 63 | protected virtual void InitComplexContentFormatter(IComplexContentFormatter complexContentFormatter) 64 | { 65 | var dic = this.ComplexSetting.ContentFormatters; 66 | if (dic == null || dic.Count == 0) 67 | { 68 | return; 69 | } 70 | foreach (var kv in dic) 71 | { 72 | var content = kv.Value; 73 | var tuple = this.GetBizFlagAndSenderKey(kv.Key); 74 | complexContentFormatter.SetFormatter(tuple.Item1, tuple.Item2, new ContentFormatter( 75 | (r, b, c, e, s) => string.Format(content, r, b, c, ContentFormatter.GetNumberDisplayed(e, this.ComplexSetting.EffectiveTimeDisplayed), s) 76 | )); 77 | } 78 | } 79 | /// 80 | /// 获取两者组合后的唯一标志 81 | /// 82 | /// 83 | /// 84 | /// 85 | protected virtual string GetUniqueKey(string senderKey, string bizFlag) 86 | { 87 | return $"{senderKey}_{bizFlag}"; 88 | } 89 | /// 90 | /// 通过唯一标志获取相应的业务编号和 91 | /// 92 | /// 93 | /// 94 | protected virtual Tuple GetBizFlagAndSenderKey(string uniqueKey) 95 | { 96 | var idx = uniqueKey.IndexOf('_'); 97 | if (idx <= 1 || idx == uniqueKey.Length - 1) 98 | { 99 | throw new ArgumentException($"The key '{uniqueKey}' formats error"); 100 | } 101 | var bizFlag = uniqueKey.Substring(idx + 1); 102 | var senderKey = uniqueKey.Substring(0, idx); 103 | return Tuple.Create(bizFlag, senderKey); 104 | } 105 | /// 106 | /// 获取周期限制设置,返回null表示为设置 107 | /// 108 | /// 109 | /// 业务标志 110 | /// 111 | public PeriodLimit GetPeriodLimit(string senderKey, string bizFlag) 112 | { 113 | PeriodLimit period = null; 114 | var uniqueKey = this.GetUniqueKey(senderKey, bizFlag); 115 | var maxLimit = this.ComplexSetting.PeriodMaxLimits; 116 | if (maxLimit != null && maxLimit.Count > 0 && maxLimit.ContainsKey(uniqueKey)) 117 | { 118 | InitPeriodLimit(); 119 | period.MaxLimit = maxLimit[uniqueKey]; 120 | if (this.ComplexSetting.PeriodLimitSeconds.ContainsKey(uniqueKey)) 121 | { 122 | period.Period = TimeSpan.FromSeconds(this.ComplexSetting.PeriodLimitSeconds[uniqueKey]); 123 | } 124 | } 125 | if (this.ComplexSetting.PeriodLimitIntervalSeconds.ContainsKey(uniqueKey)) 126 | { 127 | InitPeriodLimit(); 128 | period.Interval = TimeSpan.FromSeconds(this.ComplexSetting.PeriodLimitIntervalSeconds[uniqueKey]); 129 | } 130 | void InitPeriodLimit() 131 | { 132 | if (period == null) 133 | { 134 | period = new PeriodLimit(); 135 | } 136 | } 137 | return period; 138 | } 139 | /// 140 | /// 获取验证码的有效时间设置 141 | /// 142 | /// 143 | /// 业务标志 144 | /// 145 | public TimeSpan GetCodeEffectiveTime(string senderKey, string bizFlag) 146 | { 147 | var dic = this.ComplexSetting.CodeEffectiveSeconds; 148 | if (dic == null || dic.Count == 0) 149 | { 150 | throw new ArgumentException(nameof(this.ComplexSetting.CodeEffectiveSeconds)); 151 | } 152 | var uniqueKey = this.GetUniqueKey(senderKey, bizFlag); 153 | if (!dic.ContainsKey(uniqueKey)) 154 | { 155 | throw new KeyNotFoundException($"The effective time for code with key '{uniqueKey}' is not found"); 156 | } 157 | return TimeSpan.FromSeconds(dic[uniqueKey]); 158 | } 159 | /// 160 | /// 获取错误次数限制设置 161 | /// 162 | /// 163 | /// 业务标志 164 | /// 165 | public int GetCodeErrorLimit(string senderKey, string bizFlag) 166 | { 167 | var dic = this.ComplexSetting.CodeMaxErrorLimits; 168 | if (dic == null || dic.Count == 0) 169 | { 170 | throw new ArgumentException(nameof(this.ComplexSetting.CodeMaxErrorLimits)); 171 | } 172 | var uniqueKey = this.GetUniqueKey(senderKey, bizFlag); 173 | if (!dic.ContainsKey(uniqueKey)) 174 | { 175 | throw new KeyNotFoundException($"The error limits for code with key '{uniqueKey}' is not found"); 176 | } 177 | return dic[uniqueKey]; 178 | } 179 | /// 180 | /// 获取 181 | /// 182 | /// 183 | /// 184 | public ICodeHelper GetCodeHelper(string senderKey) 185 | { 186 | var sender = this.senderFunc(senderKey); 187 | var codeHelper = new CodeHelper(sender, this.CodeStorage); 188 | return codeHelper; 189 | } 190 | 191 | /// 192 | /// 使用指定的发送校验码 193 | /// 194 | /// 195 | /// 接收方 196 | /// 业务标志 197 | /// 校验码 198 | /// 199 | public async Task SendCodeAsync(string senderKey, string receiver, string bizFlag, string code) 200 | { 201 | var codeHelper = this.GetCodeHelper(senderKey); 202 | var period = this.GetPeriodLimit(senderKey, bizFlag); 203 | var effctiveTime = this.GetCodeEffectiveTime(senderKey, bizFlag); 204 | return await codeHelper.SendCodeAsync(receiver, bizFlag, code, effctiveTime, period); 205 | } 206 | 207 | /// 208 | /// 使用指定的验证校验码是否正确 209 | /// 210 | /// 211 | /// 接收方 212 | /// 业务标志 213 | /// 校验码 214 | /// 当验证通过时,是否重置周期次数限制,默认false 215 | /// 216 | public async Task VerifyCodeAsync(string senderKey, string receiver, string bizFlag, string code, bool resetWhileRight = false) 217 | { 218 | var codeHelper = this.GetCodeHelper(senderKey); 219 | var errorLimit = this.GetCodeErrorLimit(senderKey, bizFlag); 220 | return await codeHelper.VerifyCodeAsync(receiver, bizFlag, code, errorLimit, resetWhileRight); 221 | } 222 | 223 | /// 224 | /// 获取校验码发送的CD时间,如果无CD时间,则返回 225 | /// 226 | /// 227 | /// 接收方 228 | /// 业务标志 229 | /// 230 | public async Task GetSendCDAsync(string senderKey, string receiver, string bizFlag) 231 | { 232 | var codeHelper = this.GetCodeHelper(senderKey); 233 | var period = this.GetPeriodLimit(senderKey, bizFlag); 234 | return await codeHelper.GetSendCDAsync(receiver, bizFlag, period); 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/CheckCodeHelper/ComplexSetting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace CheckCodeHelper 8 | { 9 | /// 10 | /// 的配置信息,所有字典类属性Key值的组成方式均为 + 下划线 + 业务标志bizFlag 11 | /// 12 | public class ComplexSetting 13 | { 14 | /// 15 | /// 发送内容中验证码有效时间的显示方式,默认以秒显示 16 | /// 17 | public EffectiveTimeDisplayedInContent EffectiveTimeDisplayed { get; set; } = EffectiveTimeDisplayedInContent.Seconds; 18 | /// 19 | /// 用于构造文本的内容模板 20 | /// 21 | public IDictionary ContentFormatters { get; set; } 22 | /// 23 | /// 周期次数限制 24 | /// 25 | public IDictionary PeriodMaxLimits { get; set; } 26 | /// 27 | /// 周期时长限制(秒) 28 | /// 29 | public IDictionary PeriodLimitSeconds { get; set; } 30 | /// 31 | /// 周期内校验码发送间隔限制(秒) 32 | /// 33 | public IDictionary PeriodLimitIntervalSeconds { get; set; } 34 | /// 35 | /// 验证码有效时间(秒) 36 | /// 37 | public IDictionary CodeEffectiveSeconds { get; set; } 38 | /// 39 | /// 最大校验错误次数 40 | /// 41 | public IDictionary CodeMaxErrorLimits { get; set; } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/CheckCodeHelper/ContentFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace CheckCodeHelper 8 | { 9 | /// 10 | /// 通用的内容模板 11 | /// 12 | public class ContentFormatter : IContentFormatter 13 | { 14 | private readonly Func _func; 15 | /// 16 | /// 通用实现,这样就无需每种业务类型都要实现 17 | /// 18 | /// 传递的委托,参数顺序与一致 19 | public ContentFormatter(Func func) 20 | { 21 | this._func = func ?? throw new ArgumentNullException(nameof(func)); 22 | } 23 | /// 24 | /// 将指定参数组织成待发送的文本内容 25 | /// 26 | /// 接收方 27 | /// 业务标志 28 | /// 校验码 29 | /// 校验码有效时间范围 30 | /// 31 | /// 32 | public string GetContent(string receiver, string bizFlag, string code, TimeSpan effectiveTime, string senderKey = null) 33 | { 34 | return this._func.Invoke(receiver, bizFlag, code, effectiveTime, senderKey); 35 | } 36 | 37 | /// 38 | /// 获取用于显示的时间数字 39 | /// 40 | /// 41 | /// 42 | /// 43 | public static double GetNumberDisplayed(TimeSpan effectiveTime, EffectiveTimeDisplayedInContent displayed) 44 | { 45 | switch (displayed) 46 | { 47 | case EffectiveTimeDisplayedInContent.Seconds: 48 | return (int)effectiveTime.TotalSeconds; 49 | case EffectiveTimeDisplayedInContent.Minutes: 50 | return effectiveTime.TotalMinutes; 51 | case EffectiveTimeDisplayedInContent.Hours: 52 | return effectiveTime.TotalHours; 53 | default: 54 | int seconds = (int)effectiveTime.TotalSeconds; 55 | const int secondsPerMinute = 60; 56 | const int secondsPerHour = 360; 57 | if (seconds >= secondsPerHour && seconds % secondsPerHour == 0) 58 | { 59 | return seconds / secondsPerHour; 60 | } 61 | else if (seconds >= secondsPerMinute && seconds % secondsPerMinute == 0) 62 | { 63 | return seconds / secondsPerMinute; 64 | } 65 | return seconds; 66 | } 67 | } 68 | } 69 | 70 | /// 71 | /// 发送内容中验证码有效时间的显示方式 72 | /// 73 | public enum EffectiveTimeDisplayedInContent 74 | { 75 | /// 76 | /// 自动判断 77 | /// 当有效时间大于等于60秒,且可被60整除时,以分钟显示 78 | /// 当有效时间大于等于360秒,且可被360整除时,以小时显示 79 | /// 其它情况,以整数秒显示 80 | /// 81 | Auto = 0, 82 | /// 83 | /// 强制以整数秒显示,如果所有验证码有效时间都较短时,可以强制指定该方式 84 | /// 85 | Seconds = 1, 86 | /// 87 | /// 强制以分钟显示,注意此时会以进行显示 88 | /// 89 | Minutes = 2, 90 | /// 91 | /// 强制以小时显示,注意此时会以进行显示 92 | /// 93 | Hours = 3, 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/CheckCodeHelper/ICodeHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace CheckCodeHelper 8 | { 9 | /// 10 | /// 业务校验码辅助接口 11 | /// 12 | public interface ICodeHelper 13 | { 14 | /// 15 | /// 校验码实际发送者 16 | /// 17 | ICodeSender Sender { get; } 18 | /// 19 | /// 校验码信息存储者 20 | /// 21 | ICodeStorage Storage { get; } 22 | /// 23 | /// 发送校验码 24 | /// 25 | /// 接收方 26 | /// 业务标志 27 | /// 校验码 28 | /// 校验码有效时间范围 29 | /// 周期内最大允许发送配置,为null则表示无限制 30 | /// 校验码发送结果 31 | Task SendCodeAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime, PeriodLimit periodLimit); 32 | /// 33 | /// 验证校验码是否正确 34 | /// 35 | /// 接收方 36 | /// 业务标志 37 | /// 校验码 38 | /// 最大允许错误次数 39 | /// 当验证通过时,是否重置周期次数限制,默认false 40 | /// 验证结果 41 | Task VerifyCodeAsync(string receiver, string bizFlag, string code, int maxErrorLimit, bool resetWhileRight = false); 42 | /// 43 | /// 获取校验码发送的CD时间,如果无CD时间,则返回 44 | /// 45 | /// 接收方 46 | /// 业务标志 47 | /// 周期内允许的发送配置,为null则表示无限制 48 | /// 49 | Task GetSendCDAsync(string receiver, string bizFlag, PeriodLimit periodLimit); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/CheckCodeHelper/ICodeSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace CheckCodeHelper 8 | { 9 | /// 10 | /// 校验码实际发送接口 11 | /// 12 | public interface ICodeSender 13 | { 14 | /// 15 | /// 发送校验码内容模板 16 | /// 17 | IContentFormatter Formatter { get; } 18 | /// 19 | /// 用于标志当前sender的唯一Key 20 | /// 21 | string Key { get; set; } 22 | /// 23 | /// 判断接收者是否符合发送条件,例如当前发送者只支持邮箱,而接收方为手机号,则返回结果应当为false 24 | /// 25 | /// 接收方 26 | /// 27 | bool IsSupport(string receiver); 28 | /// 29 | /// 发送校验码信息 30 | /// 31 | /// 接收方 32 | /// 业务标志 33 | /// 校验码 34 | /// 校验码有效时间范围 35 | /// 发送结果 36 | Task SendAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime); 37 | } 38 | 39 | /// 40 | /// 用于支持某些需要异步判断的场景 41 | /// 42 | public interface ICodeSenderSupportAsync 43 | { 44 | /// 45 | /// 判断接收者是否符合发送条件 异步版 46 | /// 47 | /// 48 | /// 49 | Task IsSupportAsync(string receiver); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/CheckCodeHelper/ICodeStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace CheckCodeHelper 8 | { 9 | /// 10 | /// 校验码信息存储接口 11 | /// 12 | public interface ICodeStorage 13 | { 14 | /// 15 | /// 将校验码进行持久化,如果接收方和业务标志组合已经存在,则进行覆盖 16 | /// 17 | /// 接收方 18 | /// 业务标志 19 | /// 校验码 20 | /// 校验码有效时间范围 21 | /// 执行结果 22 | Task SetCodeAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime); 23 | /// 24 | /// 校验码错误次数+1,如果校验码已过期,则不进行任何操作 25 | /// 26 | /// 接收方 27 | /// 业务标志 28 | /// 执行结果 29 | Task IncreaseCodeErrorsAsync(string receiver, string bizFlag); 30 | /// 31 | /// 校验码发送次数周期持久化,如果接收方和业务标志组合已经存在,则进行覆盖 32 | /// 33 | /// 接收方 34 | /// 业务标志 35 | /// 周期时间范围 36 | /// 执行结果 37 | Task SetPeriodAsync(string receiver, string bizFlag, TimeSpan? period); 38 | /// 39 | /// 移除周期限制以及错误次数(适用于登录成功后,错误次数限制重新开始计时的场景) 40 | /// 41 | /// 接收方 42 | /// 业务标志 43 | /// 执行结果 44 | Task RemovePeriodAsync(string receiver, string bizFlag); 45 | /// 46 | /// 校验码周期内发送次数+1,如果周期已到,则不进行任何操作 47 | /// 48 | /// 接收方 49 | /// 业务标志 50 | /// 执行结果 51 | Task IncreaseSendTimesAsync(string receiver, string bizFlag); 52 | /// 53 | /// 获取校验码及已尝试错误次数,如果校验码不存在或已过期,则返回null 54 | /// 55 | /// 接收方 56 | /// 业务标志 57 | /// 获取结果 58 | Task> GetEffectiveCodeAsync(string receiver, string bizFlag); 59 | /// 60 | /// 获取校验码周期内已发送次数,如果周期已到或未发送过任何验证码,则返回0 61 | /// 62 | /// 接收方 63 | /// 业务标志 64 | /// 获取结果 65 | Task GetAreadySendTimesAsync(string receiver, string bizFlag); 66 | /// 67 | /// 获取最后一次校验码持久化的时间,如果未能获取到则返回null 68 | /// 69 | /// 接收方 70 | /// 业务标志 71 | /// 获取结果 72 | Task GetLastSetCodeTimeAsync(string receiver, string bizFlag); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/CheckCodeHelper/IContentFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace CheckCodeHelper 8 | { 9 | /// 10 | /// 发送校验码内容模板接口 11 | /// 12 | public interface IContentFormatter 13 | { 14 | /// 15 | /// 将指定参数组织成待发送的文本内容 16 | /// 17 | /// 接收方 18 | /// 业务标志 19 | /// 校验码 20 | /// 校验码有效时间范围 21 | /// 22 | /// 23 | string GetContent(string receiver, string bizFlag, string code, TimeSpan effectiveTime, string senderKey = null); 24 | } 25 | /// 26 | /// 基于业务标志的多内容模板 27 | /// 28 | public interface IComplexContentFormatter : IContentFormatter 29 | { 30 | /// 31 | /// 设置指定业务对应的内容模板 32 | /// 33 | /// 业务标志 34 | /// 35 | /// 内容模板 36 | void SetFormatter(string bizFlag, string senderKey, IContentFormatter formatter); 37 | /// 38 | /// 移除指定业务对应的内容模板,如果没有,则返回null 39 | /// 40 | /// 业务标志 41 | /// 42 | /// 43 | IContentFormatter RemoveFormatter(string bizFlag, string senderKey); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/CheckCodeHelper/NoneSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace CheckCodeHelper 8 | { 9 | /// 10 | /// 检验码发送接口空实现,该实现适用于无需发送校验码的场景,比如通过图片验证码展示校验码 11 | /// 12 | public class NoneSender : ICodeSender 13 | { 14 | /// 15 | /// 默认设置的 16 | /// 17 | public const string DefaultKey = "NONE"; 18 | /// 19 | /// 发送校验码内容模板 因为此实现适用场景无需发送验证码,所以此处会返回 20 | /// 21 | public IContentFormatter Formatter => throw new NotImplementedException(); 22 | /// 23 | /// 用于标志当前sender的唯一Key 24 | /// 25 | public string Key { get; set; } = DefaultKey; 26 | /// 27 | /// 判断接收者是否符合发送条件,当前实现永远返回true 28 | /// 29 | /// 30 | /// 31 | public bool IsSupport(string receiver) => true; 32 | /// 33 | /// 发送校验码信息,当前实现永远返回true 34 | /// 35 | /// 36 | /// 37 | /// 38 | /// 39 | /// 40 | public Task SendAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime) 41 | => Task.FromResult(true); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/CheckCodeHelper/PeriodLimit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace CheckCodeHelper 8 | { 9 | /// 10 | /// 校验码发送周期设置 11 | /// 12 | public class PeriodLimit 13 | { 14 | /// 15 | /// 周期内允许的最大次数,0表示无限制 16 | /// 17 | public int MaxLimit { get; set; } 18 | /// 19 | /// 周期时间,如果不设置,则表示无周期,此时代表总共只允许发送多少次 20 | /// 21 | public TimeSpan? Period { get; set; } 22 | /// 23 | /// 验证码发送间隔,如果不设置,表示可以无冷却重新发送验证码,注意该时间最大只能为验证码的有效时间,超出部分无效且容易造成提示错误 24 | /// 注意间隔不受周期时间影响,例如发送间隔是60秒,周期是12H,在单个周期的最后一秒发送验证码,在下个周期内,还是需要60-1秒之后才可以发送 25 | /// 26 | public TimeSpan? Interval { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/CheckCodeHelper/SendResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace CheckCodeHelper 9 | { 10 | /// 11 | /// 校验码发送结果 12 | /// 13 | public enum SendResult 14 | { 15 | /// 16 | /// 发送成功 17 | /// 18 | [Description("成功")] 19 | Success = 0, 20 | /// 21 | /// 超出最大发送次数 22 | /// 23 | [Description("超出最大发送次数")] 24 | MaxSendLimit = 11, 25 | /// 26 | /// 发送失败,指的发送结果为false 27 | /// 28 | [Description("发送失败")] 29 | FailInSend = 12, 30 | /// 31 | /// 无法发送,结果为false 32 | /// 33 | [Description("无法发送")] 34 | NotSupport = 13, 35 | /// 36 | /// 发送间隔时间过短 37 | /// 38 | [Description("发送间隔时间过短")] 39 | IntervalLimit = 14, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/CheckCodeHelper/VerificationResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace CheckCodeHelper 9 | { 10 | /// 11 | /// 校验码校验结果 12 | /// 13 | public enum VerificationResult 14 | { 15 | /// 16 | /// 校验成功 17 | /// 18 | [Description("成功")] 19 | Success = 0, 20 | /// 21 | /// 校验码已过期 22 | /// 23 | [Description("校验码已过期")] 24 | Expired = 31, 25 | /// 26 | /// 校验码不一致,校验失败 27 | /// 28 | [Description("校验失败")] 29 | Failed = 32, 30 | /// 31 | /// 已经达到了最大错误尝试次数,需重新发送新的校验码 32 | /// 33 | [Description("超出最大错误次数")] 34 | MaxErrorLimit = 33, 35 | } 36 | } 37 | --------------------------------------------------------------------------------