1. 程式人生 > >WebAPi介面安全之公鑰私鑰加密

WebAPi介面安全之公鑰私鑰加密

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

隨著各種裝置的興起,WebApi作為服務也越來越流行。而在無任何保護措施的情況下介面完全暴露在外面,將導致被惡意請求。最近專案的專案中由於提供給APP的介面未對介面進行時間防範導致簡訊介面被怒對造成一定的損失,臨時的措施導致PC和app的防止措施不一樣導致後來前端呼叫相當痛苦,選型過oauth,https,當然都被上級未通過,那就只能自己寫了,就很,,ԾㅂԾ,,。下面就此次的方式做一次記錄。最終的效果:傳輸過程中都是密文,別人拿到請求串不能更改請求引數,通過介面過期時間防止同一請求串一直被呼叫。

   第一步重寫MessageProcessingHandler中的ProcessRequest和ProcessResponse

無論是APi還是Mvc請求管道都提供了我們很好的去擴充套件,本次說的是api,其實mvc大概意思也是差不多的。我們現在主要寫出大致流程

從圖中可以看出我們需要在MessageProcessingHandlder上做處理。我們繼承MessageProcessingHandlder重寫ProcessRequest和ProcessResponse方法,從方法名可以看出一個是針對請求值處理,一個是針對返回值處理程式碼如下:

public class CustomerMessageProcesssingHandler : MessageProcessingHandler
    {
        protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var contentType = request.Content.Headers.ContentType;

            if (!request.Headers.Contains("platformtype"))
            {
                return request;
            }
            //根據平臺編號獲得對應私鑰
            string privateKey = Encoding.UTF8.GetString(Convert.FromBase64String(ConfigurationManager.AppSettings["PlatformPrivateKey_" + request.Headers.GetValues("platformtype").FirstOrDefault()]));
            if (request.Method == HttpMethod.Post)
            {
                // 讀取請求body中的資料
                string baseContent = request.Content.ReadAsStringAsync().Result;
                // 獲取加密的資訊
                // 相容 body: 加密資料  和 body: sign=加密資料
                baseContent = Regex.Match(baseContent, "(sign=)*(?<sign>[\\S]+)").Groups[2].Value;
                // 用加密物件解密資料
                baseContent = CommonHelper.RSADecrypt(privateKey, baseContent);
                // 將解密後的BODY資料 重置
                request.Content = new StringContent(baseContent);
                //此contentType必須最後設定 否則會變成預設值
                request.Content.Headers.ContentType = contentType;
            }
            if (request.Method == HttpMethod.Get)
            {
                string baseQuery = request.RequestUri.Query;
                // 讀取請求 url query資料
                baseQuery = baseQuery.Substring(1);
                baseQuery = Regex.Match(baseQuery, "(sign=)*(?<sign>[\\S]+)").Groups[2].Value;
                baseQuery = CommonHelper.RSADecrypt(privateKey, baseQuery);
                // 將解密後的 URL 重置URL請求
                request.RequestUri = new Uri($"{request.RequestUri.AbsoluteUri.Split('?')[0]}?{baseQuery}");
            }
            return request;
        }
        protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken)
        {
            return response;
        }
    }

上面的程式碼大部分已經有註釋了,但這裡說明三點第一:platformtype用來針對不同的平臺設定不同的公鑰和私鑰;第二:在post方法中ContentType一定要最後設定,否則會成為預設值,這個問題會導致webapi不能進行正確的引數繫結;第三:有人可能會問這裡ProcessResponse是不是可以不用重寫?答案是必須重寫如果不想對結果操作直接返回就如上當然你也可以在此對返回值進行加密,但是個人認為意義不大,看具體情況因為大部分資料加密後前端還是需要解密然後展示所以此處不做任何處理。在這一步我們已經對前端請求的加密串在handler中處理成明文重新賦值給HttpRequestMessage。

  第二步重寫AuthorizeAttribute中OnAuthorization和HandleUnauthorizedRequest


上圖是Api中Filter的請求順序,我們在第一個Filter上處理,程式碼如下:
public class CustomRequestAuthorizeAttribute : AuthorizeAttribute
    {

        public override void OnAuthorization(HttpActionContext actionContext)
        {
            //action具有[AllowAnonymous]特性不參與驗證
            if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().OfType<AllowAnonymousAttribute>().Any(x => x is AllowAnonymousAttribute))
            {
                base.OnAuthorization(actionContext);
                return;
            }
            var request = actionContext.Request;
            string method = request.Method.Method, timeStamp = string.Empty, expireyTime = ConfigurationManager.AppSettings["UrlExpireTime"], timeSign = string.Empty, platformType = string.Empty;
            if (!request.Headers.Contains("timesign") || !request.Headers.Contains("platformtype") || !request.Headers.Contains("timestamp") || !request.Headers.Contains("expiretime"))
            {
                HandleUnauthorizedRequest(actionContext);
                return;
            }
            platformType = request.Headers.GetValues("platformtype").FirstOrDefault();
            timeSign = request.Headers.GetValues("timesign").FirstOrDefault();
            timeStamp = request.Headers.GetValues("timestamp").FirstOrDefault();
            var tempExpireyTime = request.Headers.GetValues("expiretime").FirstOrDefault();
            string privateKey = Encoding.UTF8.GetString(Convert.FromBase64String(ConfigurationManager.AppSettings[$"PlatformPrivateKey_{platformType}"]));
            if (!SignValidate(tempExpireyTime, privateKey, timeStamp, timeSign))
            {
                HandleUnauthorizedRequest(actionContext);
                return;
            }
            if (tempExpireyTime != "0")
            {
                expireyTime = tempExpireyTime;
            }
            //判斷timespan是否有效
            double ts2 = ConvertHelper.ToDouble((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds, 2), ts = ts2 - ConvertHelper.ToDouble(timeStamp);
            bool falg = ts > int.Parse(expireyTime) * 1000;
            if (falg)
            {
                HandleUnauthorizedRequest(actionContext);
                return;
            }
            base.IsAuthorized(actionContext);
        }
        protected override void HandleUnauthorizedRequest(HttpActionContext filterContext)
        {
            base.HandleUnauthorizedRequest(filterContext);

            var response = filterContext.Response = filterContext.Response ?? new HttpResponseMessage();
            response.StatusCode = HttpStatusCode.Forbidden;
            var content = new
            {
                BusinessStatus = -10403,
                StatusMessage = "服務端拒絕訪問"
            };
            response.Content = new StringContent(JsonConvert.SerializeObject(content), Encoding.UTF8, "application/json");
        }
        private bool SignValidate(string expiryTime, string privateKey, string timestamp, string sign)
        {
            bool isValidate = false;
            var tempSign = CommonHelper.RSADecrypt(privateKey, sign);
            if (CommonHelper.EncryptSHA256($"expiretime{expiryTime}" + $"timestamp{timestamp}") == tempSign)
            {
                isValidate = true;
            }
            return isValidate;
        }
    }

請求頭部增加引數expiretime使用此引數作為本次介面的過期時間如果沒有則表示使用平臺預設的介面時間,是我們可以針對不同的介面設定不同的過期時間;timestamp請求時間戳來防止別人拿到介面後一直呼叫timesign是過期時間和時間戳通過hash然後在通過公鑰加密的串來防止別人修改前兩個引數。重寫HandleUnauthorizedRequest來設定返回內容。

至此整個驗證過程就結束了,我們在使用過程中可以建立BaseApi將特性標記上讓其他APi繼承,當然我們的介面中可能有的action不需要驗證看OnAuthorization第一行程式碼 增加相應的特性跳過此驗證。在整個過程中其實我們已經使用了兩種加密方式。一是本文中的CustomerMessageProcesssingHandler;另外一種就是timestamp+QueryString然後hash 在公鑰加密 這樣就不需要CustomerMessageProcesssingHandler其實就是本文中的頭部加密方式。

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

本次以HttpClient呼叫方式為例,展示Get,Post請求加密到執行的相應的action的過程;首先看一下Get請求如下:

可以看到我們的請求串url已經是密文,頭部時間sign也是密文,除非別人拿到我們的私鑰不然是不能修改其引數的。然後請求到達我們的CustomerMessageProcesssingHandler中我們看下Get中得到的引數是:

這是我們得到的前端傳過來的querystring的引數他的值就是我們前端加密後傳過來的下一步我們解密應該要得到未加密之前的引數也就是客戶端中id=1同時重新給requesturi賦值;

結果中我們可以看到id=1已被正確解密得到。接下來進入我們的CustomRequestAuthorizeAttribute

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

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

public static void PostTestByModel() {

            HttpClient http = new HttpClient();
            var timestamp = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds;
            var expiretime = "600";
            var timesign = RSAEncrypt(publicKey, EncryptSHA256($"expiretime{expiretime}timestamp{timestamp}"));
            var codeValue = RSAEncrypt(publicKey, JsonConvert.SerializeObject(new Tenmp { Id = 1, Name = "cl" }));
            http.DefaultRequestHeaders.Add("platformtype", "Web");
            http.DefaultRequestHeaders.Add("timesign", $"{timesign}");
            http.DefaultRequestHeaders.Add("timestamp", $"{string.Format("{0:N2}", timestamp.ToString()) }");
            http.DefaultRequestHeaders.Add("expiretime", expiretime);
            var url1 = string.Format($"{host}api/Values/PostTestByModel");
            HttpContent content = new StringContent(codeValue);
            MediaTypeHeaderValue typeHeader = new MediaTypeHeaderValue("application/json");
            typeHeader.CharSet = "UTF-8";
            content.Headers.ContentType = typeHeader;
            var response1 = http.PostAsync(url1, content).Result;
        }

最後當驗證不通過得到的返回值:

這也就是重寫HandleUnauthorizedRequest的目的 當然你也可以不重寫此方法那麼返回的就是401 英文的未通過驗證。

轉自:http://www.cnblogs.com/clly/p/7384008.html