详解 WebAPI 签名机制
首先,写这篇文章的原因是因为最近某一个项目中的接口被人为调用了,导致了数据库数据被串改。虽然是内部人无意点的,但还是引起了我的担忧,所有整理了下关于WebAPI的相关签名机制。
一、我们在开发接口时,有时候嫌麻烦就懒进行相关的验证或只进行一些简单的验证,这样客户端就可以直接调用:如
调用WebAPI接口:http://XXX.XXX.XX.XXX:8123/Token/GetTest?ID=123456
这种方式简单粗暴,在浏览器直接输入"http://XXX.XXX.XX.XXX:8123/Token/GetTest?ID=123456",即可获取产品列表信息了,但是这样的方式会存在很严重的安全性问题,没有进行任何的验证,大家都可以通过这个方法获取到产品列表,导致产品信息泄露,下面简单记录下使用使用TOKEN+签名认证
二、使用TOKEN+签名认证 保证请求安全性
token+签名认证的主要原理是:
1、做一个认证服务,提供一个认证的webapi,用户先访问它获取对应的token
2、用户拿着相应的token以及请求的参数和服务器端提供的签名算法计算出签名后再去访问指定的api
3、服务器端每次接收到请求就获取对应用户的token和请求参数,服务器端再次计算签名和客户端签名做对比,如果验证通过则正常访问相应的api,验证失败则 返回具体的失败信息
具体代码如下:
1、用户请求认证服务GetToken,将token保存在服务器端缓存中,并返回对应的Token到客户端(该请求不需要进行签名认证),使用GET调用方式
[HttpGet]
public IHttpActionResult GetToken(string signKey)
{
if (string.IsNullOrEmpty(signKey))
return Json<ResultMsg>(new ResultMsg((int)ExceptionStatus.ParameterError, EnumExtension.GetEnumText(ExceptionStatus.ParameterError), null));
//根据签名ID获取缓存token
string strKey = string.Format("{0}{1}", WebConfig.signKey, signKey);
Token cacheData = HttpRuntime.Cache.Get(strKey) as Token;
if (cacheData == null)
{
cacheData = new Token();
cacheData.signId = signKey;
cacheData.timespan = DateTime.Now.AddDays(1);
cacheData.signToken = Guid.NewGuid().ToString("N");
//插入缓存,缓存时间为1天
HttpRuntime.Cache.Insert(strKey, cacheData, null, cacheData.timespan, TimeSpan.Zero);
}
//返回token信息
return Json<ResultMsg>(new ResultMsg((int)ExceptionStatus.OK, EnumExtension.GetEnumText(ExceptionStatus.OK), cacheData));
}
2、客户端调用方法,GET或POST
(1) GET:需要在请求头中添加:timespan(时间戳),nonce(随机数),signKey(key),signature(签名参数)
public static T Get<T>(string url, string paras, string signId,bool isSign=true)
{
HttpWebRequest webrequest = null;
HttpWebResponse webresponse = null;
string strResult = string.Empty;
try
{
webrequest = (HttpWebRequest)WebRequest.Create(url + "?" + paras);
webrequest.Method = "GET";
webrequest.ContentType = "application/json";
webrequest.Timeout = 90000;
//加入头信息
string timespan = GetTimespan();
string ran = GetRandom(10);
webrequest.Headers.Add("signKey", signId);
DbLogger.LogWriteMessage("signKey:" + signId);
webrequest.Headers.Add("timespan", timespan);
DbLogger.LogWriteMessage("timespan:" + timespan);
webrequest.Headers.Add("nonce", ran);
DbLogger.LogWriteMessage("nonce:" + ran);
if (isSign)
{
string strSign = GetSignature(signId, timespan, ran, paras);
webrequest.Headers.Add("signature", strSign);
DbLogger.LogWriteMessage("signature:" + strSign);
}
webresponse = (HttpWebResponse)webrequest.GetResponse();
Stream stream = webresponse.GetResponseStream();
StreamReader sr = new StreamReader(stream, Encoding.UTF8);
strResult = sr.ReadToEnd();
}
catch (Exception ex)
{
return JsonConvert.DeserializeObject<T>(ex.Message);
}
finally
{
if (webresponse != null)
webresponse.Close();
if (webrequest != null)
webrequest.Abort();
}
return JsonConvert.DeserializeObject<T>(strResult);
}
(2)POST写法这里就不写了,同理需要设置header请求头参数:timespan(时间戳),nonce(随机数),signKey(key),signature(签名参数)
(3)根据请求参数计算本次请求的签名,用timespan+nonc+signKey+token+data(请求参数字符串)得到signStr签名字符串,然后再进行排序和MD5加密得到最终的signature签名字符串,添加到请求头中
public static string GetSignature(string signKey, string timespan, string nonce, string data)
{
string signToken = string.Empty;
var result = GetToken<JObject>();
if (result != null)
{
if (result["code"].ToString() == "200")
{
var tokena = JsonConvert.DeserializeObject<JObject>(result["result"].ToString());
if (tokena != null)
signToken = tokena["signToken"].ToString();
}
}
var hash = MD5.Create();
string str = signKey + timespan + nonce + signToken + data;
byte[] bytes = Encoding.UTF8.GetBytes(string.Concat(str.OrderBy(c => c)));
DbLogger.LogWriteMessage("str内容:" + string.Concat(str.OrderBy(c => c)));
//使用MD5加密
var md5Val = hash.ComputeHash(bytes);
//把二进制转化为大写的十六进制
StringBuilder strSign = new StringBuilder();
foreach (var val in md5Val)
{
strSign.Append(val.ToString("X2"));
}
return strSign.ToString();
}
(4)WebAPI接收到相应参数,通过header获取到timespan(时间戳),nonce(随机数),signKey(key),signature(签名参数),判断参数是否为空、接口是否在有效时间内、判断token是否有效、判断和请求的signature(签名)是否相同,如果通过,返回正常的结果。如果验证不通过,返回相应的错误提示信息。
public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext filterContext)
{
ResultMsg result = null;
string signKey = string.Empty, timespan = string.Empty, nonce = string.Empty, signature = string.Empty;
//判断请求的消息中是否包括判断参数
var request = filterContext.Request;
if (request.Headers.Contains("signKey"))
signKey = request.Headers.GetValues("signKey").FirstOrDefault();
if (request.Headers.Contains("timespan"))
timespan = request.Headers.GetValues("timespan").FirstOrDefault();
if (request.Headers.Contains("nonce"))
nonce = request.Headers.GetValues("nonce").FirstOrDefault();
if (request.Headers.Contains("signature"))
signature = request.Headers.GetValues("signature").FirstOrDefault();
//如果方法是GetToken,则不需要验证
if (filterContext.ActionDescriptor.ActionName.ToLower() == "gettoken")
{
if (string.IsNullOrEmpty(signKey) || string.IsNullOrEmpty(timespan) || string.IsNullOrEmpty(nonce))
{
result = new ResultMsg((int)ExceptionStatus.ParameterError, EnumExtension.GetEnumText(ExceptionStatus.ParameterError), null);
filterContext.Response = HttpResponseExtension.ToJson(result);
base.OnActionExecuting(filterContext);
return;
}
else
{
base.OnActionExecuting(filterContext);
return;
}
}
DbLogger.LogWriteMessage("测试参数");
string signtoken = string.Empty;
//判断是否包含以下参数
if (string.IsNullOrEmpty(signKey) || string.IsNullOrEmpty(timespan) || string.IsNullOrEmpty(nonce) || string.IsNullOrEmpty(signature))
{
result = new ResultMsg((int)ExceptionStatus.ParameterError, EnumExtension.GetEnumText(ExceptionStatus.ParameterError), null);
filterContext.Response = HttpResponseExtension.ToJson(result);
base.OnActionExecuting(filterContext);
return;
}
DbLogger.LogWriteMessage("测试是否在有效时间内");
//判断是否在有效时间内
double ts1 = 0;
double ts2 = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalMilliseconds;
bool timespanValidate = double.TryParse(timespan, out ts1);
double ts = ts2 - ts1;
bool falg = ts > int.Parse(WebConfig.UrlExpireTime) * 1000;
if (!timespanValidate || falg)
{
result = new ResultMsg((int)ExceptionStatus.URLExpireError, EnumExtension.GetEnumText(ExceptionStatus.URLExpireError), null);
filterContext.Response = HttpResponseExtension.ToJson(result);
base.OnActionExecuting(filterContext);
return;
}
DbLogger.LogWriteMessage("测试token是否有效");
//判断token是否有效
Token token = HttpRuntime.Cache.Get(string.Format("{0}{1}", WebConfig.signKey, signKey)) as Token;
if (token == null)
{
result = new ResultMsg((int)ExceptionStatus.TokenInvalid, EnumExtension.GetEnumText(ExceptionStatus.TokenInvalid), null);
filterContext.Response = HttpResponseExtension.ToJson(result);
base.OnActionExecuting(filterContext);
return;
}
else
signtoken = token.signToken;
DbLogger.LogWriteMessage("判断http调用方式");
string data = string.Empty;
//判断http调用方式
string method = request.Method.Method.ToUpper();
switch (method)
{
case "POST":
Stream stream = HttpContext.Current.Request.InputStream;
string responseJson = string.Empty;
StreamReader streamReader = new StreamReader(stream);
data = streamReader.ReadToEnd();
break;
case "GET":
NameValueCollection form = HttpContext.Current.Request.QueryString;
//第一步:取出所有get参数
IDictionary<string, string> parameters = new Dictionary<string, string>();
for (int f = 0; f < form.Count; f++)
{
string key = form.Keys[f];
parameters.Add(key, form[key]);
}
// 第二步:把字典按Key的字母顺序排序
IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parameters);
IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator();
// 第三步:把所有参数名和参数值串在一起
StringBuilder query = new StringBuilder();
while (dem.MoveNext())
{
string key = dem.Current.Key;
string value = dem.Current.Value;
if (!string.IsNullOrEmpty(key))
{
query.Append(key).Append(value);
}
}
data = query.ToString();
break;
default:
result = new ResultMsg((int)ExceptionStatus.HttpMehtodError, EnumExtension.GetEnumText(ExceptionStatus.HttpMehtodError), null);
filterContext.Response = HttpResponseExtension.ToJson(result);
base.OnActionExecuting(filterContext);
break;
}
DbLogger.LogWriteMessage("验证签名信息是否符合");
//验证签名信息是否符合
bool valida = ValidateSign.Validate(signKey, timespan, nonce, signtoken, data, signature);
if (!valida)
{
result = new ResultMsg((int)ExceptionStatus.HttpRequestError, EnumExtension.GetEnumText(ExceptionStatus.HttpRequestError), null);
filterContext.Response = HttpResponseExtension.ToJson(result);
base.OnActionExecuting(filterContext);
return;
}
else
base.OnActionExecuting(filterContext);
}
}
下面我们进行测试:
GET请求:
返回结果:
但我们在浏览器中直接显示或信息被串改时,不合法的请求就会被识别为请求参数已被修改
判断签名是否成功,第一次请求签名参数signature和服务器端计算result完全相同, 然后当把请求参数修改之后服务器端计算的result和请求签名参数signature不同,所以请求不合法,是非法请求,同理如果其他任何参数被修改最后计算的结果都会和签名参数不同,请求同样识别为不合法请求
总结
通过上面的案例,我们可以看出,安全的关键在于参与签名的token,整个过程中token是不参与通信的,所以只要保证token不泄露,请求就不会被伪造。
然后我们通过timestamp时间戳用来验证请求是否过期,这样就算被人拿走完整的请求链接也是无效的。
源码下载地址:https://pan.baidu.com/s/1hrBOnRY