一. 简介
简介: 上一节中,主要介绍了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 SDKClient5 {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 : AuthorizeAttribute5 {6 public override void OnAuthorization(HttpActionContext actionContext)7 {8 //1.获取报文头中的appKey和sign9 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, IAuthorizationFilter6 {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). 运行结果