WebApi安全性 使用TOKEN+签名验证

首先问大家一个问题,你在写开放的API接口时是如何保证数据的安全性的?先来看看有哪些安全性问题在开放的api接口中,我们通过http Post或者Get方式请求服务器的时候,会面临着许多的安全性问题,例如:

请求来源(身份)是否合法?

请求参数被篡改?

请求的唯一性(不可复制),防止请求被恶意攻击

为了保证数据在通信时的安全性,我们可以采用TOKEN+参数签名的方式来进行相关验证。



比如说我们客户端需要查询产品信息这个操作来进行分析,客户端点击查询按钮==》调用服务器端api进行查询==》服务器端返回查询结果

一、不进行验证的方式

api查询接口:

客户端调用:http://api.XXX.com/getproduct?id=value1

如上,这种方式简单粗暴,在浏览器直接输入"http://api.XXX.com/getproduct?id=value1",即可获取产品列表信息了,但是这样的方式会存在很严重的安全性问题,没有进行任何的验证,大家都可以通过这个方法获取到产品列表,导致产品信息泄露。

那么,如何验证调用者身份呢?如何防止参数被篡改呢?如何保证请求的唯一性? 如何保证请求的唯一性,防止请求被恶意攻击呢?


二、使用TOKEN+签名认证 保证请求安全性

token+签名认证的主要原理是:1.做一个认证服务,提供一个认证的webapi,用户先访问它获取对应的token

                                         2.用户拿着相应的token以及请求的参数和服务器端提供的签名算法计算出签名后再去访问指定的api

                3.服务器端每次接收到请求就获取对应用户的token和请求参数,服务器端再次计算签名和客户端签名做对比,如果验证通过则正常访问相应的api,验证失败则返回具体的失败信息


具体代码如下 :

1.用户请求认证服务GetToken,将TOKEN保存在服务器端缓存中,并返回对应的TOKEN到客户端(该请求不需要进行签名认证)

publicHttpResponseMessage GetToken(string staffId)

        {

            ResultMsg resultMsg =null;

            intid =0;

            //判断参数是否合法if(string.IsNullOrEmpty(staffId) || (!int.TryParse(staffId,out id)))

            {

                resultMsg =new ResultMsg();

                resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError;

                resultMsg.Info = StatusCodeEnum.ParameterError.GetEnumText();

                resultMsg.Data ="";

                return HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg));

            }

            //插入缓存Token token =(Token)HttpRuntime.Cache.Get(id.ToString());

            if(HttpRuntime.Cache.Get(id.ToString()) ==null)

            {

                token =new Token();

                token.StaffId = id;

                token.SignToken = Guid.NewGuid();

                token.ExpireTime = DateTime.Now.AddDays(1);

                HttpRuntime.Cache.Insert(token.StaffId.ToString(), token, null, token.ExpireTime, TimeSpan.Zero);

            }

            //返回token信息resultMsg =new ResultMsg();

            resultMsg.StatusCode = (int)StatusCodeEnum.Success;

            resultMsg.Info ="";

            resultMsg.Data = token;

            return HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg));

        }


2.客户端调用服务器端API,需要对请求进行签名认证,签名方式如下 

(1) get请求:按照请求参数名称将所有请求参数按照字母先后顺序排序得到:keyvaluekeyvalue...keyvalue  字符串如:将arong=1,mrong=2,crong=3 排序为:arong=1, crong=3,mrong=2  然后将参数名和参数值进行拼接得到参数字符串:arong1crong3mrong2。 

publicstaticTuple GetQueryString(Dictionary parames)

        {

            // 第一步:把字典按Key的字母顺序排序IDictionary sortedParams =newSortedDictionary(parames);

            IEnumerator> dem = sortedParams.GetEnumerator();

            // 第二步:把所有参数名和参数值串在一起StringBuilder query =newStringBuilder("");//签名字符串StringBuilder queryStr =newStringBuilder("");//url参数if(parames ==null|| parames.Count ==0)

                returnnewTuple("","");

            while (dem.MoveNext())

            {

                stringkey = dem.Current.Key;

                stringvalue = dem.Current.Value;

                if(!string.IsNullOrEmpty(key))

                {

                    query.Append(key).Append(value);

                    queryStr.Append("&").Append(key).Append("=").Append(value);

                }

            }

            returnnewTuple(query.ToString(), queryStr.ToString().Substring(1, queryStr.Length -1));

        }


post请求:将请求的参数对象序列化为json格式字符串

Product product =newProduct() { Id =1, Name ="安慕希", Count =10, Price =58.8 };

var data=JsonConvert.SerializeObject(product);


(2)在请求头中添加timespan(时间戳),nonce(随机数),staffId(用户Id),signature(签名参数)

//加入头信息request.Headers.Add("staffid", staffId.ToString());//当前请求用户StaffIdrequest.Headers.Add("timestamp", timeStamp);//发起请求时的时间戳(单位:毫秒)request.Headers.Add("nonce", nonce);//发起请求时的时间戳(单位:毫秒)request.Headers.Add("signature", GetSignature(timeStamp,nonce,staffId,data));//当前请求内容的数字签名


(3)根据请求参数计算本次请求的签名,用timespan+nonc+staffId+token+data(请求参数字符串)得到signStr签名字符串,然后再进行排序和MD5加密得到最终的signature签名字符串,添加到请求头中

privatestaticstringGetSignature(stringtimeStamp,stringnonce,intstaffId,string data)

        {

            Token token =null;

            varresultMsg = GetSignToken(staffId);

            if(resultMsg !=null)

            {

                if(resultMsg.StatusCode == (int)StatusCodeEnum.Success)

                {

                    token = resultMsg.Result;

                }

                else                {

                    thrownew Exception(resultMsg.Data.ToString());

                }

            }

            else            {

                thrownewException("token为null,员工编号为:"+staffId);

            }

            varhash = System.Security.Cryptography.MD5.Create();

            //拼接签名数据varsignStr = timeStamp +nonce+ staffId + token.SignToken.ToString() + data;

            //将字符串中字符按升序排序varsortStr =string.Concat(signStr.OrderBy(c => c));

            varbytes = Encoding.UTF8.GetBytes(sortStr);

            //使用MD5加密varmd5Val = hash.ComputeHash(bytes);

            //把二进制转化为大写的十六进制StringBuilder result =new StringBuilder();

            foreach(varcin md5Val)

            {

                result.Append(c.ToString("X2"));

            }

            return result.ToString().ToUpper();

        }


(4) webapi接收到相应的请求,取出请求头中的timespan,nonc,staffid,signature 数据,根据timespan判断此次请求是否失效,根据staffid取出相应token判断token是否失效,根据请求类型取出对应的请求参数,然后服务器端按照同样的规则重新计算请求签名,判断和请求头中的signature数据是否相同,如果相同的话则是合法请求,正常返回数据,如果不相同的话,该请求可能被恶意篡改,禁止访问相应的数据,返回相应的错误信息 

 如下使用全局过滤器拦截所有api请求进行统一的处理

publicclass ApiSecurityFilter : ActionFilterAttribute

    {

        publicoverridevoid OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)

        {

            ResultMsg resultMsg =null;

            varrequest = actionContext.Request;

            stringmethod = request.Method.Method;

            stringstaffid = String.Empty, timestamp =string.Empty, nonce =string.Empty, signature =string.Empty;

            intid =0;

            if(request.Headers.Contains("staffid"))

            {

                staffid = HttpUtility.UrlDecode(request.Headers.GetValues("staffid").FirstOrDefault());

            }

            if(request.Headers.Contains("timestamp"))

            {

                timestamp = HttpUtility.UrlDecode(request.Headers.GetValues("timestamp").FirstOrDefault());

            }

            if(request.Headers.Contains("nonce"))

            {

                nonce = HttpUtility.UrlDecode(request.Headers.GetValues("nonce").FirstOrDefault());

            }

            if(request.Headers.Contains("signature"))

            {

                signature = HttpUtility.UrlDecode(request.Headers.GetValues("signature").FirstOrDefault());

            }

            //GetToken方法不需要进行签名验证if(actionContext.ActionDescriptor.ActionName =="GetToken")

            {

                if(string.IsNullOrEmpty(staffid) || (!int.TryParse(staffid,outid) ||string.IsNullOrEmpty(timestamp) ||string.IsNullOrEmpty(nonce)))

                {

                    resultMsg =new ResultMsg();

                    resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError;

                    resultMsg.Info = StatusCodeEnum.ParameterError.GetEnumText();

                    resultMsg.Data ="";

                    actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg));

                    base.OnActionExecuting(actionContext);

                    return;

                }

                else                {

                    base.OnActionExecuting(actionContext);

                    return;

                }

            }

            //判断请求头是否包含以下参数if(string.IsNullOrEmpty(staffid) || (!int.TryParse(staffid,outid) ||string.IsNullOrEmpty(timestamp) ||string.IsNullOrEmpty(nonce) ||string.IsNullOrEmpty(signature)))

            {

                resultMsg =new ResultMsg();

                resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError;

                resultMsg.Info = StatusCodeEnum.ParameterError.GetEnumText();

                resultMsg.Data ="";

                actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg));

                base.OnActionExecuting(actionContext);

                return;

            }

            //判断timespan是否有效doublets1 =0;

            doublets2 = (DateTime.UtcNow -newDateTime(1970,1,1,0,0,0,0)).TotalMilliseconds;

            booltimespanvalidate =double.TryParse(timestamp,out ts1);

            doublets = ts2 - ts1;

            boolfalg = ts >int.Parse(WebSettingsConfig.UrlExpireTime) *1000;

            if(falg || (!timespanvalidate))

            {

                resultMsg =new ResultMsg();

                resultMsg.StatusCode = (int)StatusCodeEnum.URLExpireError;

                resultMsg.Info = StatusCodeEnum.URLExpireError.GetEnumText();

                resultMsg.Data ="";

                actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg));

                base.OnActionExecuting(actionContext);

                return;

            }

            //判断token是否有效Token token = (Token)HttpRuntime.Cache.Get(id.ToString());

            stringsigntoken =string.Empty;

            if(HttpRuntime.Cache.Get(id.ToString()) ==null)

            {

                resultMsg =new ResultMsg();

                resultMsg.StatusCode = (int)StatusCodeEnum.TokenInvalid;

                resultMsg.Info = StatusCodeEnum.TokenInvalid.GetEnumText();

                resultMsg.Data ="";

                actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg));

                base.OnActionExecuting(actionContext);

                return;

            }

            else            {

                signtoken = token.SignToken.ToString();

            }

            //根据请求类型拼接参数NameValueCollection form = HttpContext.Current.Request.QueryString;

            stringdata =string.Empty;

            switch (method)

            {

                case"POST":

                    Stream stream = HttpContext.Current.Request.InputStream;

                    stringresponseJson =string.Empty;

                    StreamReader streamReader =new StreamReader(stream);

                    data = streamReader.ReadToEnd();

                    break;

                case"GET":

                    //第一步:取出所有get参数IDictionary parameters =newDictionary();

                    for(intf =0; f < form.Count; f++)

                    {

                        stringkey = form.Keys[f];

                        parameters.Add(key, form[key]);

                    }

                    // 第二步:把字典按Key的字母顺序排序IDictionary sortedParams =newSortedDictionary(parameters);

                    IEnumerator> dem = sortedParams.GetEnumerator();

                    // 第三步:把所有参数名和参数值串在一起StringBuilder query =new StringBuilder();

                    while (dem.MoveNext())

                    {

                        stringkey = dem.Current.Key;

                        stringvalue = dem.Current.Value;

                        if(!string.IsNullOrEmpty(key))

                        {

                            query.Append(key).Append(value);

                        }

                    }

                    data = query.ToString();

                    break;

                default:

                    resultMsg =new ResultMsg();

                    resultMsg.StatusCode = (int)StatusCodeEnum.HttpMehtodError;

                    resultMsg.Info = StatusCodeEnum.HttpMehtodError.GetEnumText();

                    resultMsg.Data ="";

                    actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg));

                    base.OnActionExecuting(actionContext);

                    return;

            }


            boolresult = SignExtension.Validate(timestamp, nonce, id, signtoken,data, signature);

            if(!result)

            {

                resultMsg =new ResultMsg();

                resultMsg.StatusCode = (int)StatusCodeEnum.HttpRequestError;

                resultMsg.Info = StatusCodeEnum.HttpRequestError.GetEnumText();

                resultMsg.Data ="";

                actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg));

                base.OnActionExecuting(actionContext);

                return;

            }

            else            {

                base.OnActionExecuting(actionContext);

            }

        }

        publicoverridevoid OnActionExecuted(HttpActionExecutedContext actionExecutedContext)

        {

            base.OnActionExecuted(actionExecutedContext);        }    }


然后我们进行测试,检验api请求的合法性

Get请求:

1.获取产品数据,传递参数id=1,name="wahaha"  ,完整请求为http://localhost:14826/api/product/getproduct?id=1&name=wahaha


2.请求头添加timespan,staffid,nonce,signature字段


 3.如图当data里面的值为id1namewahaha的时候请求头中的signature和服务器端计算出来的result的值是完全一样的,当我将data修改为id1namewahaha1之后,服务器端计算出来的签名result和请求头中提交的signature就不相同了,就表示为不合法的请求了


4.不合法的请求就会被识别为请求参数已被修改

  合法的请求则会返回对应的商品信息

post请求:

1.post对象序列化为json字符串后提交到后台,后台返回相应产品信息


2.后台获取请求的参数信息

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


总结:

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,638评论 18 139
  • https://www.cnblogs.com/MR-YY/archive/2016/10/18/5972380....
    suzheya阅读 1,762评论 0 0
  • 登录的业务逻辑 { http:是短连接. 服务器如何判断当前用户是否登录? // 1. 如果是即时通信类:长连接....
    VincentHK阅读 2,243评论 0 9
  • 我们的曾经只剩下回忆, 我们的现在只剩下争吵, 我们的未来只剩下黑暗。 所以我决定给你自由, 所以我决定独自离开,...
    汐菲儿阅读 173评论 0 0
  • 燕子去了,有再来的时候;杨柳枯了,有再青的时候;桃花谢了,有再开的时候。但是,聪明的,你告诉我,我们的日子为什么一...
    鱼小哆阅读 3,355评论 18 59