详解 WebAPI 签名机制

2017 年 11 月 13 日 DotNet

(点击上方蓝字,可快速关注我们)


来源:Clark-苏

cnblogs.com/suzhiyong1988/p/7792457.html


首先,写这篇文章的原因是因为最近某一个项目中的接口被人为调用了,导致了数据库数据被串改。虽然是内部人无意点的,但还是引起了我的担忧,所有整理了下关于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


看完本文有收获?请转发分享给更多人

关注「DotNet」,提升.Net技能 

登录查看更多
0

相关内容

【SIGIR2020】用于冷启动推荐的内容感知神经哈希
专知会员服务
22+阅读 · 2020年6月2日
【高能所】如何做好⼀份学术报告& 简单介绍LaTeX 的使用
【SIGMOD2020-腾讯】Web规模本体可扩展构建
专知会员服务
29+阅读 · 2020年4月12日
【WWW2020-微软】理解用户行为用于文档推荐
专知会员服务
35+阅读 · 2020年4月5日
【Amazon】使用预先训练的Transformer模型进行数据增强
专知会员服务
56+阅读 · 2020年3月6日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
69+阅读 · 2020年1月17日
【干货】用BRET进行多标签文本分类(附代码)
专知会员服务
84+阅读 · 2019年12月27日
Keras作者François Chollet推荐的开源图像搜索引擎项目Sis
专知会员服务
29+阅读 · 2019年10月17日
用Now轻松部署无服务器Node应用程序
前端之巅
16+阅读 · 2019年6月19日
Web渗透测试Fuzz字典分享
黑白之道
20+阅读 · 2019年5月22日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
基于Web页面验证码机制漏洞的检测
FreeBuf
7+阅读 · 2019年3月15日
R工程化—Rest API 之plumber包
R语言中文社区
11+阅读 · 2018年12月25日
浅谈浏览器 http 的缓存机制
前端大全
6+阅读 · 2018年1月21日
设计和实现一款轻量级的爬虫框架
架构文摘
13+阅读 · 2018年1月17日
Arxiv
6+阅读 · 2020年4月14日
Arxiv
6+阅读 · 2019年4月8日
Arxiv
3+阅读 · 2019年3月1日
Rapid Customization for Event Extraction
Arxiv
7+阅读 · 2018年9月20日
Arxiv
3+阅读 · 2018年6月1日
Arxiv
5+阅读 · 2018年5月1日
VIP会员
相关VIP内容
相关资讯
用Now轻松部署无服务器Node应用程序
前端之巅
16+阅读 · 2019年6月19日
Web渗透测试Fuzz字典分享
黑白之道
20+阅读 · 2019年5月22日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
基于Web页面验证码机制漏洞的检测
FreeBuf
7+阅读 · 2019年3月15日
R工程化—Rest API 之plumber包
R语言中文社区
11+阅读 · 2018年12月25日
浅谈浏览器 http 的缓存机制
前端大全
6+阅读 · 2018年1月21日
设计和实现一款轻量级的爬虫框架
架构文摘
13+阅读 · 2018年1月17日
相关论文
Arxiv
6+阅读 · 2020年4月14日
Arxiv
6+阅读 · 2019年4月8日
Arxiv
3+阅读 · 2019年3月1日
Rapid Customization for Event Extraction
Arxiv
7+阅读 · 2018年9月20日
Arxiv
3+阅读 · 2018年6月1日
Arxiv
5+阅读 · 2018年5月1日
Top
微信扫码咨询专知VIP会员