Web APi之認證(Authentication)兩種實現方式【二】(十三)

前言

上一節咱們詳細講解了認證及其基本信息,這一節咱們經過兩種不一樣方式來實現認證,而且分析如何合理的利用這兩種方式,文中涉及到的基礎知識,請參看上一篇文中,就再也不敘述廢話。html

序言

對於所謂的認證說到底就是安全問題,在Web API中有多種方式來實現安全,【accepted】方式來處理基於IIS的安全(經過上節提到的WindowsIdentity依賴於HttpContext和IIS認證)或者在Web API裏經過使用Web API中的消息處理機制,可是若是咱們想應用程序運行在IIS以外此時Windows Idenitity這一方式彷佛就不太可能了,同時在Web API中自己就未提供如何處理認證的直接方式,咱們不得不自定義來實現認證功能,同時這也是咱們所推薦的方式,本身動手,豐衣足食。瀏覽器

舒適提示:下面實現方法皆基於基礎認證,若不熟悉Http協議中的Basic基礎認證,請先參看此篇文章【園友海鳥-介紹Basic基礎認證和Digest摘要認證

 

不管何種方式,對於咱們的應用程序咱們都須要在業務層使用基於憑證的用戶認證,由於是客戶端一方的需求,因此客戶端須要明確基礎驗證,基礎認證(Basic)很是簡單而且支持任何Web客戶端,可是基礎驗證的缺點是不安全,經過使用SSL則能夠進行加密就能夠在必定程度上保證了安全,若是是對於通常的應用程序經過基礎認證只是進行編碼而未加密也能夠說是安全的。咱們仍是看看上一節所給圖片

經過上述圖片的粗略信息咱們能夠看出在請求到Action方法之間要通過Web API消息處理管道,在請求到目標元素以前要通過HttpMessageHandler和認證過濾器,因此咱們能夠經過這二者來自定義實現認證。下面咱們一一來看。安全

基於Web API的認證過濾器(AuthorizationFilterAttribute)實現認證

第一步

咱們自定義一個認證身份(用戶名和密碼)的類,那麼此類必須也就要繼承於 GenericIdentity ,既然是基於基礎驗證,那麼類型固然也就是Basic了。併發

    public class BasicAuthenticationIdentity : GenericIdentity
    {
        public string Password { get; set; }
        public BasicAuthenticationIdentity(string name, string password)
            : base(name, "Basic")
        {
            this.Password = password;
        }
    }

第二步

咱們要自定義一個認證過濾器特性,並繼承 AuthorizationFilterAttribute ,此時會變成以下:ide

    public class BasicAuthenticationFilter : AuthorizationFilterAttribute
    {
        public override void OnAuthorization(HttpActionContext actionContext)
        {}
    }

那麼在這個重寫的方法咱們應該寫什麼呢?咱們慢慢來分析!請往下看。函數

  • 解析請求報文頭

首先對於客戶端發送過來的請求咱們確定是須要得到請求報頭,而後解析請求報頭中的Authorization,若此時其參數爲空,咱們將返回到客戶端,併發起質詢。

            string authParameter = null;

            var authValue = actionContext.Request.Headers.Authorization;  //actionContext:Action方法請求上下文 if (authValue != null && authValue.Scheme == "Basic")
                authParameter = authValue.Parameter;  //authparameter:獲取請求中通過Base64編碼的(用戶:密碼) if (string.IsNullOrEmpty(authParameter))

                return null;

次之,若此時認證中的參數不爲空並開始對其進行解碼,並返回一個BasicAuthenticationIdentity對象,若此時對象爲空,則一樣返回到客戶端,併發起質詢

           authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter)); //對編碼的參數進行解碼 var authToken = authParameter.Split(':');  //解碼後的參數格式爲(用戶名:密碼)將其進行分割 if (authToken.Length < 2)
                return null;

            return new BasicAuthenticationIdentity(authToken[0], authToken[1]); //將分割的用戶名和密碼傳遞給此類構造函數進行初始化

最後,咱們將上述二者封裝爲一個ParseHeader方法以便進行調用 

        public virtual BasicAuthenticationIdentity ParseHeader(HttpActionContext actionContext)
        {
            string authParameter = null;

            var authValue = actionContext.Request.Headers.Authorization;
            if (authValue != null && authValue.Scheme == "Basic")
                authParameter = authValue.Parameter;

            if (string.IsNullOrEmpty(authParameter))

                return null;

            authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter));

            var authToken = authParameter.Split(':');
            if (authToken.Length < 2)
                return null;

            return new BasicAuthenticationIdentity(authToken[0], authToken[1]);
        }
  • 接下來咱們將認證未經過而須要發起認證質詢,咱們將其封裝爲一個方法Challenge

        void Challenge(HttpActionContext actionContext)
        {
            var host = actionContext.Request.RequestUri.DnsSafeHost;
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
            actionContext.Response.Headers.Add("WWW-Authenticate", string.Format("Basic realm=\"{0}\"", host));
           
        } 
  • 定義一個方法便於對用戶名和密碼進行校驗,並將其修飾爲虛方法,以避免後續要添加其餘有關用戶數據

        public virtual bool OnAuthorize(string userName, string userPassword, HttpActionContext actionContext)
        {
            if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(userPassword))

                return false;
            else
                return true;

        }
  •  在認證成功後將認證身份設置給當前線程中Principal屬性

           var principal = new GenericPrincipal(identity, null);

            Thread.CurrentPrincipal = principal;

            //下面是針對ASP.NET而設置
            //if (HttpContext.Current != null)
            //    HttpContext.Current.User = principal;

第三步

一切已經就緒,此時在重寫方法中進行相應的調用便可,以下:this

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
    public class BasicAuthenticationFilter : AuthorizationFilterAttribute
    {
        public override void OnAuthorization(HttpActionContext actionContext)
        {
            var userIdentity = ParseHeader(actionContext);
            if (userIdentity == null)
            {
                Challenge(actionContext);
                return;
            }

            if (!OnAuthorize(userIdentity.Name, userIdentity.Password, actionContext))
            {
                Challenge(actionContext);
                return;
            }

            var principal = new GenericPrincipal(userIdentity, null);

            Thread.CurrentPrincipal = principal;

            base.OnAuthorization(actionContext);
        }

第四步 

自定義 CustomBasicAuthenticationFilter 並繼承於 BasicAuthenticationFilter ,重寫其虛方法。編碼

    public class CustomBasicAuthenticationFilter : BasicAuthenticationFilter
    {
        public override bool OnAuthorize(string userName, string userPassword, HttpActionContext actionContext)
        {
            if (userName == "xpy0928" && userPassword == "cnblogs")

                return true;
            else
                return false;

        }
    }

最後一步

註冊自定義認證特性並進行調用加密

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

    [CustomBasicAuthenticationFilter]
    public class ProductController : ApiController
    {....}

至此對於其認證方式就已經徹底實現,接下來咱們經過【搜狗瀏覽器】來驗收咱們的成果。spa

看到以下認證其用戶名和密碼的圖片,咱們知道咱們成功了一半

咱們點擊取消,觀察是否返回401並添加質詢頭即WWW-Authenticate,如咱們所料

咱們輸入正確的用戶名和密碼再試試看,結果認證成功,以下:

基於Web API的消息處理管道(HttpMessageHandler)實現認證

咱們知道HttpMessageHandler是Web API中請求-響應中的消息處理管道的重要角色,可是真正實現管道串聯的是DelegatingHandler,若你不懂Web API消息管道,請參考前面系列文章,因此咱們能夠自定義管道來進行攔截經過繼承DelegatingHandler。下面咱們一步步來實現基於此管道的認證。

第一步

和第一種方法一致再也不敘述。

第二步

這一步固然是自定義管道進行處理並繼承DelegatingHandler,重載在此類中的SendAsync方法,經過得到其請求並處理從而進行響應,若不懂此類中的具體實現,請參看前面系列文章。

  • 一樣是咱們須要根據請求來解析請求報頭,咱們依然須要解析報頭方法,可是須要稍做修改

        public virtual BasicAuthenticationIdentity ParseHeader(HttpRequestMessage requestMessage)
        {
            string authParameter = null;

            var authValue = requestMessage.Headers.Authorization;
            if (authValue != null && authValue.Scheme == "Basic")
                authParameter = authValue.Parameter;

            if (string.IsNullOrEmpty(authParameter))

                return null;

            authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter));

            var authToken = authParameter.Split(':');
            if (authToken.Length < 2)
                return null;

            return new BasicAuthenticationIdentity(authToken[0], authToken[1]);
        }
  • 此時質詢也得做相應的修改,由於此時再也不是依賴於Action請求上下文,而是請求(HttpRequestMessage)和響應(HttpResponseMessage)

        void Challenge(HttpRequestMessage request,HttpResponseMessage response)
        {
            var host = request.RequestUri.DnsSafeHost;

            response.Headers.Add(authenticationHeader, string.Format("Basic realm=\"{0}\"", host));

        }
  • 最終繼承自DelegatingHandler的代碼以下

    public class BasicAuthenticationHandler : DelegatingHandler
    {
        private const string authenticationHeader = "WWW-Authenticate";
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var crendentials = ParseHeader(request);

            if (crendentials != null)
            {
                var identity = new BasicAuthenticationIdentity(crendentials.Name, crendentials.Password);

                var principal = new GenericPrincipal(identity, null);

                Thread.CurrentPrincipal = principal;

                //針對於ASP.NET設置
                //if (HttpContext.Current != null)
                //    HttpContext.Current.User = principal;
            }

            return base.SendAsync(request, cancellationToken).ContinueWith(task => {
                var response = task.Result;
                if (crendentials == null && response.StatusCode == HttpStatusCode.Unauthorized)
                {
                    Challenge(request, response);
                }

                return response;
            });



        }

        void Challenge(HttpRequestMessage request,HttpResponseMessage response)
        {
            var host = request.RequestUri.DnsSafeHost;

            response.Headers.Add(authenticationHeader, string.Format("Basic realm=\"{0}\"", host));

        }

        public virtual BasicAuthenticationIdentity ParseHeader(HttpRequestMessage requestMessage)
        {
            string authParameter = null;

            var authValue = requestMessage.Headers.Authorization;
            if (authValue != null && authValue.Scheme == "Basic")
                authParameter = authValue.Parameter;

            if (string.IsNullOrEmpty(authParameter))

                return null;

            authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter));

            var authToken = authParameter.Split(':');
            if (authToken.Length < 2)
                return null;

            return new BasicAuthenticationIdentity(authToken[0], authToken[1]);
        }
    }

第三步 

上述咱們自定義的BasicAuthenticationFilter此時就得繼承 AuthorizeAttribute 該特性也是繼承於上述的 AuthorizationFilterAttribute ,咱們須要利用AuthorizeAttribute中的 IsAuthorized 方法來驗證當前線程中的Principal是否已經被受權。

    public class BasicAuthenticationFilter : AuthorizeAttribute
    {
        protected override bool IsAuthorized(HttpActionContext actionContext)
        {

            var identity = Thread.CurrentPrincipal.Identity;
            if (identity != null && HttpContext.Current != null)
                identity = HttpContext.Current.User.Identity;

            if (identity != null && identity.IsAuthenticated)
            {

                var basicAuthIdentity = identity as BasicAuthenticationIdentity;
                 
//能夠添加其餘須要的業務邏輯驗證代碼
if (basicAuthIdentity.Name == "xpy0928" && basicAuthIdentity.Password == "cnblogs") { return true; } } return false; } }

經過 IsAuthorized 方法返回值來看,若爲false,則返回401狀態碼,此時會觸發 BasicAuthenticationHandler  中的質詢,而且此方法裏面主要是咱們須要添加認證用戶的業務邏輯代碼。同時咱們也說過咱們第一種方法自定義實現的過濾器特性是 AuthorizationFilterAttribute (若是咱們有更多邏輯使用這個特性是個不錯的選擇),而在這裏是 AuthorizeAttribute (對於驗證用戶而且返回bool值使用此過濾器特性是個不錯的選擇)。

第四步

註冊自定義管道以及認證過濾器特性

            config.MessageHandlers.Add(new BasicAuthenticationHandler());
            config.Filters.Add(new BasicAuthenticationFilter());

最後一步

    [BasicAuthenticationFilter]
    public class ProductController : ApiController
    {.....}

下面咱們經過【360極速瀏覽器】來驗收成果。點擊按鈕直接請求控制器

接下來取消,是否返回401

至此完美結束。

總結 

用認證特性(AuthorizationFilterAttribute)仍是HttpMessageHandler實現認證,這是一個問題? 

經過比較這兩者的實現操做在實現方式上明顯有極大的不一樣,我的以爲用AuthorizationFilterAttribute來實現認證是更加簡單而且緊湊,由於實現的每一處都在每個地方,在大多數實現自定義登錄的場景下,對於用過濾器如此緊湊的業務邏輯用這個更加高效, 用HttpMessageHandler的優勢是全局應用且是Web API消息處理管道的一部分,若是對於不一樣的部分要用不一樣的認證那麼用HttpMessageHandler效果更好,可是此時你須要自定義一個過濾器,尤爲是當MessageHandler對於一個認證須要一個過濾器的時候。因此綜上所述,根據不一樣的應用場景咱們應該選擇對應的方式來實現認證。

 

源代碼連接

 WebAPi之認證.7z

相關文章
相關標籤/搜索