WebAPi接口安全之公鑰私鑰加密

WebAPi使用公鑰私鑰加密介紹和使用

隨着各類設備的興起,WebApi做爲服務也愈來愈流行。而在無任何保護措施的狀況下接口徹底暴露在外面,將致使被惡意請求。最近項目的項目中因爲提供給APP的接口未對接口進行時間防範致使短信接口被怒對形成必定的損失,臨時的措施致使PC和app的防止措施不同致使後來前端調用至關痛苦,選型過oauth,https,固然都被上級未經過,那就只能本身寫了,就很,,ԾㅂԾ,,。下面就這次的方式作一次記錄。最終的效果:傳輸過程當中都是密文,別人拿到請求串不能更改請求參數,經過接口過時時間防止同一請求串一直被調用。

前端

   第一步重寫MessageProcessingHandler中的ProcessRequest和ProcessResponse


 

不管是APi仍是Mvc請求管道都提供了咱們很好的去擴展,本次說的是api,其實mvc大概意思也是差很少的。咱們如今主要寫出大體流程web

從圖中能夠看出咱們須要在MessageProcessingHandlder上作處理。咱們繼承MessageProcessingHandlder重寫ProcessRequest和ProcessResponse方法,從方法名能夠看出一個是針對請求值處理,一個是針對返回值處理代碼以下:json

 1  public class CustomerMessageProcesssingHandler : MessageProcessingHandler
 2     {
 3         protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken)
 4         {
 5             var contentType = request.Content.Headers.ContentType;
 6 
 7             if (!request.Headers.Contains("platformtype"))
 8             {
 9                 return request;
10             }
11             //根據平臺編號得到對應私鑰
12             string privateKey = Encoding.UTF8.GetString(Convert.FromBase64String(ConfigurationManager.AppSettings["PlatformPrivateKey_" + request.Headers.GetValues("platformtype").FirstOrDefault()]));
13             if (request.Method == HttpMethod.Post)
14             {
15                 // 讀取請求body中的數據
16                 string baseContent = request.Content.ReadAsStringAsync().Result;
17                 // 獲取加密的信息
18                 // 兼容 body: 加密數據  和 body: sign=加密數據
19                 baseContent = Regex.Match(baseContent, "(sign=)*(?<sign>[\\S]+)").Groups[2].Value;
20                 // 用加密對象解密數據
21                 baseContent = CommonHelper.RSADecrypt(privateKey, baseContent);
22                 // 將解密後的BODY數據 重置
23                 request.Content = new StringContent(baseContent);
24                 //此contentType必須最後設置 不然會變成默認值
25                 request.Content.Headers.ContentType = contentType;
26             }
27             if (request.Method == HttpMethod.Get)
28             {
29                 string baseQuery = request.RequestUri.Query;
30                 // 讀取請求 url query數據
31                 baseQuery = baseQuery.Substring(1);
32                 baseQuery = Regex.Match(baseQuery, "(sign=)*(?<sign>[\\S]+)").Groups[2].Value;
33                 baseQuery = CommonHelper.RSADecrypt(privateKey, baseQuery);
34                 // 將解密後的 URL 重置URL請求
35                 request.RequestUri = new Uri($"{request.RequestUri.AbsoluteUri.Split('?')[0]}?{baseQuery}");
36             }
37             return request;
38         }
39         protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken)
40         {
41             return response;
42         }
43     }
上面的代碼大部分已經有註釋了,但這裏說明三點第一:platformtype用來針對不一樣的平臺設置不一樣的公鑰和私鑰;第二:在post方法中ContentType必定要最後設置,不然會成爲默認值,這個問題會致使webapi不能進行正確的參數綁定;第三:有人可能會問這裏ProcessResponse是否是能夠不用重寫?答案是必須重寫若是不想對結果操做直接返回就如上固然你也能夠在此對返回值進行加密,可是我的認爲意義不大,看具體狀況由於大部分數據加密後前端仍是須要解密而後展現因此此處不作任何處理。在這一步咱們已經對前端請求的加密串在handler中處理成明文從新賦值給HttpRequestMessage。
 

  第二步重寫AuthorizeAttribute中OnAuthorization和HandleUnauthorizedRequest


 


上圖是Api中Filter的請求順序,咱們在第一個Filter上處理,代碼以下:
 1    public class CustomRequestAuthorizeAttribute : AuthorizeAttribute
 2     {
 3 
 4         public override void OnAuthorization(HttpActionContext actionContext)
 5         {
 6             //action具備[AllowAnonymous]特性不參與驗證
 7             if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().OfType<AllowAnonymousAttribute>().Any(x => x is AllowAnonymousAttribute))
 8             {
 9                 base.OnAuthorization(actionContext);
10                 return;
11             }
12             var request = actionContext.Request;
13             string method = request.Method.Method, timeStamp = string.Empty, expireyTime = ConfigurationManager.AppSettings["UrlExpireTime"], timeSign = string.Empty, platformType = string.Empty;
14             if (!request.Headers.Contains("timesign") || !request.Headers.Contains("platformtype") || !request.Headers.Contains("timestamp") || !request.Headers.Contains("expiretime"))
15             {
16                 HandleUnauthorizedRequest(actionContext);
17                 return;
18             }
19             platformType = request.Headers.GetValues("platformtype").FirstOrDefault();
20             timeSign = request.Headers.GetValues("timesign").FirstOrDefault();
21             timeStamp = request.Headers.GetValues("timestamp").FirstOrDefault();
22             var tempExpireyTime = request.Headers.GetValues("expiretime").FirstOrDefault();
23             string privateKey = Encoding.UTF8.GetString(Convert.FromBase64String(ConfigurationManager.AppSettings[$"PlatformPrivateKey_{platformType}"]));
24             if (!SignValidate(tempExpireyTime, privateKey, timeStamp, timeSign))
25             {
26                 HandleUnauthorizedRequest(actionContext);
27                 return;
28             }
29             if (tempExpireyTime != "0")
30             {
31                 expireyTime = tempExpireyTime;
32             }
33             //判斷timespan是否有效
34             double ts2 = ConvertHelper.ToDouble((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds, 2), ts = ts2 - ConvertHelper.ToDouble(timeStamp);
35             bool falg = ts > int.Parse(expireyTime) * 1000;
36             if (falg)
37             {
38                 HandleUnauthorizedRequest(actionContext);
39                 return;
40             }
41             base.IsAuthorized(actionContext);
42         }
43         protected override void HandleUnauthorizedRequest(HttpActionContext filterContext)
44         {
45             base.HandleUnauthorizedRequest(filterContext);
46 
47             var response = filterContext.Response = filterContext.Response ?? new HttpResponseMessage();
48             response.StatusCode = HttpStatusCode.Forbidden;
49             var content = new
50             {
51                 BusinessStatus = -10403,
52                 StatusMessage = "服務端拒絕訪問"
53             };
54             response.Content = new StringContent(JsonConvert.SerializeObject(content), Encoding.UTF8, "application/json");
55         }
56         private bool SignValidate(string expiryTime, string privateKey, string timestamp, string sign)
57         {
58             bool isValidate = false;
59             var tempSign = CommonHelper.RSADecrypt(privateKey, sign);
60             if (CommonHelper.EncryptSHA256($"expiretime{expiryTime}" + $"timestamp{timestamp}") == tempSign)
61             {
62                 isValidate = true;
63             }
64             return isValidate;
65         }
66     }

請求頭部增長參數expiretime使用此參數做爲本次接口的過時時間若是沒有則表示使用平臺默認的接口時間,是咱們能夠針對不一樣的接口設置不一樣的過時時間;timestamp請求時間戳來防止別人拿到接口後一直調用timesign是過時時間和時間戳經過hash而後在經過公鑰加密的串來防止別人修改前兩個參數。重寫HandleUnauthorizedRequest來設置返回內容。api


 

至此整個驗證過程就結束了,咱們在使用過程當中能夠創建BaseApi將特性標記上讓其餘APi繼承,固然咱們的接口中可能有的action不須要驗證看OnAuthorization第一行代碼 增長相應的特性跳過此驗證。在整個過程當中其實咱們已經使用了兩種加密方式。一是本文中的CustomerMessageProcesssingHandler;另一種就是timestamp+QueryString而後hash 在公鑰加密 這樣就不須要CustomerMessageProcesssingHandler其實就是本文中的頭部加密方式。mvc

補充:園友建議增長請求端實例,確實是昨天有所遺漏。趁不忙補充上:

本次以HttpClient調用方式爲例,展現Get,Post請求加密到執行的相應的action的過程;首先看一下Get請求以下:app

能夠看到咱們的請求串url已是密文,頭部時間sign也是密文,除非別人拿到咱們的私鑰否則是不能修改其參數的。而後請求到達咱們的CustomerMessageProcesssingHandler中咱們看下Get中獲得的參數是:ide

這是咱們獲得的前端傳過來的querystring的參數他的值就是咱們前端加密後傳過來的下一步咱們解密應該要獲得未加密以前的參數也就是客戶端中id=1同時從新給requesturi賦值;post

結果中咱們能夠看到id=1已被正確解密獲得。接下來進入咱們的CustomRequestAuthorizeAttribute加密

在這一步咱們進行對timeSign的解密對請求只進行hash對比而後驗證時間戳是否在過時時間內最終咱們到達相應的action:url

這樣整個請求也就完成了Post跟Get區別不大重要的在於拿到傳遞參數的地方不同這裏我只貼一下調用的代碼過程同上:

 1   public static void PostTestByModel() {
 2 
 3             HttpClient http = new HttpClient();
 4             var timestamp = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds;
 5             var expiretime = "600";
 6             var timesign = RSAEncrypt(publicKey, EncryptSHA256($"expiretime{expiretime}timestamp{timestamp}"));
 7             var codeValue = RSAEncrypt(publicKey, JsonConvert.SerializeObject(new Tenmp { Id = 1, Name = "cl" }));
 8             http.DefaultRequestHeaders.Add("platformtype", "Web");
 9             http.DefaultRequestHeaders.Add("timesign", $"{timesign}");
10             http.DefaultRequestHeaders.Add("timestamp", $"{string.Format("{0:N2}", timestamp.ToString()) }");
11             http.DefaultRequestHeaders.Add("expiretime", expiretime);
12             var url1 = string.Format($"{host}api/Values/PostTestByModel");
13             HttpContent content = new StringContent(codeValue);
14             MediaTypeHeaderValue typeHeader = new MediaTypeHeaderValue("application/json");
15             typeHeader.CharSet = "UTF-8";
16             content.Headers.ContentType = typeHeader;
17             var response1 = http.PostAsync(url1, content).Result;
18         }

最後當驗證不經過獲得的返回值:

這也就是重寫HandleUnauthorizedRequest的目的 固然你也能夠不重寫此方法那麼返回的就是401 英文的未經過驗證。

相關文章
相關標籤/搜索