zhaolei
7 days ago 921de2254ff5712a44ed8575ee8efe34252f6603
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
#if !NETSTANDARD2_0
using System.Text.Json;
#else
using Newtonsoft.Json;
#endif
using System.Threading;
using System.Threading.Tasks;
 
namespace Prow.Web.SMS.Tencent
{
    /// <summary>
    /// 腾讯云短信平台接口
    /// </summary>
    public class TencentSMSProvider : ISMSProvider
    {
        /// <summary>
        /// 
        /// </summary>
        public SMSOptions Options { get { return _options; } }
 
        private readonly HttpClient _client;
        private readonly TencentSMSOptions _options;
        private readonly Random _random;
 
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="configuration"></param>
        /// <param name="factory"></param>
        public TencentSMSProvider(IConfiguration configuration, IHttpClientFactory factory)
        {
            _options = configuration.GetSection(nameof(TencentSMSOptions)).Get<TencentSMSOptions>();
            Options.RequestUrl = "https://yun.tim.qq.com/v5/tlssmssvr/sendsms";
            _client = factory.CreateClient();
            _random = new Random();
        }
 
        private static readonly ConcurrentDictionary<string, AutoExpireValidateCode> _pool = new ConcurrentDictionary<string, AutoExpireValidateCode>();
        /// <summary>
        /// 手机下发验证码方法
        /// </summary>
        /// <param name="phoneNumber"></param>
        /// <returns></returns>
        public async Task<SMSResult> SendCodeAsync(string phoneNumber)
        {
            // post https://yun.tim.qq.com/v5/tlssmssvr/sendsms?sdkappid=xxxxx&random=xxxx
            Options.Timestamp = (DateTimeOffset.UtcNow.Ticks - 621355968000000000) / 10000000;
            Options.Phone = phoneNumber;
            var requestParameters = new Dictionary<string, string>()
            {
                { "sdkappid", _options.AppId },
                { "random", Options.Timestamp.ToString() }
            };
 
            var url = QueryHelpers.AddQueryString(Options.RequestUrl, requestParameters);
            var postData = new TencentSendData()
            {
                Sig = Sign(),
                Sign = _options.Sign,
                Time = Options.Timestamp,
                Tel = new TencentPhone() { Mobile = Options.Phone },
                Tpl_id = _options.TplId
            };
            var code = _random.Next(1000, 9999).ToString();
            postData.Params.Add(code);
            postData.Params.Add(Options.Expires.Minutes.ToString());
 
            var result = _options.Debug ? await Task.FromResult(new TencenResponse() { Result = 0 }) : await RequestSendCodeUrl(url, postData);
            var ret = new SMSResult() { Result = result.Result == 0, Msg = result.Errmsg };
 
            // debug 模式下发验证码到客户端
            if (_options.Debug) ret.Data = code;
            if (ret.Result)
            {
                _pool.AddOrUpdate(Options.Phone, key => new AutoExpireValidateCode(Options.Phone, code, Options.Expires, phone => _pool.TryRemove(phone, out var _)), (key, v) => v.Reset(code));
            }
            return ret;
        }
 
        private async Task<TencenResponse> RequestSendCodeUrl(string url, TencentSendData postData)
        {
#if !NETSTANDARD2_0
            var req = await _client.PostAsJsonAsync(url, postData, CancellationToken.None, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
            var content = await req.Content.ReadAsStringAsync();
            var result = JsonSerializer.Deserialize<TencenResponse>(content, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
#else
            var req = await _client.PostAsJsonAsync(url, postData);
            var content = await req.Content.ReadAsStringAsync();
            var result = JsonConvert.DeserializeObject<TencenResponse>(content);
#endif
            if (result.Result != 0)
            {
                new Exception(result.Errmsg).Log(new NameValueCollection()
                {
                    ["UserId"] = Options.Phone,
                    ["url"] = url,
                    ["content"] = content
                });
            }
            return result;
        }
 
        private string Sign()
        {
            return Hash($"appkey={_options.AppKey}&random={Options.Timestamp}&time={Options.Timestamp}&mobile={Options.Phone}");
        }
 
        private static string Hash(string data)
        {
            using var algo = SHA256.Create();
            var sign = BitConverter.ToString(algo.ComputeHash(Encoding.UTF8.GetBytes(data)));
            sign = sign.Replace("-", "").ToLowerInvariant();
            return sign;
        }
 
        /// <summary>
        /// 验证手机验证码是否正确方法
        /// </summary>
        /// <param name="phoneNumber"></param>
        /// <param name="code"></param>
        /// <returns></returns>
        public bool Validate(string phoneNumber, string code) => _pool.TryGetValue(phoneNumber, out var signKey) && code == signKey.Code;
 
        /// <summary>
        /// 文档 https://cloud.tencent.com/document/product/382/5976
        /// </summary>
        private class TencentSendData
        {
            public string Ext { get; set; } = "";
 
            public string Extend { get; set; } = "";
 
            public ICollection<string> Params { get; } = new HashSet<string>();
 
            public string Sig { get; set; } = "";
 
            public string Sign { get; set; } = "";
 
            public TencentPhone Tel { get; set; } = new TencentPhone();
 
            public long Time { get; set; }
 
            public int Tpl_id { get; set; }
        }
 
        private class TencentPhone
        {
            public string Mobile { get; set; } = "";
 
            public string Nationcode { get; set; } = "86";
        }
 
        private class TencenResponse
        {
            public int Result { get; set; } = -1;
 
            public string Errmsg { get; set; } = "";
 
            public string Ext { get; set; } = "";
 
            public int Fee { get; set; }
 
            public string Sid { get; set; } = "";
        }
    }
}