一. 簡介
簡介: 上一節中,主要介紹了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 }

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 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。