记录下Webapi签名机制

记录下Webapi签名机制

  • 商品编号:
    #57125407_571
    • 原价:
      免费
    • 会员价:
      免费
  • 分类:
    • V1.0.0
  • 数量:
    限购1件

购物车中已存在此商品(限购一件),单击跳转购物车

  • 开发者:Clark-苏
  • 开发语言:C#
  • 开发环境:Visual Studio
  • 数据库:SqlServer
  • 商品架构:B/S
  • 大小(M):0 M
  • 编码格式:utf-8
  • 是否开源:是
  • 开源协议:BSD

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

208266-20171029110322976-1518454111

返回结果:

835511-20171106115455231-286011449

但我们在浏览器中直接显示或信息被串改时,不合法的请求就会被识别为请求参数已被修改

835511-20171106115646044-585195216

    判断签名是否成功,第一次请求签名参数signature和服务器端计算result完全相同, 然后当把请求参数修改之后服务器端计算的result和请求签名参数signature不同,所以请求不合法,是非法请求,同理如果其他任何参数被修改最后计算的结果都会和签名参数不同,请求同样识别为不合法请求

总结:

    通过上面的案例,我们可以看出,安全的关键在于参与签名的token,整个过程中token是不参与通信的,所以只要保证token不泄露,请求就不会被伪造。

    然后我们通过timestamp时间戳用来验证请求是否过期,这样就算被人拿走完整的请求链接也是无效的。


权利声明:本站所有商品信息、客户评价等信息是初心商城重要的数据资源,未经许可,禁止非法转载使用。 注:本站商品信息均来自初心商城,其真实性、准确性和合法性由初心商城负责。

                  初心源说明:初心商城主要为程序员提供开发基础的代码源以及成熟项目,网站中所有的商品有提供收费版本的, 也有提供免费版本的,按照大家各自不同的需求进行购买。实实在在的让程序员只用专注于自己的业务实现你的小梦想, 如果您对我们的成果表示认同并且觉得对你有所帮助我们愿意接受来自各方面的支持^_^。

                  支持:用手机扫描二维码支付

                  支付宝支持我们 微信支持我们

                  您的支持将被用于:
                  1、持续深入的上传更多更好的源代码
                  2、建立更加完善的技术社区
                  3、完善现在系统出现各种问题
                  4、购买域名和租赁服务器

                  1、交易规则

                  2、发货方式

                  1、自动:在上方保障服务中标有自动发货的商品,拍下后,将会自动收到来自卖家的商品获取(下载)链接

                  2、手动:在上方保障服务中标有手动发货的商品,拍下后,卖家会收到邮件,也可通过QQ或订单中的电话联系对方。

                  3、退款说明

                  1、描述:源码描述(含标题)与实际源码不一致的(例:描述PHP实际为ASP、描述的功能实际缺少、版本不符等)

                  2、演示:有演示站时,与实际源码小于95%一致的(但描述中有"不保证完全一样、有变化的可能性"类似显著声明的除外)

                  3、发货:手动发货源码,在卖家未发货前,已申请退款的

                  4、服务:卖家不提供安装服务或需额外收费的(但描述中有显著声明的除外)

                  5、其它:如质量方面的硬性常规问题等

                  备注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。交易中的商品,卖家无法对描述进行修改!

                  4、注意事项

                  1、客户买完之后未确认收货,将不会收到下载地址和下载码,确认收货之后才能收到下载地址和下载码。

                  2、在未拍下前,双方在QQ上所商定的内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准);

                  3、在商品同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外);

                  4、在没有"无任何正当退款依据"的前提下,写有"一旦售出,概不支持退款"等类似的声明,视为无效声明;

                  5、虽然交易产生纠纷的几率很小,但请尽量保留如聊天记录这样的重要信息,以防产生纠纷时出现问题不明确的情况。

                  5、交易声明

                  1、本站作为直卖平台,依据交易合同(商品描述、交易前商定的内容)来保障交易的安全及买卖双方的权益;

                  2、非平台线上交易的商品,出现任何后果均与本站无关;无论卖家以何理由要求线下交易的,请联系管理举报。

                  初心Logo

                  初心商城| 初心系列| 初心博客| 初心历史| 系统反馈

                  chuxinm.com 京ICP备16055626号 © 2016-2018 山西米立信息技术有限公司 保留所有权利
                  违法和不良信息举报电话:186-2950-9347,本网站所列数据,除特殊说明,所有数据均出自我工作室
                  本网站兼容所有主流浏览器,不支持手机自适应