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;
|
}
|
}
|
}
|