第十节:进一步扩展两种安全校验方式

依然范特西╮ 提交于 2020-02-28 13:55:38

一. 简介

简介: 上一节中,主要介绍了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         } 

复制代码

结果:

 

 

二. 扩展算法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     }

复制代码

(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         }

复制代码

 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). 运行结果

 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!