详解 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反射机制
反射是程序在运行状态下,动态的获取某个类的内部信息的一种操作.例如:类名,包名,所有属性的集合,所有方法的集合,构造方法的集合等.该操作发生在程序的运行时状态,所以编译器管不着有关反射的一些代码,通常 ...
随机推荐
- WireShark告诉你ping百度时都发生了什么
备注: 测试机器为Mac 重点展示本机发出icmp的过程(dns-->arp-->icmp) 本机默认网关 ->route -n get e -n get default rout ...
- Python Network Programming
@1: 同步网络编程(也就是阻塞方式) 同步网络编程一次只能连接一个客户端. Server端: import socket def debugPrint(name, value): print(&qu ...
- beego——获取参数
1.获取参数 我们经常需要获取用户传递的数据,包括Get.POST等方式的请求,beego里面会自动解析这些数据,你可以通过如下方式获取数据: GetString(key string) string ...
- go——接口
Go语言提供了另外一种数据类型,即接口,它把所有具有共性的方法定义在一起,任何其它类型只要实现了这些方法就是实现了这个接口. 接口代表一种调用契约,是多个方法声明的集合.在某些动态语言里,接口(int ...
- 笔记-CSS空背景图片会导致页面被加载两次
如果页面样式的背景图片路径设置为'' 或 '#', 会导致页面被重复加载两次 (Chrome.56.0.2924.87 测试) 因为:空图片路径属性值,默认加载当前页面的URL作为图片路径 Safar ...
- 理解盒模型——外边距、内边距和边框之间的关系,IE 8以下版本的浏览器中的盒模型有什么不同。
一个元素盒模型的层次从内到外分别为:内边距.边框和外边距IE8以下浏览器的盒模型中定义的元素的宽高不包括内边距和边框
- [原创] 毕设---在myeclipes中安装Hadoop开发插件
1.安装Hadoop开发插件 hadoop安装包contrib/目录下有个插件hadoop-0.20.2-eclipse-plugin.jar,拷贝到myeclipse根目录下/dropins目录下. ...
- ubuntu16.04 虚拟机 安装win7/win10
http://www.xitongcheng.com/jiaocheng/xtazjc_article_26588.html https://blog.csdn.net/sunyao_123/arti ...
- Apache 浏览器访问限制配置
浏览器访问限制配置 user_agent收入的浏览器中,我们通过百度,谷歌很容易就可以查到相关的一些资料,方便了我们对知识的查找,但在某些特定情况下,我们并不希望有人可以通过某写搜索引擎直接访问到我们 ...
- jQuery二级下拉菜单
在线演示 本地下载