详解 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
详解 WebAPI 签名机制的更多相关文章
- 【WebApi系列】详解WebApi如何传递参数
WebApi系列文章 [01]浅谈HTTP在WebApi开发中的运用 [02]聊聊WebApi体系结构 [03]详解WebApi参数的传递 [04]详解WebApi测试和PostMan [05]浅谈W ...
- Android apk签名详解——AS签名、获取签名信息、系统签名、命令行签名
Apk签名,每一个Android开发者都不陌生.它就是对我们的apk加了一个校验参数,防止apk被掉包.一开始做Android开发,就接触到了apk签名:后来在微信开放平台.高德地图等平台注册时,需要 ...
- 详解浏览器缓存机制与Apache设置缓存
一.详解浏览器缓存机制 对于,如何说明缓存机制,在网络上找到了两张图,个人认为思路是比较清晰的.总结时,上图. 这里需要注意的有两点: 1.Last-Modified.Etag是响应头里的数据 2.I ...
- 百度地图API详解之事件机制,function“闭包”解决for循环和监听器冲突的问题:
原文:百度地图API详解之事件机制,function"闭包"解决for循环和监听器冲突的问题: 百度地图API详解之事件机制 2011年07月26日 星期二 下午 04:06 和D ...
- Linux上的free命令详解、swap机制
Linux上的free命令详解 解释一下Linux上free命令的输出. 下面是free的运行结果,一共有4行.为了方便说明,我加上了列号.这样可以把free的输出看成一个二维数组FO(Free ...
- 记录下Webapi签名机制
首先,写这篇文章的原因是因为最近某一个项目中的接口被人为调用了,导致了数据库数据被串改.虽然是内部人无意点的,但还是引起了我的担忧,所有整理了下关于Webapi的相关签名机制. 一.我们在开发接口时, ...
- day09 详解内存管理机制
""" 今日内容:详解内存管理 1.引用计数 在内存中为了对变量的值进行标记从而方便管理,采用引用计数的方式对变量进行标记. (1)如果变量的值被引用一次,那么该变量的引 ...
- Android AIDL使用详解_Android IPC 机制详解
一.概述 AIDL 意思即 Android Interface Definition Language,翻译过来就是Android接口定义语言,是用于定义服务器和客户端通信接口的一种描述语言,可以拿来 ...
- 详解Java反射机制
反射是程序在运行状态下,动态的获取某个类的内部信息的一种操作.例如:类名,包名,所有属性的集合,所有方法的集合,构造方法的集合等.该操作发生在程序的运行时状态,所以编译器管不着有关反射的一些代码,通常 ...
随机推荐
- ie6不能播放视频问题
前几天做项目时碰到一个非常棘手的问题.在我自己本机的ie8上能正常播放视频的程序(ie6也能够),放用户的电脑上就是不能正常播放(可能是用户的机子系统太老或是别的什么原因.详细的我也不太清楚).没办法 ...
- Python基础教程-Dict和Set
Python的dict Python内置了字典:dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度. 比如,要根据同学的名字查找对应的 ...
- python中得到shell命令输出的方法
python中得到shell命令输出的方法: 1. import subprocess output = subprocess.Popen(['ls','-l'],stdout=subprocess ...
- django settings相关配置
settings """ Django settings for mysite project. Generated by 'django-admin startproj ...
- R语言操作mysql上亿数据量(ff包ffbase包和ETLUtils包)
平时都是几百万的数据量,这段时间公司中了个大标,有上亿的数据量. 现在情况是数据已经在数据库里面了,需要用R分析,但是完全加载不进来内存. 面对现在这种情况,R提供了ff, ffbase , ETLU ...
- delphi webbrowser 获取iframe
procedure TForm1.Button4Click(Sender: TObject);var Index: Integer; Document: IHTMLDocument2; FrameId ...
- HDU - 1695 GCD (容斥+枚举)
题意:求区间1<=i<=b与区间1<=j<=d之间满足gcd(i,j) = k 的数对 (i,j) 个数.(i,j)与(j,i) 算一个. 分析:gcd(i,j)=k可以转化为 ...
- sql 转
- Kali更新源,亲测目前可用的源
kali更新的时候老是无法定位软件包,网络上大部分中科大.阿里云kali源都不可用,都千篇一律,最后找了这个,网易的,还不错,贴出来大家看看: # 源 deb http://mirrors.163.c ...
- 解决You have new mail in /var/spool/mail/root提示
终端远程登陆后经常提示You have new mail in /var/spool/mail/root 这个提示是LINUX会定时查看LINUX各种状态做汇总,每经过一段时间会把汇总的信息发送的ro ...