zoukankan      html  css  js  c++  java
  • 第十节:进一步扩展两种安全校验方式

    一. 简介

    简介: 上一节中,主要介绍了JWT校验,它是无状态的,是基于Token校验的一种升级,它适用的范围很广泛,APP、JS前端、后台等等客户端调用服务器端的校验。本节补充几种后台接口的校验方式,它主要适用于后台代码的调用,不适合JS、APP等客户端直接调用。

      PS:在一些对接一些银行接口或者一些支付接口,通常会提到这么几个名词:

      (1). 根据参数名正序排序、根据参数名的ASCII码排序。

      (2). appKey和appSecret,通常appKey是要当做参数进行传递,appSecret用于Sign值的计算(通常拼接后用MD5加密),有的让你 MD5(拼接参数),然后再和appSecret拼接一块,有的直接吧appSecret和其它参数按照一定规则直接拼接,最后进行MD5加密。

    1. 根据参数名正序排序

    eg:参数名分别为appKey、abp、userName、userPwd,排序先根据首字母排序,首字母相同,看第二个字母,依次类推,所以排序的结果为:abp、appkey、userName、userPwd,我们最终想拼接的字符串的形式为:【abp=hh&appkey=hh&userName=hh&userPwd=hh】

    代码分享:

    借助orderBy和Select可以实现正序排序,然后利用Join方法进行拼接

     1        [HttpGet]
     2         public string TestParamZx()
     3         {
     4             Dictionary<string, string> dics = new Dictionary<string, string>();
     5             dics.Add("abp", "hh");
     6             dics.Add("appkey", "hh");
     7             dics.Add("useName", "hh");
     8             dics.Add("userPwd", "hh");
     9             //根据名称正序排序 拿出来key和value,中间用=拼接
    10             var param = dics.OrderBy(u => u.Key).Select(u => u.Key + "=" + u.Value);
    11             //将param中的集合遍历用&拼接成字符串
    12             var finalParam = string.Join("&", param);
    13             return finalParam;
    14         }

    结果:

    2. 根据参数名的ASCII码由小到大排序

     eg:参数名分别为1、2、A、a、B、b,根据其ASCII排序,所以排序的结果为:1、2、A、B、a、b,我们最终想拼接的字符串的形式为:【1=hh&2=hh&A=hh&B=hh&a=hh&b=hh】

    代码分享:

    这里不能直接借助orderBy和Select可以实现ASCII排序,需要对orderBy利用CompareOrdinal进行改造,  然后利用Join方法进行拼接

     1         [HttpGet]
     2         public string TestParamASCII()
     3         {
     4             Dictionary<string, string> dics = new Dictionary<string, string>();
     5             dics.Add("1", "hh");
     6             dics.Add("2", "hh");
     7             dics.Add("A", "hh");
     8             dics.Add("a", "hh");
     9             dics.Add("B", "hh");
    10             dics.Add("b", "hh");
    11             var finalParam = GetParamSrc(dics);
    12             return finalParam;
    13         }
    14 
    15         /// <summary>
    16         /// 参数按照参数名ASCII码从小到大排序(字典序)
    17         /// </summary>
    18         /// <param name="paramsMap"></param>
    19         /// <returns></returns>
    20         public static string GetParamSrc(Dictionary<string, string> paramsMap)
    21         {
    22             //繁琐写法
    23             //var vDic = paramsMap.OrderBy(x => x.Key, new ComparerString()).ToDictionary(x => x.Key, y => y.Value);
    24             //StringBuilder str = new StringBuilder();
    25             //foreach (KeyValuePair<string, string> kv in vDic)
    26             //{
    27             //    string pkey = kv.Key;
    28             //    string pvalue = kv.Value;
    29             //    str.Append(pkey + "=" + pvalue + "&");
    30             //}
    31             //string result = str.ToString().Substring(0, str.ToString().Length - 1);
    32             //return result;
    33 
    34             //简介写法
    35             return string.Join("&", paramsMap.OrderBy(x => x.Key, new ComparerString()).Select(u => u.Key + "=" + u.Value));
    36         }
    37         public class ComparerString : IComparer<String>
    38         {
    39             public int Compare(String x, String y)
    40             {
    41                 return string.CompareOrdinal(x, y);
    42             }
    43         } 
    View Code

    结果:

     

    二. 扩展算法1

    1.前提

      有appKey、appSecret、sign这么几个参数,appKey和appSecret事先存在数据库里,且一一对应,服务商只把appKey和appSecret分发给调用者,调用者采用Get请求的方式,除了传递参数外,需要在报文头中传递appKey和sign这两个参数,其中sign的计算方法为把所有的参数的参数名按照正序排序,然后再和appSecret拼接起来,一起计算MD5值,

    即 MD5(a+b+c..+appSecret) → MD5("goodId=001&money=150&appSecret=0806")

    服务器端验证:见CheckPer1.cs 服务器端调用见:SDKClient类

    PS:这里也可以改成按照参数名的ASCII由小到大排序,就换成另外一种算法了。

    2.深度分析

      这种接口的验证规则适用于后台的代码调用,不适用js或其它前端调用,比如js调用的话,不但组装这个这种格式的参数麻烦,而且appSecret就和加密算法就直接暴露在外面了,当然你可以对js文件进行混淆来解决这个问题,但是这类接口还是更加适合后台代码调用,这样的话appSecret保存在服务器端,更加安全。

      即使appKey和sign这两个参数被截取了,也只能发相同数据的请求同一个接口,任何一个参数变化,sign均会发生变化,即验证不过去,相同的数据完全可以通过业务代码来限制。

    3.举一个使用场景

      一个App项目,有两个服务器,一个是业务服务器,一个是下单服务器,app项目请求的是业务服务器,不能直接请求下单服务器,但执行一个下单业务,流程如下:app采用jwt算法调用业务服务器→业务服务器进行jwt校验→校验通过→对参数进行组装MD5(a+b+c..+appSecret),调用下单服务器。

    注:业务服务器供app调用,采用jwt算法,下单服务器供业务服务器调用,采用上述扩展算法(a+b+c..+appSecret)

    4. 实战测试

      前提:appKey为:ypf 、appSecret为:0806, 这里我们就不再做JWT校验了,直接通过PostMan调用业务服务器,即:用PostMan调用:http://localhost:2131/api/Seventh/BuyGoods?goodId=001&money=150 模拟客户端请求。

    (1). 业务服务器代码 和封装的SDKClient类(对参数进行拼接,发送Get请求)

     1         /// <summary>
     2         /// 开放给客户端(模拟购买商品接口)
     3         /// 正常和客户端直接应该有验证,比如jwt验证,这里省略了,直接用postMan调用
     4         /// </summary>
     5         /// <returns></returns>
     6         [HttpGet]
     7         public async Task<string> BuyGoods(string goodId, int money)
     8         {
     9             //appKey和appScret分别为:ypf、0806
    10             SDKClient sdk = new SDKClient("ypf", "0806");
    11             //这里的userId实际应该从jwt中解析出来
    12             var payload = new Dictionary<string, object>
    13                     {
    14                          {"userId", "1" },
    15                          {"goodId", goodId },
    16                          {"money",money }
    17                     };
    18             SDKResult sdkResult = await sdk.GetAsync("http://localhost:2131/api/Seventh/CommitOrder", payload);
    19             return sdkResult.Result;
    20         }
     1  /// <summary>
     2     /// 封装请求类
     3     /// </summary>
     4     public class SDKClient
     5     {
     6         private string appKey;
     7         private string appSecret;
     8         public SDKClient(string appKey, string appSecret)
     9         {
    10             this.appKey = appKey;
    11             this.appSecret = appSecret;
    12         }
    13 
    14         /// <summary>
    15         ///封装发送请求的方法 
    16         /// </summary>
    17         /// <param name="url">请求地址</param>
    18         /// <param name="queryStringData">请求参数,键值对</param>
    19         /// <returns></returns>
    20         public async Task<SDKResult> GetAsync(string url, IDictionary<string, object> queryStringData)
    21         {
    22             if (queryStringData == null)
    23             {
    24                 throw new ArgumentNullException("queryStringData不能为null");
    25             }
    26             //根据key的参数名正序排序(首字母有小到大,首字母相同看第二个字母)
    27             var qsItems = queryStringData.OrderBy(kv => kv.Key).Select(kv => kv.Key + "=" + kv.Value);
    28             //循环遍历qsItems值,用&拼接起来
    29             var queryString = string.Join("&", qsItems);
    30             string finalStr = queryString + "&appSecret=" + appSecret;
    31             string sign = SecurityHelp.CalcMD5(finalStr);
    32             using (HttpClient hc = new HttpClient())
    33             {
    34                 hc.DefaultRequestHeaders.Add("appKey", appKey);
    35                 hc.DefaultRequestHeaders.Add("sign", sign);
    36                 var resp = await hc.GetAsync(url + "?" + queryString);
    37                 SDKResult sdkResult = new SDKResult();
    38                 sdkResult.Result = await resp.Content.ReadAsStringAsync();
    39                 sdkResult.StatusCode = resp.StatusCode;
    40                 return sdkResult;
    41             }
    42         }
    43 
    44     }
    45 
    46     public class SDKResult
    47     {
    48         public string Result { get; set; }
    49         public HttpStatusCode StatusCode { get; set; }
    50     }
    SDKClient

    (2).  过滤器代码和订单服务器代码

     1  /// <summary>
     2     /// 扩展算法一 的过滤器
     3     /// </summary>
     4     public class CheckPer1 : AuthorizeAttribute
     5     {
     6         public override void OnAuthorization(HttpActionContext actionContext)
     7         {
     8             //1.获取报文头中的appKey和sign
     9             IEnumerable<string> appKeys;
    10             if (!actionContext.Request.Headers.TryGetValues("appKey", out appKeys))
    11             {
    12                 //HttpContext.Current.Response.Write("报文头中的AppKey为空");  //这里不能这么返回,用Response.write不能截断,仍然会进入到方法中
    13                 actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("报文头中的appKey为空"));
    14             }
    15             IEnumerable<string> signs;
    16             if (!actionContext.Request.Headers.TryGetValues("sign", out signs))
    17             {
    18                 actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("报文头中的sign为空"));
    19             }
    20             string appKey = appKeys.First();
    21             string sign = signs.First();
    22             //2.根据appKey查询数据库获取appSecret
    23             //(这里进行模拟,暂不查询数据库  分别为ypf和0806代替)
    24             var appInfor = new AppInfor()
    25             {
    26                 AppKey = "ypf",
    27                 AppSecret = "0806",
    28                 IsEnabled = false
    29             };
    30             if (appInfor == null)
    31             {
    32                 actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("该appKey不存在"));
    33             }
    34             //if (!appInfor.IsEnabled)
    35             //{
    36             //    actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("该AppKey已被封禁"));
    37             //}
    38 
    39             //3.计算用户输入参数的连接+AppSecret的Md5值 
    40             //orderedQS就是按照key(参数的名字)进行排序的QueryString集合 
    41             var orderedQS = actionContext.Request.GetQueryNameValuePairs().OrderBy(kv => kv.Key);
    42             //拼接key=value的数组
    43             var segments = orderedQS.Select(kv => kv.Key + "=" + kv.Value);
    44             //用&符号拼接起来
    45             string queryString = string.Join("&", segments);
    46             //密钥统一用0806表示
    47             string finalStr = queryString + "&appSecret=" + "0806";
    48             //计算Sign值
    49             string computedSign = SecurityHelp.CalcMD5(finalStr);
    50             //4. 用户传进来md5值和计算出来的比对一下,就知道数据是否有被篡改过
    51             if (sign.Equals(computedSign, StringComparison.CurrentCultureIgnoreCase))
    52             {
    53                 //表示检验通过
    54             }
    55             else
    56             {
    57                 actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("sign校验失败"));
    58             }
    59 
    60         }
     1         /// <summary>
     2         /// 模拟订单服务器中的下单接口
     3         /// 该接口需要采用 扩展算法(一) 的校验
     4         /// </summary>
     5         /// <param name="dic"></param>
     6         /// <returns></returns>
     7         [HttpGet]
     8         [CheckPer1]
     9         public string CommitOrder(string userId, string goodId, int money)
    10         {
    11             var data = new
    12             {
    13                 stauts = "ok",
    14                 msg = userId + "," + goodId + "," + money
    15             };
    16             return JsonConvert.SerializeObject(data);
    17         }

    (3). 运行结果

     

    三. 扩展算法2

    1.前提

      有appkey、appSecret、timeStamp这么几个参数,其中appkey和appSecret实现存在数据库里,且一一对应,服务商把appkey和appSecret分发给调用者, 调用者采用Post请求的方式,提交和返回的数据都为Json格式,Http请求的头文件中要加“content-type: application/json”,字符编码 统一采用UTF8,签名算法如下:

    finalStr=(appkey+appSecret+timeStamp)的值全部转换为大写字符

    sign=MD5(finalStr),

    每个请求的报文头要有传 appkey、timeStamp(时间戳)、sign(签名)值过来统一校验。时间戳 timestamp是14位标准的时间戳格式,时间戳有效期为 10 分钟, 服务器时间减时间戳大于 10 分钟的一律视为过期,签名会失败。 在服务器端进行验证。

    2. 深度分析

      这种接口的验证规则适用于后台的代码调用,不适用js或其它前端调用,比如js调用的话,appSecret就和加密算法就直接暴露在外面了,非常危险。当然你可以对js文件进行混淆来解决这个问题,但是这类接口还是更加适合后台代码调用,这样的话appSecret保存在服务器端,更加安全。

      即使 Aappkey、timeStamp(时间戳)、sign(签名)参数被截取了,访问该接口或者其它接口,也只能在10分钟能有效。

    3.举一个使用场景

      一个App项目,有两个服务器,一个是业务服务器,一个是下单服务器,app项目请求的是业务服务器,不能直接请求下单服务器,但执行一个下单业务,流程如下:app采用jwt算法调用业务服务器→业务服务器进行jwt校验→校验通过→对参数进行组装sign、appkey、timeStamp,调用下单服务器。

    注:业务服务器供app调用,采用jwt算法,下单服务器供业务服务器调用,采用上述扩展算法MD5(appkey+appSecret+timestamp)。

    4. 实战测试

      前提:appKey为:ypf 、appSecret为:0806, 这里我们就不再做JWT校验了,直接通过PostMan调用业务服务器,即:用PostMan调用:http://localhost:2131/api/Seventh/BuyGoods2?goodId=001&money=150 模拟客户端请求

     (1). 业务服务器代码 和封装的SDKClient2类(对参数进行拼接,发送Post请求)

     1         /// <summary>
     2         /// 开放给客户端(模拟购买商品接口)
     3         /// 正常和客户端直接应该有验证,比如jwt验证,这里省略了,直接用postMan调用
     4         /// </summary>
     5         /// <returns></returns>
     6         [HttpGet]
     7         public async Task<string> BuyGoods2(string goodId, int money)
     8         {
     9             //appKey和appScret分别为:ypf、0806
    10             SDKClient2 sdk = new SDKClient2("ypf", "0806", DateTime.Now.ToString("yyyyMMddhhmmss"));
    11             //这里的userId正常应该从jwt字符串中解析出来
    12             var payload = new
    13             {
    14                 userId = "1",
    15                 goodId = goodId,
    16                 money = money
    17             };
    18             SDKResult sdkResult = await sdk.PostAsync("http://localhost:2131/api/Seventh/CommitOrder2", payload);
    19             return sdkResult.Result;
    20         }
     1     /// <summary>
     2     /// 扩展算法二的封装调用
     3     /// </summary>
     4     public class SDKClient2
     5     {
     6         private string appKey;
     7         private string appSecret;
     8         private string timeStamp;
     9         public SDKClient2(string appKey, string appSecret,string timeStamp)
    10         {
    11             this.appKey = appKey;
    12             this.appSecret = appSecret;
    13             this.timeStamp = timeStamp;
    14         }
    15 
    16         public async Task<SDKResult> PostAsync(string url, dynamic data)
    17         {
    18             if (data == null)
    19             {
    20                 throw new ArgumentNullException("data不能为null");
    21             }
    22 
    23             using (HttpClient hc = new HttpClient())
    24             {
    25                 string finalStr = (appKey + appSecret + timeStamp).ToUpper();
    26                 string sign= SecurityHelp.CalcMD5(finalStr);
    27 
    28                 hc.DefaultRequestHeaders.Add("appKey", appKey);
    29                 hc.DefaultRequestHeaders.Add("timeStamp", timeStamp);
    30                 hc.DefaultRequestHeaders.Add("sign", sign);
    31 
    32                 var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json");
    33                 var resp = await hc.PostAsync(url, content);
    34                 SDKResult sdkResult = new SDKResult();
    35                 sdkResult.Result = await resp.Content.ReadAsStringAsync();
    36                 sdkResult.StatusCode = resp.StatusCode;
    37                 return sdkResult;
    38             }
    39         }
    SDKClient2

    (2).  过滤器代码和订单服务器代码

     1     /// <summary>
     2     ///扩展算法二 的过滤器
     3     ///换一种新的过滤器写法
     4     /// </summary>
     5     public class CheckPer2 : FilterAttribute, IAuthorizationFilter
     6     {
     7         public async Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
     8         {
     9             //1.获取Appkey、Timestamp(时间戳)、Sign(签名)
    10             IEnumerable<string> appKeys;
    11             if (!actionContext.Request.Headers.TryGetValues("appKey", out appKeys))
    12             {
    13                 return new HttpResponseMessage(HttpStatusCode.Unauthorized) { Content = new StringContent("报文头中的appKey为空") };
    14             }
    15             IEnumerable<string> timestamps;
    16             if (!actionContext.Request.Headers.TryGetValues("timeStamp", out timestamps))
    17             {
    18                 return new HttpResponseMessage(HttpStatusCode.Unauthorized) { Content = new StringContent("报文头中的timeStamp为空") };
    19             }
    20             IEnumerable<string> signs;
    21             if (!actionContext.Request.Headers.TryGetValues("sign", out signs))
    22             {
    23                 return new HttpResponseMessage(HttpStatusCode.Unauthorized) { Content = new StringContent("报文头中的sign为空") };
    24             }
    25             string Appkey = appKeys.First();
    26             string Timestamp = timestamps.First();
    27             string Sign = signs.First();
    28             //2. 计算Timestamp是否过期(要注意小时制问题, 需要统一下面的这种方式转换)
    29             long nowTimeStr = long.Parse(DateTime.Now.ToString("yyyyMMddhhmmss"));
    30             long timeStampStr = long.Parse(Timestamp);
    31             if (nowTimeStr - timeStampStr > 600 || timeStampStr - nowTimeStr > 600)
    32             {
    33                 return new HttpResponseMessage(HttpStatusCode.Unauthorized) { Content = new StringContent("已过期") };
    34             }
    35             //3. 计算Sign值是否合法
    36             //这里假设秘钥为0806 (实际应该根据appKey去数据库中查)
    37             string AppSecret = "0806";
    38             string finalStr = (Appkey + AppSecret + Timestamp).ToUpper();
    39             string realSign = SecurityHelp.CalcMD5(finalStr);
    40             if (realSign.Equals(Sign, StringComparison.OrdinalIgnoreCase))
    41             {
    42                 //表示校验通过
    43                 return await continuation();
    44             }
    45             else
    46             {
    47                 //表示校验未通过 
    48                 return new HttpResponseMessage(HttpStatusCode.Unauthorized) { Content = new StringContent("sign验证失败") };
    49             }
    50         }
    51     }
     1        /// <summary>
     2         /// 模拟订单服务器中的下单接口
     3         /// 该接口需要采用 扩展算法(二) 的校验
     4         /// </summary>
     5         /// <param name="dic"></param>
     6         /// <returns></returns>
     7         [HttpPost]
     8         [CheckPer2]
     9         public string CommitOrder2(orderData orderData)
    10         {
    11             var data = new
    12             {
    13                 stauts = "ok",
    14                 msg = orderData.userId + "," + orderData.goodId + "," + orderData.money
    15             };
    16 
    17             return JsonConvert.SerializeObject(data);
    18         }

    (3). 运行结果

    !

    • 作       者 : Yaopengfei(姚鹏飞)
    • 博客地址 : http://www.cnblogs.com/yaopengfei/
    • 声     明1 : 本人才疏学浅,用郭德纲的话说“我是一个小学生”,如有错误,欢迎讨论,请勿谩骂^_^。
    • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
     
  • 相关阅读:
    nullnullConnecting with WiFi Direct 与WiFi直接连接
    nullnullUsing WiFi Direct for Service Discovery 直接使用WiFi服务发现
    nullnullSetting Up the Loader 设置装载机
    nullnullDefining and Launching the Query 定义和启动查询
    nullnullHandling the Results 处理结果
    装置输出喷泉装置(贪心问题)
    数据状态什么是事务?
    停止方法iOS CGD 任务开始与结束
    盘文件云存储——金山快盘
    函数标识符解决jQuery与其他库冲突的方法
  • 原文地址:https://www.cnblogs.com/yaopengfei/p/10468728.html
Copyright © 2011-2022 走看看