yangle
2024-12-30 7f80be364af2ba22f8cf69b5e7f2ed771297ba2e
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Web;
 
namespace Top.Api.Util
{
    /// <summary>
    /// SPI请求校验结果。
    /// </summary>
    public class CheckResult
    {
        public bool Success { get; set; }
 
        public string Body { get; set; }
    }
 
    /// <summary>
    /// SPI服务提供方工具类。
    /// </summary>
    public class SpiUtils
    {
        private const string TOP_SIGN_LIST = "top-sign-list";
        private static readonly string[] HEADER_FIELDS_IP = {"X-Real-IP", "X-Forwarded-For", "Proxy-Client-IP",
        "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"};
 
        /// <summary>
        /// 校验SPI请求签名,不支持带上传文件的HTTP请求。
        /// </summary>
        /// <param name="request">HttpRequest对象实例</param>
        /// <param name="secret">APP密钥</param>
        /// <returns>校验结果</returns>
        public static CheckResult CheckSign(HttpRequest request, string secret)
        {
            CheckResult result = new CheckResult();
            string ctype = request.ContentType;
            if (ctype.StartsWith(Constants.CTYPE_APP_JSON) || ctype.StartsWith(Constants.CTYPE_TEXT_XML) || ctype.StartsWith(Constants.CTYPE_TEXT_PLAIN) || ctype.StartsWith(Constants.CTYPE_APPLICATION_XML))
            {
                result.Body = GetStreamAsString(request, GetRequestCharset(ctype));
                result.Success = CheckSignInternal(request, result.Body, secret);
            }
            else if (ctype.StartsWith(Constants.CTYPE_FORM_DATA))
            {
                result.Success = CheckSignInternal(request, null, secret);
            }
            else
            {
                throw new TopException("Unspported SPI request");
            }
            return result;
        }
 
        /// <summary>
        /// 校验SPI请求签名,适用于Content-Type为application/x-www-form-urlencoded或multipart/form-data的GET或POST请求。
        /// </summary>
        /// <param name="request">请求对象</param>
        /// <param name="secret">app对应的secret</param>
        /// <returns>true:校验通过;false:校验不通过</returns>
        public static bool CheckSign4FormRequest(HttpRequest request, string secret)
        {
            return CheckSignInternal(request, null, secret);
        }
 
        /// <summary>
        /// 校验SPI请求签名,适用于Content-Type为text/xml或text/json的POST请求。
        /// </summary>
        /// <param name="request">请求对象</param>
        /// <param name="body">请求体的文本内容</param>
        /// <param name="secret">app对应的secret</param>
        /// <returns>true:校验通过;false:校验不通过</returns>
        public static bool CheckSign4TextRequest(HttpRequest request, string body, string secret)
        {
            return CheckSignInternal(request, body, secret);
        }
 
        private static bool CheckSignInternal(HttpRequest request, string body, string secret)
        {
            IDictionary<string, string> parameters = new SortedDictionary<string, string>(StringComparer.Ordinal);
            string charset = GetRequestCharset(request.ContentType);
 
            // 1. 获取header参数
            AddAll(parameters, GetHeaderMap(request, charset));
 
            // 2. 获取url参数
            Dictionary<string, string> queryMap = GetQueryMap(request, charset);
            AddAll(parameters, queryMap);
 
            // 3. 获取form参数
            AddAll(parameters, GetFormMap(request));
 
            // 4. 生成签名并校验
            string remoteSign = null;
            if (queryMap.ContainsKey(Constants.SIGN))
            {
                remoteSign = queryMap[Constants.SIGN];
            }
            string localSign = Sign(parameters, body, secret, charset);
            return localSign.Equals(remoteSign);
        }
 
        private static void AddAll(IDictionary<string, string> dest, IDictionary<string, string> from)
        {
            if (from != null && from.Count > 0)
            {
                IEnumerator<KeyValuePair<string, string>> em = from.GetEnumerator();
                while (em.MoveNext())
                {
                    KeyValuePair<string, string> kvp = em.Current;
                    dest.Add(kvp.Key, kvp.Value);
                }
            }
        }
 
        /// <summary>
        /// 签名规则:hex(md5(secret+sorted(header_params+url_params+form_params)+body)+secret)
        /// </summary>
        private static string Sign(IDictionary<string, string> parameters, string body, string secret, string charset)
        {
            IEnumerator<KeyValuePair<string, string>> em = parameters.GetEnumerator();
 
            // 第1步:把所有参数名和参数值串在一起
            StringBuilder query = new StringBuilder(secret);
            while (em.MoveNext())
            {
                string key = em.Current.Key;
                if (!Constants.SIGN.Equals(key))
                {
                    string value = em.Current.Value;
                    query.Append(key).Append(value);
                }
            }
            if (body != null)
            {
                query.Append(body);
            }
 
            query.Append(secret);
 
            // 第2步:使用MD5加密
            MD5 md5 = MD5.Create();
            byte[] bytes = md5.ComputeHash(Encoding.GetEncoding(charset).GetBytes(query.ToString()));
 
            // 第3步:把二进制转化为大写的十六进制
            StringBuilder result = new StringBuilder();
            for (int i = 0; i < bytes.Length; i++)
            {
                result.Append(bytes[i].ToString("X2"));
            }
 
            return result.ToString();
        }
 
        private static string GetRequestCharset(string ctype)
        {
            string charset = "utf-8";
            if (!string.IsNullOrEmpty(ctype))
            {
                string[] entires = ctype.Split(';');
                foreach (string entry in entires)
                {
                    string _entry = entry.Trim();
                    if (_entry.StartsWith("charset"))
                    {
                        string[] pair = _entry.Split('=');
                        if (pair.Length == 2)
                        {
                            if (!string.IsNullOrEmpty(pair[1]))
                            {
                                charset = pair[1].Trim();
                            }
                        }
                        break;
                    }
                }
            }
            return charset;
        }
 
        public static Dictionary<string, string> GetHeaderMap(HttpRequest request, string charset)
        {
            Dictionary<string, string> headerMap = new Dictionary<string, string>();
            string signList = request.Headers[TOP_SIGN_LIST];
            if (!string.IsNullOrEmpty(signList))
            {
                string[] keys = signList.Split(',');
                foreach (string key in keys)
                {
                    string value = request.Headers[key];
                    if (string.IsNullOrEmpty(value))
                    {
                        headerMap.Add(key, "");
                    }
                    else
                    {
                        headerMap.Add(key, HttpUtility.UrlDecode(value, Encoding.GetEncoding(charset)));
                    }
                }
            }
            return headerMap;
        }
 
        public static Dictionary<string, string> GetQueryMap(HttpRequest request, string charset)
        {
            Dictionary<string, string> queryMap = new Dictionary<string, string>();
            string queryString = request.Url.Query;
            if (!string.IsNullOrEmpty(queryString))
            {
                queryString = queryString.Substring(1); // 忽略?号
                string[] parameters = queryString.Split('&');
                foreach (string parameter in parameters)
                {
                    string[] kv = parameter.Split('=');
                    if (kv.Length == 2)
                    {
                        string key = HttpUtility.UrlDecode(kv[0], Encoding.GetEncoding(charset));
                        string value = HttpUtility.UrlDecode(kv[1], Encoding.GetEncoding(charset));
                        queryMap.Add(key, value);
                    }
                    else if (kv.Length == 1)
                    {
                        string key = HttpUtility.UrlDecode(kv[0], Encoding.GetEncoding(charset));
                        queryMap.Add(key, "");
                    }
                }
            }
            return queryMap;
        }
 
        public static Dictionary<string, string> GetFormMap(HttpRequest request)
        {
            Dictionary<string, string> formMap = new Dictionary<string, string>();
            NameValueCollection form = request.Form;
            string[] keys = form.AllKeys;
            foreach (string key in keys)
            {
                string value = request.Form[key];
                if (string.IsNullOrEmpty(value))
                {
                    formMap.Add(key, "");
                }
                else
                {
                    formMap.Add(key, value);
                }
            }
            return formMap;
        }
 
        public static string GetStreamAsString(HttpRequest request, string charset)
        {
            Stream stream = null;
            StreamReader reader = null;
 
            try
            {
                // 以字符流的方式读取HTTP请求体
                stream = request.InputStream;
                reader = new StreamReader(stream, Encoding.GetEncoding(charset));
                return reader.ReadToEnd();
            }
            finally
            {
                // 释放资源
                if (reader != null) reader.Close();
                if (stream != null) stream.Close();
            }
        }
 
        /// <summary>
        /// 检查SPI请求到达服务器端是否已经超过指定的分钟数,如果超过则拒绝请求。
        /// </summary>
        /// <returns>true代表不超过,false代表超过。</returns>
        public static bool CheckTimestamp(HttpRequest request, int minutes)
        {
            string ts = request.QueryString[Constants.TIMESTAMP];
            if (!string.IsNullOrEmpty(ts))
            {
                DateTime remote = DateTime.ParseExact(ts, Constants.DATE_TIME_FORMAT, null);
                DateTime local = DateTime.Now;
                return remote.AddMinutes(minutes).CompareTo(local) > 0;
            }
            else
            {
                return false;
            }
        }
 
        /// <summary>
        /// 检查发起SPI请求的来源IP是否是TOP机房的出口IP。
        /// </summary>
        /// <param name="request">HTTP请求对象</param>
        /// <param name="topIpList">TOP网关IP出口地址段列表,通过taobao.top.ipout.get获得</param>
        /// <returns>true表达IP来源合法,false代表IP来源不合法</returns>
        public static bool CheckRemoteIp(HttpRequest request, List<string> topIpList)
        {
            string ip = request.UserHostAddress;
            foreach (string ipHeader in HEADER_FIELDS_IP)
            {
                string realIp = request.Headers[ipHeader];
                if (!string.IsNullOrEmpty(realIp) && !"unknown".Equals(realIp))
                {
                    ip = realIp;
                    break;
                }
            }
 
            if (topIpList != null)
            {
                foreach (string topIp in topIpList)
                {
                    if (StringUtil.IsIpInRange(ip, topIp))
                    {
                        return true;
                    }
                }
            }
            return false;
        }
    }
}