第十节:进一步扩展两种安全校验方式
一. 简介
简介: 上一节中,主要介绍了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保存在服务器端,更加安全。
即使 Appkey、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 }
(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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。