├── .github
└── workflows
│ └── sonarcloud.yml
├── .gitignore
├── LICENSE
├── README.md
└── src
├── CheckCodeHelper.Samples
├── CheckCodeHelper.Samples.csproj
├── ComplexHelperTest.cs
├── ConsoleSender.cs
├── Program.cs
└── appsettings.json
├── CheckCodeHelper.Sender.AlibabaSms
├── AlibabaSmsExtensions.cs
├── AlibabaSmsParameterSetting.cs
├── AlibabaSmsSender.cs
└── CheckCodeHelper.Sender.AlibabaSms.csproj
├── CheckCodeHelper.Sender.EMail
├── AttachmentInfo.cs
├── CheckCodeHelper.Sender.EMail.csproj
├── EMailExtensions.cs
├── EMailHelper.cs
├── EMailMimeMessageSetting.cs
├── EMailSender.cs
└── EMailSetting.cs
├── CheckCodeHelper.Sender.Sms
├── CheckCodeHelper.Sender.Sms.csproj
├── EmaySms.cs
├── ISms.cs
├── SmsExtensions.cs
├── SmsSender.cs
└── Utils
│ ├── GzipHelper.cs
│ ├── KeyGenerator.cs
│ └── SymmetricAlgorithmHelper.cs
├── CheckCodeHelper.Storage.Memory
├── CheckCodeHelper.Storage.Memory.csproj
├── MemoryCacheStorage.cs
└── MemoryExtensions.cs
├── CheckCodeHelper.Storage.Redis
├── CheckCodeHelper.Storage.Redis.csproj
├── DateTimeOffsetHelper.cs
├── RedisExtensions.cs
└── RedisStorage.cs
├── CheckCodeHelper.Storage.RedisCache
├── CheckCodeHelper.Storage.RedisCache.csproj
└── RedisCacheStorage.cs
├── CheckCodeHelper.sln
└── CheckCodeHelper
├── CheckCodeHelper.csproj
├── CodeExtensions.cs
├── CodeHelper.cs
├── ComplexContentFormatter.cs
├── ComplexHelper.cs
├── ComplexSetting.cs
├── ContentFormatter.cs
├── ICodeHelper.cs
├── ICodeSender.cs
├── ICodeStorage.cs
├── IContentFormatter.cs
├── NoneSender.cs
├── PeriodLimit.cs
├── SendResult.cs
└── VerificationResult.cs
/.github/workflows/sonarcloud.yml:
--------------------------------------------------------------------------------
1 | name: sonarcloud
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 | types: [opened, synchronize, reopened]
8 | jobs:
9 | build:
10 | name: Build
11 | runs-on: windows-2019
12 | steps:
13 | - name: Set up JDK 11
14 | uses: actions/setup-java@v1
15 | with:
16 | java-version: 1.11
17 | - uses: actions/checkout@v2
18 | with:
19 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
20 | - name: Setup .NET
21 | uses: actions/setup-dotnet@v1
22 | with:
23 | dotnet-version: 3.1.x
24 | - name: Restore dependencies
25 | run: dotnet restore .\src\CheckCodeHelper.sln
26 | - name: Cache SonarCloud packages
27 | uses: actions/cache@v1
28 | with:
29 | path: ~\sonar\cache
30 | key: ${{ runner.os }}-sonar
31 | restore-keys: ${{ runner.os }}-sonar
32 | - name: Cache SonarCloud scanner
33 | id: cache-sonar-scanner
34 | uses: actions/cache@v1
35 | with:
36 | path: .\.sonar\scanner
37 | key: ${{ runner.os }}-sonar-scanner
38 | restore-keys: ${{ runner.os }}-sonar-scanner
39 | - name: Install SonarCloud scanner
40 | if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
41 | shell: powershell
42 | run: |
43 | New-Item -Path .\.sonar\scanner -ItemType Directory
44 | dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
45 | - name: Build and analyze
46 | env:
47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
48 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
49 | shell: powershell
50 | run: |
51 | .\.sonar\scanner\dotnet-sonarscanner begin /k:"fdstar_CheckCodeHelper" /o:"fdstar" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io"
52 | dotnet build .\src\CheckCodeHelper.sln --no-restore
53 | .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}"
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /src/*/obj
2 | /src/*/bin
3 | /src/.vs
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 fdstar
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CheckCodeHelper
2 | 适用于发送验证码及校验验证码的场景(比如找回密码功能)
3 | 支持周期内限制最大发送次数,支持单次发送后最大校验错误次数
4 | 存储方案默认提供了`Redis`和`MemoryCache`实现,你也可以自己实现`ICodeStorage`来支持其它存储方案。
5 |
6 |
7 | [](https://mit-license.org/)
8 | | Lib | Version | Summary | .Net |
9 | |---|---|---|---|
10 | |CheckCodeHelper|[](https://www.nuget.org/packages/CheckCodeHelper/)|主类库|`.NET45`、`.NET Standard 2.0`|
11 | |CheckCodeHelper.Sender.EMail|[](https://www.nuget.org/packages/CheckCodeHelper.Sender.EMail/)|基于EMail的`ICodeSender`实现|`.NET45`、`.NET Standard 2.0`|
12 | |CheckCodeHelper.Sender.Sms|[](https://www.nuget.org/packages/CheckCodeHelper.Sender.Sms/)|基于非模板短信的`ICodeSender`实现,默认提供`emay`短信实现|`.NET452`、`.NET Standard 2.0`|
13 | |CheckCodeHelper.Sender.AlibabaSms|[](https://www.nuget.org/packages/CheckCodeHelper.Sender.AlibabaSms/)|基于阿里模板短信的`ICodeSender`实现|`.NET45`、`.NET Standard 2.0`|
14 | |CheckCodeHelper.Storage.Redis|[](https://www.nuget.org/packages/CheckCodeHelper.Storage.Redis/)|基于Redis的`ICodeStorage`实现|`.NET45`、`.NET Standard 2.0`|
15 | |CheckCodeHelper.Storage.Memory|[](https://www.nuget.org/packages/CheckCodeHelper.Storage.Memory/)|基于MemoryCache的`ICodeStorage`实现|`.NET45`、`.NET Standard 2.0`|
16 |
17 |
18 | ## 如何使用
19 | 你可以在此处查看使用例子 https://github.com/fdstar/CheckCodeHelper/blob/master/src/CheckCodeHelper.Samples
20 | - `Program.PrevDemo()`为`new`显示声明方式实现的Demo
21 | - `ComplexHelperTest`为`.Net Core`依赖注入方式实现的Demo
22 | - `ConsoleSender`为自定义`ICodeSender`及`ICodeSenderSupportAsync`的例子
23 |
24 | ## Release History
25 | **2021-11-30 Release**
26 | |CheckCodeHelper v1.0.6|
27 | |:--|
28 | |鉴于`IComplexContentFormatter`在`ComplexHelper`中作为构造参数传入,其本身可能已包含如何构造要发送的文本内容,故调整`ComplexHelper.ComplexSetting.ContentFormatters`为空未设置时,不再抛出异常|
29 |
30 | **2021-11-22 Release**
31 | |CheckCodeHelper v1.0.5|
32 | |:--|
33 | |`ComplexHelper`公开读取配置的方法以支持某些特殊场景|
34 | |`ComplexContentFormatter`调整唯一标志的组成方式,使其与`ComplexHelper`保持一致以减少字符串碎片|
35 | |**CheckCodeHelper.Storage.Redis v1.0.2**|
36 | |`ICodeStorage.RemovePeriodAsync`同时移除周期限制及错误次数记录|
37 | |**CheckCodeHelper.Storage.Memory v1.0.1**|
38 | |`ICodeStorage.RemovePeriodAsync`同时移除周期限制及错误次数记录|
39 |
40 | **2021-11-03 Release**
41 | |CheckCodeHelper v1.0.4|
42 | |:--|
43 | |增加`ICodeSenderSupportAsync`以支持`ICodeSender.IsSupport`异步场景,如果`ICodeSender`同时实现了`ICodeSenderSupportAsync`,则`CodeHelper`会通过`ICodeSenderSupportAsync.IsSupportAsync`判断`SendResult.NotSupport`|
44 | |修正`ComplexHelper`获取`PeriodLimit`时,如果未配置`ComplexSetting.PeriodMaxLimits`会导致`ComplexSetting.PeriodLimitIntervalSeconds`无效的问题|
45 |
46 | **2021-10-20 Release**
47 | |
CheckCodeHelper.Storage.Redis v1.0.1
|
48 | |:--|
49 | |调整为通过`Lua`脚本合并执行`Redis`的多条更新指令|
50 |
51 | **2021-10-18 Release**
52 | |CheckCodeHelper v1.0.3|
53 | |:--|
54 | |增加`EffectiveTimeDisplayedInContent`以调整验证码有效期在发送内容中的展示方式,`ComplexHelper`已支持该枚举,`ComplexSetting.EffectiveTimeDisplayed`默认设置为`Seconds`,即在所有的发送内容中以秒对应的数字进行展示,设置为`Auto`时如果有效时间为整360秒或以上且可被360整除,则展示为对应的小时数,有效时间为整60秒或以上且可被60整除,则展示为对应的分钟数
55 | `SendResult.NotSupprot`修正拼写错误为`SendResult.NotSupport`;`VerificationResult.VerificationFailed`简化为`Failed`。注意对于`CheckCodeHelper`这是**破坏性调整**,会造成升级后相关判断产生错误|
56 | |**CheckCodeHelper.Sender.EMail v1.0.3**|
57 | |移除不必要的`Subject Func`,同步调整`TextFormat`及`Subject`到`EMailMimeMessageSetting`,注意对于`EMail`部分这是一个**破坏性调整** |
58 | |**CheckCodeHelper.Sender.AlibabaSms v1.0.0**|
59 | |基于阿里模板短信的`ICodeSender`实现|
60 |
61 | **2021-07-20 Release**
62 | |CheckCodeHelper v1.0.2|
63 | |:--|
64 | |增加`PeriodLimit.Interval`以支持限制发送检验码的时间间隔|
65 | |**CheckCodeHelper.Sender.Sms v1.0.1**|
66 | |升级依赖的`RestSharp`版本至`106.12.0`以解决潜在的安全问题|
67 |
68 | **2021-07-17 Release**
69 | |CheckCodeHelper v1.0.1|
70 | |:--|
71 | |增加`ComplexHelper`以统一在单个应用中校验码的发送与验证入口,支持按需调用指定的`ICodeSender`|
72 | |**CheckCodeHelper.Sender.EMail v1.0.2**|
73 | |增加`Subject Func`以支持`ComplexHelper`|
74 |
75 | **2021-07-15 Release**
76 | |CheckCodeHelper v1.0.0|
77 | |:--|
78 | |主体功能|
79 | |**CheckCodeHelper.Sender.Sms v1.0.0**|
80 | |基于非模板短信的`ICodeSender`实现|
81 | |**CheckCodeHelper.Sender.EMail v1.0.1**|
82 | |基于EMail的`ICodeSender`实现|
83 | |**CheckCodeHelper.Storage.Redis v1.0.0**|
84 | |基于`Redis`的`ICodeStorage`实现|
85 | |**CheckCodeHelper.Storage.Memory v1.0.0**|
86 | |基于`MemoryCache`的`ICodeStorage`实现|
87 |
88 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Samples/CheckCodeHelper.Samples.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp3.1
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Always
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Samples/ComplexHelperTest.cs:
--------------------------------------------------------------------------------
1 | using CheckCodeHelper.Sender.AlibabaSms;
2 | using CheckCodeHelper.Sender.EMail;
3 | using CheckCodeHelper.Sender.Sms;
4 | using CheckCodeHelper.Storage.Memory;
5 | using CheckCodeHelper.Storage.Redis;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using System;
9 | using System.Collections.Generic;
10 | using System.IO;
11 | using System.Text;
12 |
13 | namespace CheckCodeHelper.Samples
14 | {
15 | public static class ComplexHelperTest
16 | {
17 | static IServiceCollection services;
18 | static IConfiguration configuration;
19 | static ComplexHelperTest()
20 | {
21 | var basePath = Path.GetDirectoryName(typeof(ComplexHelperTest).Assembly.Location);
22 | services = new ServiceCollection();
23 | configuration = new ConfigurationBuilder()
24 | .AddJsonFile(Path.Combine(basePath, "appsettings.json"), false)
25 | .Build();
26 |
27 | Init();
28 | }
29 |
30 | public static void Init()
31 | {
32 | services.AddOptions();
33 |
34 | //注册ICodeSender
35 | services.AddSingletonForNoneSender();
36 | services.AddSingletonForSmsSenderWithEmay(configuration.GetSection("EmaySetting"));
37 | services.AddSingletonForEMailSender(configuration.GetSection("EMailSetting"), configuration.GetSection("EMailMimeMessageSetting"));
38 | services.AddSingletonForAlibabaSms(configuration.GetSection("AlibabaConfig"), configuration.GetSection("AlibabaSmsParameterSetting"));
39 |
40 | //增加自定义ICodeSender例子,且该Sender未设置周期限制
41 | services.AddSingleton();
42 |
43 | //注册ICodeStorage
44 | services.AddSingletonForRedisStorage(configuration.GetValue("Redis:Configuration"));
45 | //services.AddSingletonForMemoryCacheStorage();
46 |
47 | //注册ComplexHelper
48 | services.AddSingletonForComplexHelper(configuration.GetSection("ComplexSetting"));
49 | }
50 |
51 | public static void Start()
52 | {
53 | var serviceProvider = services.BuildServiceProvider();
54 |
55 | //获取Helper,如果默认的InitComplexContentFormatter不符合业务需求,可继承后重写
56 | //ComplexHelper依赖ICodeSender.Key来获取实际的验证码发送者,如果找不到,会产生异常
57 | var complexHelper = serviceProvider.GetRequiredService();
58 |
59 | var bizFlag = configuration.GetValue("BizFlag");
60 | if (bizFlag == "LoginValidError")
61 | {
62 | LoginError(complexHelper);
63 | return;
64 | }
65 | else
66 | {
67 | CodeValidator(complexHelper);
68 | }
69 | }
70 |
71 | private static void CodeValidator(ComplexHelper complexHelper)
72 | {
73 | Console.WriteLine("****** 您当前正在进行验证码校验Demo ******");
74 | var bizFlag = configuration.GetValue("BizFlag");
75 | var senderKey = configuration.GetValue("CurrentSenderKey");
76 | var receiver = configuration.GetValue("Receiver");
77 | var code = CodeHelper.GetRandomNumber(); //生成随机的验证码
78 |
79 | Action getTimeAction = () =>
80 | {
81 | var time = complexHelper.CodeStorage.GetLastSetCodeTimeAsync(receiver, bizFlag).Result;
82 | if (time.HasValue)
83 | {
84 | Console.WriteLine("上次发送时间:{0:yy-MM-dd HH:mm:ss.fff}", time.Value.LocalDateTime);
85 | }
86 | else
87 | {
88 | Console.WriteLine("未能获取到最后一次发送时间");
89 | }
90 | var ts = complexHelper.GetSendCDAsync(senderKey, receiver, bizFlag).Result;
91 | Console.WriteLine("校验码发送剩余CD时间:{0}秒", ts.TotalSeconds);
92 | };
93 |
94 | getTimeAction();
95 |
96 | var sendResult = complexHelper.SendCodeAsync(senderKey, receiver, bizFlag, code).Result;
97 |
98 | Console.WriteLine("发送结果:{0} 请求时间:{1:yy-MM-dd HH:mm:ss}", sendResult, DateTime.Now);
99 | if (sendResult == SendResult.Success || sendResult == SendResult.IntervalLimit)
100 | {
101 | Console.WriteLine("*****************************");
102 | while (true)
103 | {
104 | Console.WriteLine("请输入校验码:");
105 | var vCode = Console.ReadLine();
106 | if (string.IsNullOrWhiteSpace(vCode)) continue;
107 | getTimeAction();
108 | var vResult = complexHelper.VerifyCodeAsync(senderKey, receiver, bizFlag, vCode).Result;
109 | Console.WriteLine("{2:yy-MM-dd HH:mm:ss }校验码 {0} 校验结果:{1}", vCode, vResult, DateTime.Now);
110 | if (vResult != VerificationResult.Failed)
111 | {
112 | break;
113 | }
114 | }
115 | }
116 | }
117 |
118 | private static void LoginError(ComplexHelper complexHelper)
119 | {
120 | Console.WriteLine("****** 您当前正在进行密码校验错误Demo ******");
121 | var senderKey = NoneSender.DefaultKey; //密码校验错误无需实际发送内容
122 | var bizFlag = configuration.GetValue("BizFlag");
123 | var correct = "1"; //密码正确时,得到的code
124 | var error = "0"; //密码错误时,得到的code
125 | var correctPwd = "123456";//这里不管输入的是什么账号,密码假设为同一个密码
126 | Console.WriteLine("所有账户的正确密码为:" + correctPwd);
127 | do
128 | {
129 | var account = ReadLine("** 请输入您的账号 **", "账号不能为空");
130 | var pwd = ReadLine("** 请输入您的密码 **", "密码不能为空");
131 | //检验前进行一次发送,因为周期设置成只能发送一次,所以无需关心发送结果
132 | var sendResult = complexHelper.SendCodeAsync(senderKey, account, bizFlag, correct).Result;
133 | Console.WriteLine("周期设置结果:" + sendResult + " 注意周期设置结果不影响校验");
134 |
135 | var code = GetCode(pwd);
136 | var verifyResult = complexHelper.VerifyCodeAsync(senderKey, account, bizFlag, code, true).Result;//密码校验正确时,重置允许密码连续错误的次数
137 | Console.WriteLine("您账号 {0} 的密码验证结果 {1}", account, verifyResult);
138 | }
139 | while (true);
140 |
141 | string GetCode(string pwd)
142 | {
143 | return pwd == correctPwd ? correct : error;
144 | }
145 |
146 | string ReadLine(string title, string errorNotice)
147 | {
148 | Console.WriteLine(title);
149 | do
150 | {
151 | var input = Console.ReadLine();
152 | if (string.IsNullOrWhiteSpace(input))
153 | {
154 | Console.WriteLine(errorNotice);
155 | }
156 | else
157 | {
158 | return input;
159 | }
160 | }
161 | while (true);
162 | }
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Samples/ConsoleSender.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using System.Threading.Tasks;
5 |
6 | namespace CheckCodeHelper.Samples
7 | {
8 | ///
9 | /// 在控制台输出校验码
10 | ///
11 | public class ConsoleSender : ICodeSender, ICodeSenderSupportAsync
12 | {
13 | public ConsoleSender(IContentFormatter formatter)
14 | {
15 | this.Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
16 | }
17 | public IContentFormatter Formatter { get; }
18 |
19 | public string Key { get; set; } = "Console";
20 |
21 | public bool IsSupport(string receiver) => true;
22 |
23 | public Task IsSupportAsync(string receiver)
24 | {
25 | return Task.FromResult(this.IsSupport(receiver));
26 | }
27 |
28 | public Task SendAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime)
29 | {
30 | var content = this.Formatter.GetContent(receiver, bizFlag, code, effectiveTime, this.Key);
31 | Console.WriteLine("发送内容:{0}", content);
32 | return Task.FromResult(true);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Samples/Program.cs:
--------------------------------------------------------------------------------
1 | using CheckCodeHelper.Sender.EMail;
2 | using CheckCodeHelper.Sender.Sms;
3 | using CheckCodeHelper.Storage.Memory;
4 | using CheckCodeHelper.Storage.Redis;
5 | using CheckCodeHelper.Storage.RedisCache;
6 | using Microsoft.Extensions.Options;
7 | using StackExchange.Redis;
8 | using StackExchange.Redis.Extensions.Core.Abstractions;
9 | using StackExchange.Redis.Extensions.Core.Configuration;
10 | using StackExchange.Redis.Extensions.Core.Implementations;
11 | using StackExchange.Redis.Extensions.Newtonsoft;
12 | using StackExchange.Redis.Extensions.Protobuf;
13 | using System;
14 | using System.Collections.Generic;
15 | using static CheckCodeHelper.Sender.EMail.EMailMimeMessageSetting;
16 |
17 | namespace CheckCodeHelper.Samples
18 | {
19 | class Program
20 | {
21 | static readonly string BizFlag = "ForgetAndResetPassword";
22 | static readonly string Receiver = "test";//根据不同的sender,输入不同的接收验证码账号
23 | static readonly TimeSpan effectiveTime = TimeSpan.FromMinutes(1);
24 | static void Main(string[] args)
25 | {
26 | ComplexHelperTest.Start();
27 |
28 | //PrevDemo();
29 |
30 | Console.ReadLine();
31 |
32 | }
33 |
34 | private static void PrevDemo()
35 | {
36 | ICodeSender sender = null;
37 | ICodeStorage storage;
38 |
39 | //storage = GetRedisCacheStorage(); //基于StackExchange.Redis.Extensions.Core
40 | //storage = GetRedisStorage(); //仅基于StackExchange.Redis
41 | storage = GetMemoryCacheStorage(); //基于MemoryCache
42 |
43 | //sender = new NoneSender(); //无需发送验证码场景
44 | //sender = GetSmsSender(); //通过短信发送验证码
45 | //sender = GetEMailSender(); //通过邮件发送验证码
46 |
47 | CheckCodeHelperDemo(storage, sender);
48 | }
49 |
50 | private static void CheckCodeHelperDemo(ICodeStorage storage, ICodeSender sender = null)
51 | {
52 | if (sender == null)
53 | {
54 | var senderKey = "CONSOLE";
55 | sender = new ConsoleSender(GetFormatter(BizFlag, senderKey))
56 | {
57 | Key = senderKey,
58 | };
59 | }
60 |
61 | var helper = new CodeHelper(sender, storage);
62 | var code = CodeHelper.GetRandomNumber(); //生成随机的验证码
63 |
64 | Action getTimeAction = () =>
65 | {
66 | //ICodeStorage.GetLastSetCodeTime用于获取最后一次发送校验码时间
67 | //用于比如手机验证码发送后,用户刷新页面时,页面上用于按钮倒计时计数的计算
68 | var time = storage.GetLastSetCodeTimeAsync(Receiver, BizFlag).Result;
69 | if (time.HasValue)
70 | {
71 | Console.WriteLine("上次发送时间:{0:yy-MM-dd HH:mm:ss.fff}", time.Value);
72 | }
73 | else
74 | {
75 | Console.WriteLine("未能获取到最后一次发送时间");
76 | }
77 | };
78 | getTimeAction();
79 |
80 | var sendResult = helper.SendCodeAsync(Receiver, BizFlag, code, effectiveTime, new PeriodLimit
81 | {
82 | //设置周期为20分钟,然后在此段时间内最多允许发送验证码5次
83 | MaxLimit = 5,
84 | Period = TimeSpan.FromMinutes(20)
85 | }).Result;
86 |
87 | Console.WriteLine("发送结果:{0} 发送时间:{1:yy-MM-dd HH:mm:ss}", sendResult, DateTime.Now);
88 | if (sendResult == SendResult.Success)
89 | {
90 | Console.WriteLine("*****************************");
91 | while (true)
92 | {
93 | Console.WriteLine("请输入校验码:");
94 | var vCode = Console.ReadLine();
95 | if (string.IsNullOrWhiteSpace(vCode)) continue;
96 | getTimeAction();
97 | var vResult = helper.VerifyCodeAsync(Receiver, BizFlag, vCode, 3).Result;
98 | Console.WriteLine("{2:yy-MM-dd HH:mm:ss }校验码 {0} 校验结果:{1}", vCode, vResult, DateTime.Now);
99 | if (vResult != VerificationResult.Failed)
100 | {
101 | break;
102 | }
103 | }
104 | }
105 | }
106 | #region ICodeSender
107 | private static ICodeSender GetEMailSender()
108 | {
109 | var emailSetting = new EMailSetting()//设置您的smtp信息
110 | {
111 | Host = "smtp.exmail.qq.com",
112 | Port = 465,
113 | UseSsl = true,
114 | UserName = "测试",
115 | Password = "",//填入发送邮件的邮箱密码
116 | UserAddress = "",//填入发送邮件的邮箱地址
117 | };
118 | var subjectSetting = new EMailMimeMessageSetting
119 | {
120 | DefaultTextFormat = MimeKit.Text.TextFormat.Plain,
121 | Parameters = new Dictionary
122 | {
123 | { BizFlag , new MimeMessageParameter{ Subject="找回密码验证码测试邮件"}}
124 | }
125 | };
126 | var helper = new EMailHelper(Options.Create(emailSetting));
127 | var sender = new EMailSender(GetFormatter(BizFlag, EMailSender.DefaultKey), helper, Options.Create(subjectSetting));
128 | return sender;
129 | }
130 | private static ICodeSender GetSmsSender()
131 | {
132 | //此处以亿美为例
133 | string host = "http://shmtn.b2m.cn:80";
134 | string appid = "11";//填入亿美appid
135 | string secretKey = "22"; //填入亿美secretKey
136 | ISms sms = new EmaySms(Options.Create(new EmaySetting
137 | {
138 | Host = host,
139 | AppId = appid,
140 | SecretKey = secretKey
141 | }));
142 | var sender = new SmsSender(GetFormatter(BizFlag, SmsSender.DefaultKey), sms);
143 | return sender;
144 | }
145 | #endregion
146 | #region IContentFormatter
147 | private static IContentFormatter GetFormatter(string bizFlag, string senderKey)
148 | {
149 | var simpleFormatter = new ContentFormatter(
150 | (r, b, c, e, s) => $"{r}您好,您的忘记密码验证码为{c},有效期为{(int)e.TotalSeconds}秒.");
151 | //如果就一个业务场景,也可以直接返回simpleFormatter
152 | var formatter = new ComplexContentFormatter();
153 | formatter.SetFormatter(bizFlag, senderKey, simpleFormatter);
154 | return formatter;
155 | }
156 | #endregion
157 | #region ICodeStorage
158 | private static ICodeStorage GetRedisCacheStorage()
159 | {
160 | var redisConfig = new RedisConfiguration
161 | {
162 | Hosts = new RedisHost[] {
163 | new RedisHost{
164 | Host="127.0.0.1",
165 | Port=6379
166 | }
167 | }
168 | };
169 | var redisManager = new RedisCacheConnectionPoolManager(redisConfig);
170 | var redisClient = new RedisCacheClient(redisManager,
171 | new NewtonsoftSerializer(), redisConfig);//new ProtobufSerializer();
172 | var storage = new RedisCacheStorage(redisClient);
173 | return storage;
174 | }
175 | private static ICodeStorage GetRedisStorage()
176 | {
177 | var multiplexer = ConnectionMultiplexer.Connect("127.0.0.1:6379");
178 | var storage = new RedisStorage(multiplexer);
179 | return storage;
180 | }
181 | private static ICodeStorage GetMemoryCacheStorage()
182 | {
183 | return new MemoryCacheStorage();
184 | }
185 | #endregion
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Samples/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Redis": { //Redis配置
3 | "Configuration": "127.0.0.1:6379"
4 | },
5 | "EmaySetting": { //亿美短信配置
6 | "Host": "http://shmtn.b2m.cn:80",
7 | "AppId": "111",
8 | "SecretKey": "2222"
9 | },
10 | "AlibabaConfig": { //阿里短信账号配置
11 | "AccessKeyId": "",
12 | "AccessKeySecret": "",
13 | "Endpoint": "dysmsapi.aliyuncs.com"
14 | },
15 | "AlibabaSmsParameterSetting": { //阿里短信请求参数配置
16 | "DefaultSignName": "阿里云短信测试专用",
17 | "Parameters": {
18 | "ForgetAndResetPassword": {
19 | "SignName": null,
20 | "TemplateCode": "SMS_207948308"
21 | }
22 | }
23 | },
24 | "EMailMimeMessageSetting": { //邮件Mime Message配置
25 | "DefaultTextFormat": "Plain",
26 | "Parameters": {
27 | "ForgetAndResetPassword": {
28 | "Subject": "找回密码",
29 | "TextFormat": null
30 | }
31 | }
32 | },
33 | "EMailSetting": { //邮箱配置
34 | "Host": "smtp.mxhichina.com",
35 | "Port": 465,
36 | "UseSsl": true,
37 | "UserName": "测试邮箱",
38 | "UserAddress": "阿里邮箱地址",
39 | "Password": "邮箱密码"
40 | },
41 | "ComplexSetting": { //所有字典类属性Key值的组成方式均为 ICodeSender.Key + 下划线 + 业务标志bizFlag
42 | "EffectiveTimeDisplayed": "Auto", //设置为Auto会导致ContentFormatters有效期部分的展示发生变化
43 | "ContentFormatters": { //内容模板
44 | "NONE_ForgetAndResetPassword": "",
45 |
46 | "Console_ForgetAndResetPassword": "{0}的{1}(找回密码)验证码为{2},有效期为{3}秒",
47 |
48 | "EMAIL_ForgetAndResetPassword": "您的找回密码验证码为{2},有效期为{3}分钟。",
49 |
50 | "SMS_ForgetAndResetPassword": "【亿美软通】找回密码验证码为{2},有效期为{3}分钟,请尽快使用。",
51 |
52 | "ALIBABASMS_ForgetAndResetPassword": "{{\"code\":\"{2}\",\"time\":\"{3}\"}}" //阿里短信此处为Json格式,注意非表示占位的花括号需要用两个表示 {{ }}
53 | },
54 | "PeriodMaxLimits": { //周期次数限制
55 | "NONE_LoginValidError": 1, //登录错误校验比较特殊,需要设置为1,即一个周期内只允许设置一次值
56 |
57 | "NONE_ForgetAndResetPassword": 10,
58 |
59 | "EMAIL_ForgetAndResetPassword": 5,
60 |
61 | "SMS_ForgetAndResetPassword": 5,
62 |
63 | "ALIBABASMS_ForgetAndResetPassword": 5
64 | },
65 | "PeriodLimitSeconds": { //周期时间限制(秒)
66 | "NONE_LoginValidError": 3600, //登录错误校验的周期限制,一般为24小时,Demo设置为1小时
67 |
68 | "NONE_ForgetAndResetPassword": 120,
69 |
70 | "EMAIL_ForgetAndResetPassword": 3600,
71 |
72 | "SMS_ForgetAndResetPassword": 3600,
73 |
74 | "ALIBABASMS_ForgetAndResetPassword": 3600
75 | },
76 | "PeriodLimitIntervalSeconds": { //周期内校验码发送间隔限制(秒)
77 | //"NONE_LoginValidError": 30, //因为登录错误一个周期只允许发一次,所以这里可以不用设置
78 |
79 | "NONE_ForgetAndResetPassword": 40, //对应验证码有效时间配置为30,超出部分无效且会造成CD读取到的时间在最后10秒突然没了
80 |
81 | "SMS_ForgetAndResetPassword": 60,
82 |
83 | "ALIBABASMS_ForgetAndResetPassword": 60
84 | },
85 | "CodeEffectiveSeconds": { //验证码有效时间(秒)
86 | "NONE_LoginValidError": 3600, //登录错误校验的验证码有效期需与周期时间限制一致
87 |
88 | "NONE_ForgetAndResetPassword": 30,
89 |
90 | "Console_ForgetAndResetPassword": 40,
91 |
92 | "EMAIL_ForgetAndResetPassword": 600,
93 |
94 | "SMS_ForgetAndResetPassword": 120,
95 |
96 | "ALIBABASMS_ForgetAndResetPassword": 120
97 | },
98 | "CodeMaxErrorLimits": { //校验错误次数
99 | "NONE_LoginValidError": 5, //设置一个周期内,密码错误允许连续错误几次
100 |
101 | "NONE_ForgetAndResetPassword": 3,
102 |
103 | "Console_ForgetAndResetPassword": 5,
104 |
105 | "EMAIL_ForgetAndResetPassword": 5,
106 |
107 | "SMS_ForgetAndResetPassword": 5,
108 |
109 | "ALIBABASMS_ForgetAndResetPassword": 5
110 | }
111 | },
112 | //以下为业务测试数据,实际不应该走配置
113 | "CurrentSenderKey": "Console",
114 | "BizFlag": "ForgetAndResetPassword", //LoginValidError
115 | "Receiver": "test"
116 | }
117 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Sender.AlibabaSms/AlibabaSmsExtensions.cs:
--------------------------------------------------------------------------------
1 | #if NETSTANDARD2_0_OR_GREATER
2 | using AlibabaCloud.OpenApiClient.Models;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Text;
9 | using System.Threading.Tasks;
10 |
11 | namespace CheckCodeHelper.Sender.AlibabaSms
12 | {
13 | ///
14 | /// 仅用于Net Core的注册方法
15 | ///
16 | public static class AlibabaSmsExtensions
17 | {
18 | ///
19 | /// 注册阿里短信发送相关的服务,注意此方法仅适用于全系统就一种的情况
20 | /// 注意此方法不会注册依赖的
21 | ///
22 | ///
23 | /// 仅包含的配置节点
24 | /// 仅包含的配置节点
25 | ///
26 | public static IServiceCollection AddSingletonForAlibabaSms(this IServiceCollection services, IConfiguration config, IConfiguration setting)
27 | {
28 | services.Configure(config);
29 | services.Configure(setting);
30 | services.AddSingleton();
31 | return services;
32 | }
33 | }
34 | }
35 | #endif
36 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Sender.AlibabaSms/AlibabaSmsParameterSetting.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace CheckCodeHelper.Sender.AlibabaSms
8 | {
9 | ///
10 | /// 阿里短信请求参数配置
11 | ///
12 | public class AlibabaSmsParameterSetting
13 | {
14 | ///
15 | /// 默认的短信签名,如果未设置则以此为短信签名
16 | ///
17 | public string DefaultSignName { get; set; }
18 |
19 | ///
20 | /// 参数字典 Key为bizFlag
21 | ///
22 | public IDictionary Parameters { get; set; }
23 | ///
24 | /// 阿里短信请求参数 https://next.api.aliyun.com/document/Dysmsapi/2017-05-25/SendSms
25 | ///
26 | public class AlibabaSmsRequestParameter
27 | {
28 | ///
29 | /// 短信签名名称
30 | ///
31 | public string SignName { get; set; }
32 | ///
33 | /// 短信模板ID
34 | ///
35 | public string TemplateCode { get; set; }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Sender.AlibabaSms/AlibabaSmsSender.cs:
--------------------------------------------------------------------------------
1 | #if NETSTANDARD2_0_OR_GREATER
2 | using Microsoft.Extensions.Options;
3 | #endif
4 | using AlibabaCloud.OpenApiClient.Models;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Text;
9 | using System.Text.RegularExpressions;
10 | using System.Threading.Tasks;
11 | using static CheckCodeHelper.Sender.AlibabaSms.AlibabaSmsParameterSetting;
12 | using AlibabaCloud.SDK.Dysmsapi20170525;
13 | using AlibabaCloud.SDK.Dysmsapi20170525.Models;
14 |
15 | namespace CheckCodeHelper.Sender.AlibabaSms
16 | {
17 | ///
18 | /// 通过阿里短信发送校验码
19 | ///
20 | public class AlibabaSmsSender : ICodeSender
21 | {
22 | ///
23 | /// 默认设置的
24 | ///
25 | public const string DefaultKey = "ALIBABASMS";
26 | private readonly AlibabaSmsParameterSetting setting;
27 | private readonly Config config;
28 |
29 | ///
30 | /// 通过阿里短信发送校验码
31 | ///
32 | ///
33 | ///
34 | ///
35 | public AlibabaSmsSender(
36 | IContentFormatter formatter,
37 | #if NETSTANDARD2_0_OR_GREATER
38 | IOptions setting,
39 | IOptions config
40 | #else
41 | AlibabaSmsParameterSetting setting,
42 | Config config
43 | #endif
44 | )
45 | {
46 | this.Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
47 | #if NETSTANDARD2_0_OR_GREATER
48 | this.setting = setting.Value;
49 | this.config = config.Value;
50 | #else
51 | this.setting = setting ?? throw new ArgumentNullException(nameof(setting));
52 | this.config = config;
53 | #endif
54 | if (this.setting.Parameters == null || this.setting.Parameters.Count == 0)
55 | {
56 | throw new ArgumentException(nameof(this.setting.Parameters));
57 | }
58 | }
59 |
60 | ///
61 | /// 发送验证码Json模板
62 | ///
63 | public IContentFormatter Formatter { get; }
64 |
65 | ///
66 | /// 用于标志当前sender的唯一Key
67 | ///
68 | public string Key { get; set; } = DefaultKey;
69 |
70 | ///
71 | /// 判断接收者是否符合发送条件,目前是宽松判断,即正则 ^1\d{10}$
72 | ///
73 | ///
74 | ///
75 | public virtual bool IsSupport(string receiver)
76 | {
77 | return !string.IsNullOrWhiteSpace(receiver)
78 | && Regex.IsMatch(receiver, @"^1\d{10}$");
79 | }
80 |
81 | ///
82 | /// 发送校验码信息
83 | ///
84 | /// 接收方
85 | /// 业务标志
86 | /// 校验码
87 | /// 校验码有效时间范围
88 | ///
89 | public async Task SendAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime)
90 | {
91 | var param = this.GetRequestParameter(bizFlag);
92 | var request = new SendSmsRequest
93 | {
94 | PhoneNumbers = receiver,
95 | SignName = this.GetSignName(param, bizFlag),
96 | TemplateCode = param.TemplateCode,
97 | TemplateParam = this.Formatter.GetContent(receiver, bizFlag, code, effectiveTime, this.Key),
98 | };
99 | var client = this.CreateClient();
100 | var response = await client.SendSmsAsync(request).ConfigureAwait(false);
101 | return string.Equals(response.Body.Code, "OK", StringComparison.OrdinalIgnoreCase);
102 | }
103 |
104 | private Client CreateClient()
105 | {
106 | return new Client(this.config);
107 | }
108 |
109 | private AlibabaSmsRequestParameter GetRequestParameter(string bizFlag)
110 | {
111 | if (!this.setting.Parameters.ContainsKey(bizFlag))
112 | {
113 | throw new KeyNotFoundException($"The request parameter for '{bizFlag}' is not found");
114 | }
115 | return this.setting.Parameters[bizFlag];
116 | }
117 |
118 | private string GetSignName(AlibabaSmsRequestParameter parameter, string bizFlag)
119 | {
120 | var signName = parameter.SignName;
121 | if (string.IsNullOrWhiteSpace(signName))
122 | {
123 | signName = this.setting.DefaultSignName;
124 | if (string.IsNullOrWhiteSpace(signName))
125 | {
126 | throw new ArgumentException($"The sign name for '{bizFlag}' is not correct");
127 | }
128 | }
129 | return signName;
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Sender.AlibabaSms/CheckCodeHelper.Sender.AlibabaSms.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net45;netstandard2.0
5 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml
6 | dong fang
7 | 通用验证码发送及校验类库--通过阿里短信发送校验信息
8 | https://github.com/fdstar/CheckCodeHelper
9 | https://mit-license.org/
10 | false
11 | 1.0.0
12 | MIT
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Sender.EMail/AttachmentInfo.cs:
--------------------------------------------------------------------------------
1 | using MimeKit;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace CheckCodeHelper.Sender.EMail
10 | {
11 | ///
12 | /// 邮件附件信息
13 | ///
14 | public sealed class AttachmentInfo : IDisposable
15 | {
16 | ///
17 | /// 释放
18 | ///
19 | ~AttachmentInfo()
20 | {
21 | this.Dispose();
22 | }
23 |
24 | ///
25 | /// 附件类型
26 | ///
27 | public string ContentType { get; set; }
28 | ///
29 | /// 文件名称
30 | ///
31 | public string FileName { get; set; }
32 | ///
33 | /// 文件传输编码方式
34 | ///
35 | public ContentEncoding ContentTransferEncoding { get; set; } = ContentEncoding.Default;
36 | ///
37 | /// 文件数组
38 | ///
39 | public byte[] Data { get; set; }
40 | private Stream _stream;
41 | ///
42 | /// 文件数据流
43 | ///
44 | public Stream Stream
45 | {
46 | get
47 | {
48 | if (this._stream == null && this.Data != null)
49 | {
50 | _stream = new MemoryStream(this.Data);
51 | }
52 | return this._stream;
53 | }
54 | set { this._stream = value; }
55 | }
56 | ///
57 | /// 释放Stream
58 | ///
59 | public void Dispose()
60 | {
61 | if (this._stream != null)
62 | {
63 | this._stream.Dispose();
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Sender.EMail/CheckCodeHelper.Sender.EMail.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net45;netstandard2.0
5 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml
6 | dong fang
7 | 通用验证码发送及校验类库--通过邮件发送校验信息
8 | https://github.com/fdstar/CheckCodeHelper
9 | https://mit-license.org/
10 | false
11 | 1.0.3
12 | MIT
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Sender.EMail/EMailExtensions.cs:
--------------------------------------------------------------------------------
1 | #if NETSTANDARD2_0_OR_GREATER
2 | using Microsoft.Extensions.Configuration;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.Extensions.Options;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Text;
9 | using System.Threading.Tasks;
10 |
11 | namespace CheckCodeHelper.Sender.EMail
12 | {
13 | ///
14 | /// 仅用于Net Core的注册方法
15 | ///
16 | public static class EMailExtensions
17 | {
18 | ///
19 | /// 注册邮件发送相关的服务,注意此方法仅适用于全系统就一种的情况
20 | /// 注意此方法不会注册依赖的
21 | ///
22 | ///
23 | /// 仅包含的配置节点
24 | /// 仅包含的配置节点
25 | ///
26 | public static IServiceCollection AddSingletonForEMailSender(this IServiceCollection services, IConfiguration emailSetting, IConfiguration emailMimeMessageSetting)
27 | {
28 | services.Configure(emailMimeMessageSetting);
29 | services.AddSingletonForEMailHelper(emailSetting);
30 | services.AddSingleton();
31 | return services;
32 | }
33 |
34 | ///
35 | /// 注册邮件发送辅助类的相关实现,注意此方法不注册
36 | ///
37 | ///
38 | /// 仅包含的配置节点
39 | ///
40 | public static IServiceCollection AddSingletonForEMailHelper(this IServiceCollection services, IConfiguration configuration)
41 | {
42 | services.Configure(configuration);
43 | services.AddSingleton();
44 | return services;
45 | }
46 | }
47 | }
48 | #endif
49 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Sender.EMail/EMailHelper.cs:
--------------------------------------------------------------------------------
1 | using MailKit.Net.Smtp;
2 | using MimeKit;
3 | using MimeKit.Text;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace CheckCodeHelper.Sender.EMail
11 | {
12 | #if NETSTANDARD2_0_OR_GREATER
13 | using Microsoft.Extensions.Options;
14 | #endif
15 | ///
16 | /// 发送邮件辅助类
17 | ///
18 | public class EMailHelper
19 | {
20 | ///
21 | /// 邮箱配置
22 | ///
23 | public EMailSetting Setting { get; }
24 |
25 | #if NETSTANDARD2_0_OR_GREATER
26 | ///
27 | /// 邮件发送设置
28 | ///
29 | ///
30 | public EMailHelper(IOptions option)
31 | : this(option.Value)
32 | {
33 | }
34 | #endif
35 |
36 | ///
37 | /// 邮件发送设置
38 | ///
39 | ///
40 | #if NETSTANDARD2_0_OR_GREATER
41 | private
42 | #else
43 | public
44 | #endif
45 | EMailHelper(EMailSetting setting)
46 | {
47 | this.Setting = setting ?? throw new ArgumentNullException(nameof(setting));
48 | }
49 | ///
50 | /// 发送电子邮件,默认发送方为
51 | ///
52 | /// 邮件主题
53 | /// 邮件内容主题
54 | /// 接收方信息
55 | /// 内容主题模式,默认TextFormat.Text
56 | /// 附件
57 | /// 抄送方信息
58 | /// 密送方信息
59 | /// 是否自动释放附件所用Stream
60 | ///
61 | public async Task SendEMailAsync(string subject, string content, IEnumerable toAddress, TextFormat textFormat = TextFormat.Text, IEnumerable attachments = null, IEnumerable ccAddress = null, IEnumerable bccAddress = null, bool dispose = true)
62 | {
63 | await SendEMailAsync(subject, content, new MailboxAddress[] { new MailboxAddress(this.Setting.UserName, this.Setting.UserAddress) }, toAddress, textFormat, attachments, ccAddress, bccAddress, dispose).ConfigureAwait(false);
64 | }
65 |
66 | ///
67 | /// 发送电子邮件
68 | ///
69 | /// 邮件主题
70 | /// 邮件内容主题
71 | /// 发送方信息
72 | /// 接收方信息
73 | /// 内容主题模式,默认TextFormat.Text
74 | /// 附件
75 | /// 抄送方信息
76 | /// 密送方信息
77 | /// 是否自动释放附件所用Stream
78 | ///
79 | public async Task SendEMailAsync(string subject, string content, MailboxAddress fromAddress, IEnumerable toAddress, TextFormat textFormat = TextFormat.Text, IEnumerable attachments = null, IEnumerable ccAddress = null, IEnumerable bccAddress = null, bool dispose = true)
80 | {
81 | await SendEMailAsync(subject, content, new MailboxAddress[] { fromAddress }, toAddress, textFormat, attachments, ccAddress, bccAddress, dispose).ConfigureAwait(false);
82 | }
83 |
84 | ///
85 | /// 发送电子邮件
86 | ///
87 | /// 邮件主题
88 | /// 邮件内容主题
89 | /// 发送方信息
90 | /// 接收方信息
91 | /// 内容主题模式,默认TextFormat.Text
92 | /// 附件
93 | /// 抄送方信息
94 | /// 密送方信息
95 | /// 是否自动释放附件所用Stream
96 | ///
97 | public async Task SendEMailAsync(string subject, string content, IEnumerable fromAddress, IEnumerable toAddress, TextFormat textFormat = TextFormat.Text, IEnumerable attachments = null, IEnumerable ccAddress = null, IEnumerable bccAddress = null, bool dispose = true)
98 | {
99 | var message = new MimeMessage();
100 | message.From.AddRange(fromAddress);
101 | message.To.AddRange(toAddress);
102 | if (!this.IsEmpty(ccAddress))
103 | {
104 | message.Cc.AddRange(ccAddress);
105 | }
106 | if (!this.IsEmpty(bccAddress))
107 | {
108 | message.Bcc.AddRange(bccAddress);
109 | }
110 | message.Subject = subject;
111 | var body = new TextPart(textFormat)
112 | {
113 | Text = content
114 | };
115 | message.Body = this.GetMimeEntity(body, attachments);
116 | message.Date = DateTime.Now;
117 | using (var client = new SmtpClient())
118 | {
119 | //创建连接
120 | await this.SmtpClientSetting(client).ConfigureAwait(false);
121 | await client.SendAsync(message).ConfigureAwait(false);
122 | await client.DisconnectAsync(true).ConfigureAwait(false);
123 | if (dispose && attachments != null)
124 | {
125 | foreach (var att in attachments)
126 | {
127 | att.Dispose();
128 | }
129 | }
130 | }
131 | }
132 | ///
133 | /// SmtpClient连接配置
134 | ///
135 | ///
136 | ///
137 | protected virtual async Task SmtpClientSetting(SmtpClient client)
138 | {
139 | await client.ConnectAsync(this.Setting.Host, this.Setting.Port, this.Setting.UseSsl).ConfigureAwait(false);
140 | await client.AuthenticateAsync(this.Setting.UserAddress, this.Setting.Password).ConfigureAwait(false);
141 | }
142 | private string ConvertToBase64(string inputStr, Encoding encoding)
143 | {
144 | return Convert.ToBase64String(encoding.GetBytes(inputStr));
145 | }
146 | private string ConvertHeaderToBase64(string inputStr, Encoding encoding)
147 | {//https://www.cnblogs.com/qingspace/p/3732677.html
148 | var encode = !string.IsNullOrEmpty(inputStr) && inputStr.Any(c => c > 127);
149 | if (encode)
150 | {
151 | return "=?" + encoding.WebName + "?B?" + ConvertToBase64(inputStr, encoding) + "?=";
152 | }
153 | return inputStr;
154 | }
155 | private bool IsEmpty(IEnumerable source)
156 | {
157 | return source == null || !source.Any();
158 | }
159 | private MimeEntity GetMimeEntity(MimePart body, IEnumerable attachments)
160 | {
161 | MimeEntity entity = body;
162 | if (!this.IsEmpty(attachments))
163 | {
164 | var mult = new Multipart("mixed")
165 | {
166 | body
167 | };
168 | foreach (var att in attachments)
169 | {
170 | if (att.Stream != null)
171 | {
172 | var attachment = string.IsNullOrWhiteSpace(att.ContentType) ? new MimePart() : new MimePart(att.ContentType);
173 | attachment.Content = new MimeContent(att.Stream);
174 | attachment.ContentDisposition = new ContentDisposition(ContentDisposition.Attachment);
175 | attachment.ContentTransferEncoding = att.ContentTransferEncoding;
176 | attachment.FileName = ConvertHeaderToBase64(att.FileName, Encoding.UTF8);//解决附件中文名问题
177 | mult.Add(attachment);
178 | }
179 | }
180 | entity = mult;
181 | }
182 | return entity;
183 | }
184 | }
185 | }
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Sender.EMail/EMailMimeMessageSetting.cs:
--------------------------------------------------------------------------------
1 | using MimeKit.Text;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace CheckCodeHelper.Sender.EMail
9 | {
10 | ///
11 | /// 邮件MIME Message配置
12 | ///
13 | public class EMailMimeMessageSetting
14 | {
15 | ///
16 | /// 默认的邮件内容格式,如果不设置默认为
17 | ///
18 | public TextFormat DefaultTextFormat { get; set; }
19 | ///
20 | /// 邮件配置 Key为bizFlag
21 | ///
22 | public IDictionary Parameters { get; set; }
23 | ///
24 | /// 邮件参数
25 | ///
26 | public class MimeMessageParameter
27 | {
28 | ///
29 | /// 邮件主题
30 | ///
31 | public string Subject { get; set; }
32 | ///
33 | /// 邮件内容的格式
34 | ///
35 | public TextFormat? TextFormat { get; set; }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Sender.EMail/EMailSender.cs:
--------------------------------------------------------------------------------
1 | #if NETSTANDARD2_0_OR_GREATER
2 | using Microsoft.Extensions.Options;
3 | #endif
4 | using MimeKit;
5 | using MimeKit.Text;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Text;
10 | using System.Text.RegularExpressions;
11 | using System.Threading.Tasks;
12 | using static CheckCodeHelper.Sender.EMail.EMailMimeMessageSetting;
13 |
14 | namespace CheckCodeHelper.Sender.EMail
15 | {
16 | ///
17 | /// 通过邮件发送验证码
18 | ///
19 | public class EMailSender : ICodeSender
20 | {
21 | ///
22 | /// 默认设置的
23 | ///
24 | public const string DefaultKey = "EMAIL";
25 | private readonly EMailMimeMessageSetting mimeMessageSetting;
26 |
27 | #if NETSTANDARD2_0_OR_GREATER
28 | ///
29 | /// 通过邮件发送验证码
30 | ///
31 | /// 验证码内容模板
32 | /// 邮件发送者
33 | /// 邮件主题配置
34 | public EMailSender(IContentFormatter formatter, EMailHelper helper, IOptions mimeMessageSetting)
35 | {
36 | this.Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
37 | this.EMailHelper = helper ?? throw new ArgumentNullException(nameof(helper));
38 | this.mimeMessageSetting = mimeMessageSetting.Value ?? throw new ArgumentNullException(nameof(mimeMessageSetting));
39 | if (this.mimeMessageSetting.Parameters == null || this.mimeMessageSetting.Parameters.Count == 0)
40 | {
41 | throw new ArgumentException(nameof(this.mimeMessageSetting.Parameters));
42 | }
43 | }
44 | #else
45 | ///
46 | /// 通过邮件发送验证码
47 | ///
48 | /// 验证码内容模板
49 | /// 邮箱配置
50 | /// 邮件主题配置
51 | public EMailSender(IContentFormatter formatter, EMailSetting emailSetting, EMailMimeMessageSetting subjectSetting)
52 | {
53 | this.Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
54 | this.mimeMessageSetting = subjectSetting ?? throw new ArgumentNullException(nameof(subjectSetting));
55 | this.EMailHelper = new EMailHelper(emailSetting ?? throw new ArgumentNullException(nameof(emailSetting)));
56 | if (this.mimeMessageSetting.Parameters == null || this.mimeMessageSetting.Parameters.Count == 0)
57 | {
58 | throw new ArgumentException(nameof(this.mimeMessageSetting.Parameters));
59 | }
60 | }
61 | #endif
62 | ///
63 | /// 发送验证码内容模板
64 | ///
65 | public IContentFormatter Formatter { get; }
66 | ///
67 | /// 用于标志当前sender的唯一Key
68 | ///
69 | public string Key { get; set; } = DefaultKey;
70 | ///
71 | /// 邮件发送者
72 | ///
73 | public EMailHelper EMailHelper { get; }
74 | ///
75 | /// 判断接收者是否符合发送条件
76 | ///
77 | ///
78 | ///
79 | public virtual bool IsSupport(string receiver)
80 | {
81 | return !string.IsNullOrWhiteSpace(receiver)
82 | && Regex.IsMatch(receiver, @"^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$");
83 | }
84 | ///
85 | /// 发送校验码信息
86 | ///
87 | /// 接收方
88 | /// 业务标志
89 | /// 校验码
90 | /// 校验码有效时间范围
91 | ///
92 | public virtual async Task SendAsync(string receiver, string bizFlag, string code, TimeSpan effectiveTime)
93 | {
94 | var parameter = this.GetParameter(bizFlag);
95 | var content = this.Formatter.GetContent(receiver, bizFlag, code, effectiveTime, this.Key);
96 | await this.EMailHelper.SendEMailAsync(parameter.Subject, content, new List {
97 | new MailboxAddress(receiver)
98 | }, this.GetTextFormat(parameter)).ConfigureAwait(false);
99 | return true;
100 | }
101 |
102 | private MimeMessageParameter GetParameter(string bizFlag)
103 | {
104 | if (!this.mimeMessageSetting.Parameters.ContainsKey(bizFlag))
105 | {
106 | throw new KeyNotFoundException($"The parameter for '{bizFlag}' is not found");
107 | }
108 | return this.mimeMessageSetting.Parameters[bizFlag];
109 | }
110 |
111 | private TextFormat GetTextFormat(MimeMessageParameter parameter)
112 | {
113 | var textFormat = parameter.TextFormat;
114 | if (!textFormat.HasValue)
115 | {
116 | textFormat = this.mimeMessageSetting.DefaultTextFormat;
117 | }
118 | return textFormat.Value;
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Sender.EMail/EMailSetting.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace CheckCodeHelper.Sender.EMail
8 | {
9 | ///
10 | /// 邮箱配置
11 | ///
12 | public class EMailSetting
13 | {
14 | ///
15 | /// 邮件服务器Host
16 | ///
17 | public string Host { get; set; }
18 | ///
19 | /// 邮件服务器Port
20 | ///
21 | public int Port { get; set; }
22 | ///
23 | /// 邮件服务器是否是ssl
24 | ///
25 | public bool UseSsl { get; set; }
26 | ///
27 | /// 发送邮件的账号友善名称
28 | ///
29 | public string UserName { get; set; }
30 | ///
31 | /// 发送邮件的账号地址
32 | ///
33 | public string UserAddress { get; set; }
34 | ///
35 | /// 发送邮件所需的账号密码
36 | ///
37 | public string Password { get; set; }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Sender.Sms/CheckCodeHelper.Sender.Sms.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net452;netstandard2.0
5 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml
6 | dong fang
7 | 通用验证码发送及校验类库--通过短信发送校验信息
8 | https://github.com/fdstar/CheckCodeHelper
9 | https://mit-license.org/
10 | false
11 | 1.0.1
12 | MIT
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/CheckCodeHelper.Sender.Sms/EmaySms.cs:
--------------------------------------------------------------------------------
1 | using CheckCodeHelper.Sender.Sms.Utils;
2 | using Newtonsoft.Json;
3 | using RestSharp;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Security.Cryptography;
8 | using System.Text;
9 | using System.Threading.Tasks;
10 |
11 | namespace CheckCodeHelper.Sender.Sms
12 | {
13 | #if NETSTANDARD2_0_OR_GREATER
14 | using Microsoft.Extensions.Options;
15 | #endif
16 | ///
17 | /// 亿美短信 http://www.emay.cn/
18 | ///
19 | public class EmaySms : ISms
20 | {
21 | private readonly string _appId;
22 | private readonly byte[] _secretKey;
23 | private const string SendSingleSmsUrl = "/inter/sendSingleSMS";
24 | private const string SendBatchSmsUrl = "/inter/sendBatchSMS";
25 | ///
26 | /// 默认Key值
27 | ///
28 | public const string DefaultKey = "Emay";
29 |
30 | #if NETSTANDARD2_0_OR_GREATER
31 | ///
32 | /// 亿美短信 http://www.emay.cn/
33 | ///
34 | ///
35 | public EmaySms(IOptions option)
36 | : this(option.Value)
37 | {
38 | }
39 | #endif
40 |
41 | ///
42 | /// 亿美短信 http://www.emay.cn/
43 | ///
44 | ///
45 | #if NET45_OR_GREATER
46 | public
47 | #else
48 | private
49 | #endif
50 | EmaySms(EmaySetting setting)
51 | {
52 | if (setting == null || string.IsNullOrWhiteSpace(setting.Host) || string.IsNullOrWhiteSpace(setting.AppId) || string.IsNullOrWhiteSpace(setting.SecretKey))
53 | {
54 | throw new ArgumentNullException(nameof(setting));
55 | }
56 | this._appId = setting.AppId;
57 | var key = new byte[16];
58 | Array.Copy(Encoding.UTF8.GetBytes(setting.SecretKey.PadRight(key.Length)), key, key.Length);
59 | this._secretKey = key;
60 | this.Client = new RestClient(setting.Host);
61 | }
62 | ///
63 | /// IRestClient
64 | ///
65 | public IRestClient Client { get; }
66 | ///
67 | /// 请求有效期(秒),默认60
68 | ///
69 | public int ValidPeriod { get; set; } = 60;
70 | ///
71 | /// 请求时是否需要Gzip压缩,默认true
72 | ///
73 | public bool UseGzip { get; set; } = true;
74 | ///
75 | /// 用于区分唯一Key值,默认
76 | ///
77 | public string Key { get; set; } = DefaultKey;
78 |
79 | private IRestRequest GetRestRequest(object data, string url,bool useGZip)
80 | {
81 | var str = JsonConvert.SerializeObject(data);
82 | var request = new RestRequest(url, Method.POST);
83 | request.AddHeader("appId", this._appId);
84 | var rawData = Encoding.UTF8.GetBytes(str);
85 | if (useGZip)
86 | {
87 | request.AddHeader("gzip", "on");
88 | rawData = GZipHelper.Compress(rawData);
89 | }
90 | var encryptData = AesHelper.Encrypt(rawData, this._secretKey, null, CipherMode.ECB, PaddingMode.PKCS7);
91 | request.AddParameter("", encryptData, ParameterType.RequestBody);
92 | return request;
93 | }
94 | private object GetSingleSmsObj(string mobile, string content, string bizId, DateTime? sendTime)
95 | {
96 | var data = new
97 | {
98 | mobile,
99 | content,
100 | timerTime = sendTime.HasValue ? sendTime.Value.ToString("yyyy-MM-dd HH:mm:ss") : string.Empty,
101 | customSmsId = bizId,
102 | requestTime = DateTime.Now.Ticks,
103 | requestValidPeriod = this.ValidPeriod
104 | };
105 | return data;
106 | }
107 | private bool IsResponseSuccess(IRestResponse response, bool useGZip)
108 | {
109 | bool isSuccess = response.Headers.FirstOrDefault(p => p.Name == "result")?.Value.ToString() == "SUCCESS";
110 | #if DEBUG
111 | var responseStr = this.GetResponseContent(response, useGZip);
112 | #endif
113 | return isSuccess;
114 | }
115 | private string GetResponseContent(IRestResponse response, bool useGZip)
116 | {
117 | var data = AesHelper.Decrypt(response.RawBytes, this._secretKey, null, CipherMode.ECB, PaddingMode.PKCS7);
118 | if (useGZip)
119 | {
120 | data = GZipHelper.Decompress(data);
121 | }
122 | return Encoding.UTF8.GetString(data);
123 | }
124 | ///
125 | /// 调用亿美Api
126 | ///
127 | ///
128 | ///
129 | ///
130 | protected bool CallApi(string url, Func