第十節:進一步擴展兩種安全校驗方式


一. 簡介

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM