[.NET] 在WebAPI中加入自訂的http基本驗證方式

當我們撰寫WebAPI的時候,最擔心的就是這個WebAPI會被其他不明人士、或是未經授權的人任意的使用
透過http的基本驗證的方式,可以作到允許的使用者及帳號才能存取我們自己寫的WebAPI

一般來說,透過http的基本驗證方式,就是在http請求的Header上,加入一個名為Authorization的Header,其內容為"Basic " + Base64Encode(username:password)
在Client端把伺服器給予允許存取的username與password,經過Base64的編碼後,放入至名為Authorization的Header中,隨著http的請求一起送至Server端
而Server端在取得這樣的驗證資訊後,驗證是否為允許存取的資訊,並回傳訊息

以這樣的驗證方式,若是直接在WebAPI的程序中直接加入[Authorize]的Attribute,就會以預設的驗證方式進行驗證
所以在本篇文章中,我們會透過覆寫驗證功能的作法,將驗證功能另外獨立作出一個AOP的Attribute,讓每一個WebAPI的程序直接加入Attribute就可以進行驗證,而不需額外加寫程式碼

首先,先在WebAPI的專案中,加入一個BasicAuthenticationIdentity的類別庫,並將下面的程式碼加入至該類別庫中

using System.Security.Principal;
public class BasicAuthenticationIdentity : GenericIdentity
{
    public BasicAuthenticationIdentity(string name, string password)
        : base(name, "Basic")
    {
        this.Password = password;
    }

    public string Password { get; set; }
}

這個類別庫主要的目的是將要驗證的帳號與密碼,建立一個新的物件,並繼承GenericIdentity這個類別,代表要傳入的資訊為一個泛型的使用者物件

接下來,在專案中加入BasicAuthenticationFilter這個類別庫,並將下面程式碼加入至該類別庫中

using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using System.Net.Http;
using System.Threading;
using System.Security.Principal;
using System.Net;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class BasicAuthenticationFilter : AuthorizationFilterAttribute
{
    bool Active = true;

    public BasicAuthenticationFilter()
    { }

    public BasicAuthenticationFilter(bool active)
    {
        Active = active;
    }

    public override void OnAuthorization(HttpActionContext actionContext)
    {
        if (Active)
        {
            var identity = ParseAuthorizationHeader(actionContext);
            if (identity == null)
            {
                Challenge(actionContext);
                return;
            }


            if (!OnAuthorizeUser(identity.Name, identity.Password, actionContext))
            {
                Challenge(actionContext);
                return;
            }

            var principal = new GenericPrincipal(identity, null);

            Thread.CurrentPrincipal = principal;
            base.OnAuthorization(actionContext);
        }
    }

    protected virtual bool OnAuthorizeUser(string username, string password, HttpActionContext actionContext)
    {
        if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
            return false;

        return true;
    }

    protected virtual BasicAuthenticationIdentity ParseAuthorizationHeader(HttpActionContext actionContext)
    {
        string authHeader = null;
        var auth = actionContext.Request.Headers.Authorization;
        if (auth != null && auth.Scheme == "Basic")
            authHeader = auth.Parameter;

        if (string.IsNullOrEmpty(authHeader))
            return null;

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

        var tokens = authHeader.Split(':');
        if (tokens.Length < 2)
            return null;

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


    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));
    }
}

這一段類別庫程式碼最主要的內容,在於覆寫OnAuthorization這一個事件,置換成我們自己要進行帳號與密碼驗證動作的程式碼

接著在專案中,加入第三個類別庫ApiBasicAuthenticationFilter,並加入下面的程式碼內容

using System.Web.Http.Controllers;

public class ApiBasicAuthenticationFilter : BasicAuthenticationFilter
{

    public ApiBasicAuthenticationFilter()
    { }

    public ApiBasicAuthenticationFilter(bool active) : base(active)
    { }


    protected override bool OnAuthorizeUser(string username, string password, HttpActionContext actionContext)
    {
        // 在這裡加上帳號密碼的驗證,可以從資料庫取出資料進行比對
        string strUserName = username;
        string strPassword = password;
        bool blIsAuthorize = false;

        /* 作一個假的驗證,測試用
        if (strUserName == "maduka" && strPassword == "ABCDE")
            blIsAuthorize = true;
        */

        return blIsAuthorize;
    }
}

這一個類別庫繼承了剛剛建立的BasicAuthenticationFilter,並覆寫了OnAuthorizeUser這一個事件,在OnAuthorizeUser這一個事件中,加入我們自己必須進行的帳號密碼驗證動作,而呼叫WebAPI的Client端程式傳入的帳號與密碼,就是在這裡進行資料的驗證,當然驗證的動作也可以與資料庫進行比較,也就是說這裡完全就是由我們自己決定該怎麼驗證帳號資訊了

最後,我們在WebAPI的控制器中,加入ApiBasicAuthenticationFilter這個Attribute,這樣在Server端的WebAPI,就完成了驗證的準備動作

// GET api/values
[SwaggerOperation("GetAll")]
[ApiBasicAuthenticationFilter]
public IEnumerable<string> Get()
{
    return new string[] { "value1", "value2" };
}

然後實際執行一次呼叫WebAPI的動作,此時會發現,由於透過一般WebAPI的呼叫方式,也就是不帶入任何基本驗證資訊的http請求,WebAPI是會要求http的基本驗證的

若是透過Postman或是其他REST API的測試工具,將Authorization的基本驗證資訊放入後作出的請求,就可以順利的進行WebAPI的呼叫

WebAPI的使用授權及驗證一直以來都是WebAPI開發上一個很重要的課題,透過這樣基本驗證功能的覆寫,並使用簡易的AOP屬性,就可以很方便的快速套用在每一個WebAPI上

範例程式下載:
https://github.com/madukapai/maduka-WebAPI

參考資料:
A WebAPI Basic Authentication Authorization Filter