WebApi接口安全性 接口權限調用、參數防篡改防止惡意調用

背景介紹

最近使用WebApi開發一套對外接口,主要是數據的外送以及結果回傳,接口沒什麼難度,採用WebApi+EF的架構簡單建立一個模板工程,使用template生成一套WebApi接口,去掉put、delete等操做,修改一下就能夠上線。這些都不在話下,反正網上一大堆教程,隨便找那個step by step作下來就能夠了。html

 

而後發佈上線後,接口是放在外網,面臨兩個問題:web

  1. 如何保證接口的調用的合法性
  2. 如何保證接口及數據的安全性

其實這兩個問題是相互結合的,先保證合法,而後在合法基礎上保證請求的惟一性,避免參數被篡改。算法

鑑於接口上線期限緊迫,結合衆多案例,先解決掉接口調用數據的安全性問題,這裏採用了RSA報文加解密的方案,保證數據安全和防止接口被惡意調用以及參數篡改的問題。api

本文參考博客園多篇博文,內容多有引用,文末附有參照博文的地址。數組

如下爲正文!安全

正文

首先,接口面臨的問題:

  1. 請求來源(身份)是否合法(部分解決,後續在處理)
  2. 請求參數被篡改?
  3. 請求的惟一性(不可複製),防止請求被惡意攻擊

 

解決方案:

 

  1. 參數加密: 客戶端和服務端參數採用RSA加密後傳遞,原則上只有持有私鑰的服務端才能解密客戶端公鑰加密的參數,避免了參數篡改的問題
  2. 請求籤名:採用一套簽名算法,對請求進行簽名驗證,保證請求的惟一性

 

這裏參照了WebAPi使用公鑰私鑰加密介紹和使用 一文,進行公鑰私鑰加解密的處理服務器

先說服務端:微信

擴展 MessageProcessingHandler

先看一下MessageProcessingHandler的介紹:架構

#region 程序集 System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
// C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.2\System.Net.Http.dll
#endregion

using System.Threading;
using System.Threading.Tasks;

namespace System.Net.Http
{
    //
    // 摘要:
    //     僅對請求和/或響應消息進行一些小型處理的處理程序的基類。
    public abstract class MessageProcessingHandler : DelegatingHandler
    {
        //
        // 摘要:
        //     建立的一個實例 System.Net.Http.MessageProcessingHandler 類。
        protected MessageProcessingHandler();
        //
        // 摘要:
        //     建立的一個實例 System.Net.Http.MessageProcessingHandler 具備特定的內部處理程序類。
        //
        // 參數:
        //   innerHandler:
        //     內部處理程序負責處理 HTTP 響應消息。
        protected MessageProcessingHandler(HttpMessageHandler innerHandler);

        //
        // 摘要:
        //     處理每一個發送到服務器的請求。
        //
        // 參數:
        //   request:
        //     要處理的 HTTP 請求消息。
        //
        //   cancellationToken:
        //     可由其餘對象或線程用以接收取消通知的取消標記。
        //
        // 返回結果:
        //     已處理的 HTTP 請求消息。
        protected abstract HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken);
        //
        // 摘要:
        //     處理來自服務器的每一個響應。
        //
        // 參數:
        //   response:
        //     要處理的 HTTP 響應消息。
        //
        //   cancellationToken:
        //     可由其餘對象或線程用以接收取消通知的取消標記。
        //
        // 返回結果:
        //     已處理的 HTTP 響應消息。
        protected abstract HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken);
        //
        // 摘要:
        //     異步發送 HTTP 請求到要發送到服務器的內部處理程序。
        //
        // 參數:
        //   request:
        //     要發送到服務器的 HTTP 請求消息。
        //
        //   cancellationToken:
        //     可由其餘對象或線程用以接收取消通知的取消標記。
        //
        // 返回結果:
        //     表示異步操做的任務對象。
        //
        // 異常:
        //   T:System.ArgumentNullException:
        //     request 是 null。
        protected internal sealed override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
    }
}

 

擴展這個類的目的是解密參數,其實也能夠推遲到Action過濾器中作,可是仍是以爲時機上在這裏處理比較合適。具體的建議瞭解一下WebApi消息管道以及擴展過濾器的相關文章,本文再也不延伸。異步

下面是擴展的實現代碼:

 

/// <summary>
    /// 請求預處理,報文解密
    /// </summary>
    /// <seealso cref="System.Net.Http.MessageProcessingHandler"/>
    public class ArgDecryptMessageProcesssingHandler : MessageProcessingHandler
    {

        /// <summary>
        /// 處理每一個發送到服務器的請求。
        /// </summary>
        /// <param name="request">          要處理的 HTTP 請求消息。</param>
        /// <param name="cancellationToken">可由其餘對象或線程用以接收取消通知的取消標記。</param>
        /// <returns>已處理的 HTTP 請求消息。</returns>
        protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var contentType = request.Content.Headers.ContentType;

            //swagger請求直接跳過不予處理
            if (request.RequestUri.AbsolutePath.Contains("/swagger"))
            {
                return request;
            }

            //得到平臺私鑰
            string privateKey = Common.GetRsaPrivateKey();

            //獲取Get中的Query信息,解密後重置請求上下文
            if (request.Method == HttpMethod.Get)
            {
                string baseQuery = request.RequestUri.Query;
                if (!string.IsNullOrEmpty(baseQuery))
                {
                    baseQuery = baseQuery.Substring(1);
                    baseQuery = Regex.Match(baseQuery, "(sign=)*(?<sign>[\\S]+)").Groups[2].Value;
                    baseQuery = RsaHelper.RSADecrypt(privateKey, baseQuery);
                    var requestUrl = $"{request.RequestUri.AbsoluteUri.Split('?')[0]}?{baseQuery}";
                    request.RequestUri = new Uri(requestUrl);
                }

            }

            //獲取Post請求中body中的報文信息,解密後重置請求上下文
            if (request.Method == HttpMethod.Post)
            {
                string baseContent = request.Content.ReadAsStringAsync().Result;
                baseContent = Regex.Match(baseContent, "(sign=)*(?<sign>[\\S]+)").Groups[2].Value;
                baseContent = RsaHelper.RSADecrypt(privateKey, baseContent);
                request.Content = new StringContent(baseContent);
                //此contentType必須最後設置 不然會變成默認值
                request.Content.Headers.ContentType = contentType;
            }

            return request;
        }

        /// <summary>
        /// 處理來自服務器的每一個響應。
        /// </summary>
        /// <param name="response">         要處理的 HTTP 響應消息。</param>
        /// <param name="cancellationToken">可由其餘對象或線程用以接收取消通知的取消標記。</param>
        /// <returns>已處理的 HTTP 響應消息。</returns>
        protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken)
        {
            return response;
        }
    }

獲取平臺私鑰那裏,實際上能夠針對不一樣的接口調用方單獨一個,另起一篇在介紹。

 

而後找到解決方案【App_Start】目錄下的WebApiConfig類,在裏面添加以下代碼,啓用消息處理擴展類:

public static void Register(HttpConfiguration config)
        {
           

            // Web API 路由
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
            config.MessageHandlers.Add(new ArgDecryptMessageProcesssingHandler());


        }

擴展 ActionFilterAttribute

注意!注意!注意!

原博文中是擴展的 AuthorizeAttribute,即認證和受權過濾器,代碼實現上是沒有多大差異的;在時機上認證和受權過濾器要比方法過濾器執行的要早,更適合作認證和受權的操做。而咱們擴展這個過濾器的目的是對報文進行簽名驗證以及超時驗證,因此使用方法過濾器更恰當些。

下面是擴展過濾器的代碼:

/// <summary>
    /// 擴展方法過濾器,進入方法前驗證簽名
    /// </summary>
    public class ApiVerifyFilter : ActionFilterAttribute
    {

        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            base.OnActionExecuting(actionContext);

            //獲取平臺私鑰
            string privateKey = Common.GetRsaPrivateKey();

            //獲取請求的超時時間,爲了測試設置爲100秒,即兩次調用間隔不能超過100秒
            string expireyTime = ConfigurationManager.AppSettings["UrlExpireTime"];
            var request = actionContext.Request;

            //驗證簽名所需header內容
            if (!request.Headers.Contains("signature") || !request.Headers.Contains("timestamp") || !request.Headers.Contains("nonce"))
            {
                SetSpecialResponseMessage(actionContext, 40301);
                return;
            }
            var token = string.Empty;
            var signature = request.Headers.GetValues("signature").FirstOrDefault();
            var timeStamp = request.Headers.GetValues("timestamp").FirstOrDefault();
            var nonce = request.Headers.GetValues("nonce").FirstOrDefault();

            //驗證簽名
            if (!Common.SignValidate(privateKey, nonce, timeStamp, signature, token))
            {
                SetSpecialResponseMessage(actionContext, 40302);
                return;
            }
            //檢查接口調用是否超時
            var ts = Common.DateTime2TimeStamp(DateTime.UtcNow) - Convert.ToDouble(timeStamp);
            if (ts > int.Parse(expireyTime) * 1000)
            {
                SetSpecialResponseMessage(actionContext, 40303);
                return;
            }
        }

        /// <summary>
        /// 設置簽名驗證異常返回狀態
        /// </summary>
        /// <param name="actionContext">當前請求上下文</param>
        /// <param name="statusCode">異常狀態碼</param>
        private static void SetSpecialResponseMessage(HttpActionContext actionContext, int statusCode)
        {
            BizResponseModel model = new BizResponseModel
            {
                Status = statusCode,
                Date = DateTime.Now.ToString("yyyyMMddhhmmssfff"),
                Message = "服務端拒絕訪問"
            };
            switch (statusCode)
            {
                case 40301:
                    model.Message = "沒有設置簽名、時間戳、隨機字符串";
                    break;
                case 40302:
                    model.Message = "簽名無效";
                    break;
                case 40303:
                    model.Message = "無效的請求";
                    break;
                default:
                    break;
            }
            actionContext.Response = new HttpResponseMessage
            {
                Content = new StringContent(JsonConvert.SerializeObject(model))
            };
        }


        public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
        {
            base.OnActionExecuted(actionExecutedContext);
        }
    }

這裏爲了方便寫了個ResponseModel,代碼以下:

/// <summary>
    /// 特殊狀態
    /// </summary>
    public class BizResponseModel
    {
        public int Status { get; set; }
        public string Message { get; set; }
        public string Date { get; set; }
    }

而後下面是用的公共方法:

/// <summary>
        /// 獲取時間戳毫秒數
        /// </summary>
        /// <param name="dateTime"></param>
        /// <returns></returns>
        public static long DateTime2TimeStamp(DateTime dateTime)
        {
            TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
            return Convert.ToInt64(ts.TotalMilliseconds);
        }


        public static bool SignValidate(string privateKey, string nonce, string timestamp, string signature, string token)
        {
            bool isValidate = false;
            var tempSign = RsaHelper.RSADecrypt(privateKey, signature);
            string[] arr = new[] { token, timestamp, nonce }.OrderBy(z => z).ToArray();
            string arrString = string.Join("", arr);
            var sha256Result = arrString.EncryptSha256();
            if (sha256Result == tempSign)
            {
                isValidate = true;
            }
            return isValidate;
        }

簽名驗證的過程以下:

  1. 獲取到報文Header中的 nonce、timestamp、signature、token信息
  2. 將token、timestamp、nonce 三者合併數組中,而後進行順序排序(排序爲了保證後續三個字符串拼接後一致)
  3. 將數組拼接成字符串,而後進行sha256 哈希運算(這裏隨便什麼運算都行,主要爲了防止超長加密麻煩)
  4. 將上一步的哈希結果與[signature] RSA解密結果進行比對,一致則簽名驗證經過,不然則簽名不一致,請求爲僞造

 


而後,如今須要啓用剛添加的方法過濾器,由於是繼承與屬性,能夠全局啓用,或者單個Controller中啓用、或者爲某個Action啓用。全局啓用代碼以下:

下的WebApiConfig類添加以下代碼:

config.Filters.Add(new ApiVerifyFilter());

 

OK,所有完成,最後附上兩個先後的效果對比!

網盤

 

參考博文:

WebApi安全性 使用TOKEN+簽名驗證

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

使用OAuth打造webapi認證服務供本身的客戶端使用

Asp.Net WebAPI中Filter過濾器的使用以及執行順序

微信 公衆號開發文檔

 

寫博文太累了,回家吃螃蟹補補~

相關文章
相關標籤/搜索