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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
     
  • 相关阅读:
    Java虚拟机(JVM)中的内存设置详解
    设置TOMCAT的JVM虚拟机内存大小
    什么是SQL注入式攻击
    常见的数据库基础面试题大全
    Struts1与Struts2的12点区别
    sql server导出insert语句
    jsp 将html字符串输出html标签
    GC 基础
    jstl fn标签
    Struts2 中result type属性说明
  • 原文地址:https://www.cnblogs.com/yaopengfei/p/10468728.html
Copyright © 2011-2022 走看看