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