1. 程式人生 > >ABP框架中簡訊傳送處理,包括阿里雲簡訊和普通簡訊商的簡訊傳送整合

ABP框架中簡訊傳送處理,包括阿里雲簡訊和普通簡訊商的簡訊傳送整合

在一般的系統中,往往也有簡訊模組的需求,如動態密碼的登入,系統密碼的找回,以及為了獲取使用者手機號碼的簡訊確認等等,在ABP框架中,本身提供了對郵件、簡訊的基礎支援,那麼只需要根據自己的情況實現對應的介面即可。本篇隨筆介紹ABP框架中簡訊傳送處理,包括阿里雲簡訊和普通簡訊商的簡訊傳送整合。

1、基於第三方阿里雲簡訊的實現

阿里雲簡訊的實現,GitHub上也有一些人實現了一些模組,我們只需要使用對應的模組,然後在Core模組中配置一下依賴即可。

我們一般在做某件事情的時候,先去看看別人是否已經做好了,使用它或者參考它來做事情是個不錯的思路。

基於這個道理,我們可以在VS的Nuget包管理中查詢一下基於ABP的阿里雲簡訊,可以找到一個合適的進行參考。

這個阿里雲的ABP實現適合我們當前的ABP框架版本,因此使用它即可,因此安裝引入對應的類庫在Core專案中。

 

在網站https://github.com/tangyanglai/Sms.Core 我們看到它的使用過程,引入後在專案中啟動模組依賴中新增對應的程式碼即可。

    [DependsOn(typeof(AliyunSmsModule))]

那麼我們在專案中的程式碼如下所示

 

預設支援兩種配置方式,配置檔案和SettingManager。下面以配置檔案為例,格式為:

{
  "AliyunSmsSettings": {
    "AccessKeyId": "",
    "AccessKeySecret": "",
    "SignName": "",           //SendCodeAsync傳送驗證碼使用
    "TemplateCode": "" ,    //SendCodeAsync傳送驗證碼使用
  } 
}

根據上面的說明,我們在Host專案的AppSettings.json中增加對應的阿里雲配置項,如下所示。

 

其中AccessKeyId是標識使用者身份的ID,AccessKeySecret 是祕鑰,SigName是我們申請的簡訊商戶簽名,TemplateCode是我們驗證碼的配置

 

 而簡訊一般是基於某個模板進行傳送的,因此需要確定系統使用的簡訊模板。

 

 阿里雲的傳送模組是使用ISmsTemplateSender進行傳送的,因此在程式碼中使用如下所示。

那麼在使用傳送簡訊驗證碼的地方,如AccountService應用層中,使用的時候使用它的注入介面即可傳送簡訊驗證碼了。

 

 使用傳送簡訊的操作如下所示。

        /// <summary>
        /// 傳送簡訊驗證碼
        /// </summary>
        /// <param name="phone">手機號碼</param>
        /// <param name="code">驗證碼</param>
        /// <returns></returns>
        public async Task<SmsResult> SendCodeAsync(string phone, string code)
        {
            return await _smsTemplateSender.SmsService.SendCodeAsync(phone, code);

        }  
        /// <summary>
        /// 傳送模板訊息
        /// </summary>
        /// <param name="input">模板物件</param>
        /// <returns></returns>
        public async Task<SmsResult> SendTemplateMessageAsync(SendTemplateMessageInput input)
        {
            return await _smsTemplateSender.SmsService.SendTemplateMessageAsync(input);
        }

 

 2、使用自己的阿里雲簡訊傳送封裝

我之前隨筆《使用阿里雲的簡訊服務傳送簡訊》中寫過如何處理阿里雲簡訊,雖然那個是常規.net framework的程式中整合的,不過在.net Core的程式碼都是差不多的。

我們知道ABP框架提供了對應的簡訊傳送介面,一般注入在系統中使用即可。

namespace MyProject.Net
{
    /// <summary>
    /// 簡訊傳送介面
    /// </summary>
    public interface ISmsSender
    {
        Task<CommonResult> SendAsync(string number, string message);
    }
}

那麼我們自己定義的簡訊傳送介面,實現它即可,然後注入使用對應的介面即可。

根據阿里雲介面需求,定義一個類似的模型用作載入引數的。

    /// <summary>
    /// 阿里雲配置引數
    /// </summary>
    internal class AliyunSmsSettting
    {
        public string AccessKeyId { get; set; }
        public string AccessKeySecret { get; set; }
        public string RegionId { get; set; }
        public string EndpointName { get; set; }
        public string Domain { get; set; }
        public string Product { get; set; }
        public string SignName { get; set; }
        public string TemplateCode { get; set; }
        public string TemplateParam { get; set; }
    }

然後讓我們的介面實現函式,初始化的時候獲取對應的配置資訊供使用。

{
    /// <summary>
    /// 使用簡單封裝,不依賴其他外部模組的阿里雲簡訊傳送
    /// </summary>
    public class AliyunSmsSender : IShouldInitialize, ISmsSender, ITransientDependency
    {
        public IConfiguration AppConfiguration { get; set; }
        public IIocManager IocManager { get; set; }
        public ILogger Logger { get; set; }

        private const string Key = "AliyunSmsSettings";
        private const string endpoint = "dysmsapi.aliyuncs.com";

        /// <summary>
        /// 簡訊配置資訊
        /// </summary>
        private AliyunSmsSettting SmsSettings { get; set; }

        public AliyunSmsSender(IConfiguration appConfiguration, IIocManager iocManager)
        {
            this.AppConfiguration = appConfiguration;
            this.IocManager = iocManager;
            this.Logger = NullLogger.Instance;
        }
        public void Initialize()
        {
            this.SmsSettings = GetConfigFromConfigOrSettingsByKey<AliyunSmsSettting>().Result;
        }

然後根據我之前隨筆的實現邏輯,給他實現對應的傳送操作即可,部分關鍵程式碼如下所示

        /// <summary>
        /// 傳送簡訊
        /// </summary>
        /// <param name="number">手機號碼</param>
        /// <param name="message">訊息或驗證碼</param>
        /// <returns></returns>
        public async Task<CommonResult> SendAsync(string number, string message)
        {
            var result = await PrivateSend(number, message);
            return result;
        }

        /// <summary>
        /// 傳送邏輯
        /// </summary>
        /// <returns></returns>
        private async Task<CommonResult> PrivateSend(string number, string code)
        { 
            string nowDate = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'");//GTM時間
            var keyValues = new Dictionary<string, string>();//宣告一個字典
            //1.系統引數
            keyValues.Add("SignatureMethod", "HMAC-SHA1");
            keyValues.Add("SignatureNonce", Guid.NewGuid().ToString());
            keyValues.Add("AccessKeyId", this.SmsSettings.AccessKeyId);
            keyValues.Add("SignatureVersion", "1.0");
            keyValues.Add("Timestamp", nowDate);
            keyValues.Add("Format", "Json");//可換成xml

            //2.業務api引數
            keyValues.Add("Action", "SendSms");
            keyValues.Add("Version", "2017-05-25");
            keyValues.Add("RegionId", "cn-hangzhou");
            keyValues.Add("PhoneNumbers", number);
            keyValues.Add("SignName", this.SmsSettings.SignName);
            keyValues.Add("TemplateCode", this.SmsSettings.TemplateCode);
            keyValues.Add("TemplateParam", string.Format("{{\"code\":\"{0}\"}}", code));
            keyValues.Add("OutId", "123");

            //3.去除簽名關鍵字key
            if (keyValues.ContainsKey("Signature"))
            {
                keyValues.Remove("Signature");
            }

            //4.引數key排序
            Dictionary<string, string> ascDic = keyValues.OrderBy(o => o.Key).ToDictionary(o => o.Key, p => p.Value.ToString());
            //5.構造待簽名的字串
            var builder = new StringBuilder();
            foreach (var item in ascDic)
            {
                if (item.Key == "SignName")
                {
                }
                else
                {
                    builder.Append("&").Append(specialUrlEncode(item.Key)).Append("=").Append(specialUrlEncode(item.Value));
                }
                if (item.Key == "RegionId")
                {
                    builder.Append("&").Append(specialUrlEncode("SignName")).Append("=").Append(specialUrlEncode(keyValues["SignName"]));
                }
            }
            string sorteQueryString = builder.ToString().Substring(1);

            StringBuilder stringToSign = new StringBuilder();
            stringToSign.Append("GET").Append("&");
            stringToSign.Append(specialUrlEncode("/")).Append("&");
            stringToSign.Append(specialUrlEncode(sorteQueryString));

            string Sign = MySign(this.SmsSettings.AccessKeySecret + "&", stringToSign.ToString());
            //6.簽名最後也要做特殊URL編碼
            string signture = specialUrlEncode(Sign);

            //最終打印出合法GET請求的URL
            string url = string.Format("http://{0}/?Signature={1}{2}", endpoint, signture, builder);
            var modal = await GetHtmlResult(url);

           return new CommonResult(modal.Success, modal.Message);            
        }

然後在Core模組中初始化的時候,替換對應的簡訊傳送實現即可。

 

 這樣就可以使用我們自己的簡訊介面了

 

 傳送程式碼如下所示

        /// <summary>
        /// 傳送簡訊驗證碼
        /// </summary>
        /// <param name="phone">手機號碼</param>
        /// <param name="code">驗證碼</param>
        /// <returns></returns>
        public async Task<CommonResult> SendSmsCodeAsync(string phone, string code)
        {
            return await _smsSender.SendAsync(phone, code); //使用阿里雲介面
        }

 

3、普通簡訊商的簡訊傳送整合

還有一種我們可能不是基於阿里雲,而是其他提供商的介面傳送,操作也是自定義簡訊介面的封裝。

我們使用如下引數來確定簡訊提供商的資訊,也可以根據需要自己調整。

 

定義一個配置對應的配置物件,方便獲取引數資訊。

    /// <summary>
    /// 自定義簡訊配置
    /// </summary>
    internal class MySmsSettings
    {
        /// <summary>
        /// 供應商程式碼
        /// </summary>
        public string spcode { get; set; }
        /// <summary>
        /// 賬戶
        /// </summary>
        public string username { get; set; }
        /// <summary>
        /// 密碼
        /// </summary>
        public string password { get; set; }
    }

由於我們這個的實現也是基於標準介面ISmsSender的,那麼我們實現這個後,也需要特定指定這個實現為ISmsSender的使用。

例如在CoreModule中替換為這個簡訊實現的話,如下程式碼。

   //使用自定義的 ISmsSender
   Configuration.ReplaceService<ISmsSender, MySmsSender>();

使用介面傳送簡訊的時候,就和我們上面的操作類似的了。

&n