SpiUtils.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Collections.Specialized;
  5. using System.IO;
  6. using System.Security.Cryptography;
  7. using System.Text;
  8. using System.Web;
  9. using Microsoft.AspNetCore.Http;
  10. namespace Top.Api.Util
  11. {
  12. /// <summary>
  13. /// SPI请求校验结果。
  14. /// </summary>
  15. public class CheckResult
  16. {
  17. public bool Success { get; set; }
  18. public string Body { get; set; }
  19. }
  20. /// <summary>
  21. /// SPI服务提供方工具类。
  22. /// </summary>
  23. public class SpiUtils
  24. {
  25. private const string TOP_SIGN_LIST = "top-sign-list";
  26. private static readonly string[] HEADER_FIELDS_IP = {"X-Real-IP", "X-Forwarded-For", "Proxy-Client-IP",
  27. "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"};
  28. /// <summary>
  29. /// 校验SPI请求签名,不支持带上传文件的HTTP请求。
  30. /// </summary>
  31. /// <param name="request">HttpRequest对象实例</param>
  32. /// <param name="secret">APP密钥</param>
  33. /// <returns>校验结果</returns>
  34. public static CheckResult CheckSign(HttpRequest request, string secret)
  35. {
  36. CheckResult result = new CheckResult();
  37. string ctype = request.ContentType;
  38. 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))
  39. {
  40. result.Body = GetStreamAsString(request, GetRequestCharset(ctype));
  41. result.Success = CheckSignInternal(request, result.Body, secret);
  42. }
  43. else if (ctype.StartsWith(Constants.CTYPE_FORM_DATA))
  44. {
  45. result.Success = CheckSignInternal(request, null, secret);
  46. }
  47. else
  48. {
  49. throw new TopException("Unspported SPI request");
  50. }
  51. return result;
  52. }
  53. /// <summary>
  54. /// 校验SPI请求签名,适用于Content-Type为application/x-www-form-urlencoded或multipart/form-data的GET或POST请求。
  55. /// </summary>
  56. /// <param name="request">请求对象</param>
  57. /// <param name="secret">app对应的secret</param>
  58. /// <returns>true:校验通过;false:校验不通过</returns>
  59. public static bool CheckSign4FormRequest(HttpRequest request, string secret)
  60. {
  61. return CheckSignInternal(request, null, secret);
  62. }
  63. /// <summary>
  64. /// 校验SPI请求签名,适用于Content-Type为text/xml或text/json的POST请求。
  65. /// </summary>
  66. /// <param name="request">请求对象</param>
  67. /// <param name="body">请求体的文本内容</param>
  68. /// <param name="secret">app对应的secret</param>
  69. /// <returns>true:校验通过;false:校验不通过</returns>
  70. public static bool CheckSign4TextRequest(HttpRequest request, string body, string secret)
  71. {
  72. return CheckSignInternal(request, body, secret);
  73. }
  74. private static bool CheckSignInternal(HttpRequest request, string body, string secret)
  75. {
  76. IDictionary<string, string> parameters = new SortedDictionary<string, string>(StringComparer.Ordinal);
  77. string charset = GetRequestCharset(request.ContentType);
  78. // 1. 获取header参数
  79. AddAll(parameters, GetHeaderMap(request, charset));
  80. // 2. 获取url参数
  81. Dictionary<string, string> queryMap = GetQueryMap(request, charset);
  82. AddAll(parameters, queryMap);
  83. // 3. 获取form参数
  84. AddAll(parameters, GetFormMap(request));
  85. // 4. 生成签名并校验
  86. string remoteSign = null;
  87. if (queryMap.ContainsKey(Constants.SIGN))
  88. {
  89. remoteSign = queryMap[Constants.SIGN];
  90. }
  91. string localSign = Sign(parameters, body, secret, charset);
  92. return localSign.Equals(remoteSign);
  93. }
  94. private static void AddAll(IDictionary<string, string> dest, IDictionary<string, string> from)
  95. {
  96. if (from != null && from.Count > 0)
  97. {
  98. IEnumerator<KeyValuePair<string, string>> em = from.GetEnumerator();
  99. while (em.MoveNext())
  100. {
  101. KeyValuePair<string, string> kvp = em.Current;
  102. dest.Add(kvp.Key, kvp.Value);
  103. }
  104. }
  105. }
  106. /// <summary>
  107. /// 签名规则:hex(md5(secret+sorted(header_params+url_params+form_params)+body)+secret)
  108. /// </summary>
  109. private static string Sign(IDictionary<string, string> parameters, string body, string secret, string charset)
  110. {
  111. IEnumerator<KeyValuePair<string, string>> em = parameters.GetEnumerator();
  112. // 第1步:把所有参数名和参数值串在一起
  113. StringBuilder query = new StringBuilder(secret);
  114. while (em.MoveNext())
  115. {
  116. string key = em.Current.Key;
  117. if (!Constants.SIGN.Equals(key))
  118. {
  119. string value = em.Current.Value;
  120. query.Append(key).Append(value);
  121. }
  122. }
  123. if (body != null)
  124. {
  125. query.Append(body);
  126. }
  127. query.Append(secret);
  128. // 第2步:使用MD5加密
  129. MD5 md5 = MD5.Create();
  130. byte[] bytes = md5.ComputeHash(Encoding.GetEncoding(charset).GetBytes(query.ToString()));
  131. // 第3步:把二进制转化为大写的十六进制
  132. StringBuilder result = new StringBuilder();
  133. for (int i = 0; i < bytes.Length; i++)
  134. {
  135. result.Append(bytes[i].ToString("X2"));
  136. }
  137. return result.ToString();
  138. }
  139. private static string GetRequestCharset(string ctype)
  140. {
  141. string charset = "utf-8";
  142. if (!string.IsNullOrEmpty(ctype))
  143. {
  144. string[] entires = ctype.Split(';');
  145. foreach (string entry in entires)
  146. {
  147. string _entry = entry.Trim();
  148. if (_entry.StartsWith("charset"))
  149. {
  150. string[] pair = _entry.Split('=');
  151. if (pair.Length == 2)
  152. {
  153. if (!string.IsNullOrEmpty(pair[1]))
  154. {
  155. charset = pair[1].Trim();
  156. }
  157. }
  158. break;
  159. }
  160. }
  161. }
  162. return charset;
  163. }
  164. public static Dictionary<string, string> GetHeaderMap(HttpRequest request, string charset)
  165. {
  166. Dictionary<string, string> headerMap = new Dictionary<string, string>();
  167. string signList = request.Headers[TOP_SIGN_LIST];
  168. if (!string.IsNullOrEmpty(signList))
  169. {
  170. string[] keys = signList.Split(',');
  171. foreach (string key in keys)
  172. {
  173. string value = request.Headers[key];
  174. if (string.IsNullOrEmpty(value))
  175. {
  176. headerMap.Add(key, "");
  177. }
  178. else
  179. {
  180. headerMap.Add(key, HttpUtility.UrlDecode(value, Encoding.GetEncoding(charset)));
  181. }
  182. }
  183. }
  184. return headerMap;
  185. }
  186. public static Dictionary<string, string> GetQueryMap(HttpRequest request, string charset)
  187. {
  188. Dictionary<string, string> queryMap = new Dictionary<string, string>();
  189. string queryString = request.QueryString.ToUriComponent();
  190. if (!string.IsNullOrEmpty(queryString))
  191. {
  192. queryString = queryString.Substring(1); // 忽略?号
  193. string[] parameters = queryString.Split('&');
  194. foreach (string parameter in parameters)
  195. {
  196. string[] kv = parameter.Split('=');
  197. if (kv.Length == 2)
  198. {
  199. string key = HttpUtility.UrlDecode(kv[0], Encoding.GetEncoding(charset));
  200. string value = HttpUtility.UrlDecode(kv[1], Encoding.GetEncoding(charset));
  201. queryMap.Add(key, value);
  202. }
  203. else if (kv.Length == 1)
  204. {
  205. string key = HttpUtility.UrlDecode(kv[0], Encoding.GetEncoding(charset));
  206. queryMap.Add(key, "");
  207. }
  208. }
  209. }
  210. return queryMap;
  211. }
  212. public static Dictionary<string, string> GetFormMap(HttpRequest request)
  213. {
  214. Dictionary<string, string> formMap = new Dictionary<string, string>();
  215. if (request.ContentType != null && (request.ContentType.ToLower().Contains("form-data") || request.ContentType.ToLower().Contains("www-form")))
  216. {
  217. IFormCollection form = request.Form;
  218. var keys = form.Keys;
  219. foreach (string key in keys)
  220. {
  221. string value = request.Form[key];
  222. if (string.IsNullOrEmpty(value))
  223. {
  224. formMap.Add(key, "");
  225. }
  226. else
  227. {
  228. formMap.Add(key, value);
  229. }
  230. }
  231. }
  232. return formMap;
  233. }
  234. public static string GetStreamAsString(HttpRequest request, string charset)
  235. {
  236. Stream stream = null;
  237. StreamReader reader = null;
  238. try
  239. {
  240. // 以字符流的方式读取HTTP请求体
  241. stream = request.Body;
  242. reader = new StreamReader(stream, Encoding.GetEncoding(charset));
  243. return reader.ReadToEnd();
  244. }
  245. finally
  246. {
  247. // 释放资源
  248. if (reader != null) reader.Close();
  249. if (stream != null) stream.Close();
  250. }
  251. }
  252. /// <summary>
  253. /// 检查SPI请求到达服务器端是否已经超过指定的分钟数,如果超过则拒绝请求。
  254. /// </summary>
  255. /// <returns>true代表不超过,false代表超过。</returns>
  256. public static bool CheckTimestamp(HttpRequest request, int minutes)
  257. {
  258. string ts = request.Query[Constants.TIMESTAMP];
  259. if (!string.IsNullOrEmpty(ts))
  260. {
  261. DateTime remote = DateTime.ParseExact(ts, Constants.DATE_TIME_FORMAT, null);
  262. DateTime local = DateTime.Now;
  263. return remote.AddMinutes(minutes).CompareTo(local) > 0;
  264. }
  265. else
  266. {
  267. return false;
  268. }
  269. }
  270. /// <summary>
  271. /// 检查发起SPI请求的来源IP是否是TOP机房的出口IP。
  272. /// </summary>
  273. /// <param name="request">HTTP请求对象</param>
  274. /// <param name="topIpList">TOP网关IP出口地址段列表,通过taobao.top.ipout.get获得</param>
  275. /// <returns>true表达IP来源合法,false代表IP来源不合法</returns>
  276. public static bool CheckRemoteIp(HttpRequest request, List<string> topIpList)
  277. {
  278. string ip = request.Host.Host;
  279. foreach (string ipHeader in HEADER_FIELDS_IP)
  280. {
  281. string realIp = request.Headers[ipHeader];
  282. if (!string.IsNullOrEmpty(realIp) && !"unknown".Equals(realIp))
  283. {
  284. ip = realIp;
  285. break;
  286. }
  287. }
  288. if (topIpList != null)
  289. {
  290. foreach (string topIp in topIpList)
  291. {
  292. if (StringUtil.IsIpInRange(ip, topIp))
  293. {
  294. return true;
  295. }
  296. }
  297. }
  298. return false;
  299. }
  300. }
  301. }