[WebAPI]Basic Authentication(RFC 2617+SSL(https))驗證WebAPI 2

  • 5161
  • 0
  • 2014-12-02

摘要:[WebAPI]以Basic Authentication驗證(Windows驗證或資料庫驗證)WebAPI 2

由於基本驗證是根據RFC 2617實作,而且其數位憑證必須透過加密的方式傳送,不然就會被人看光光,所以先來設定IIS如何使用https

首先先做一次Windows驗證方式的https

建立憑證:

首先打開iis, 滑鼠先點一下自己的pc名稱之後,選擇右方的(伺服器憑證選項)

然後選擇(建立自我簽署憑證),然後打入自己希望的名稱之後,按下確認就建立憑證成功了

iis就是利用剛剛建立的憑證,去檢查使用者打入的帳號密碼是否為本機電腦帳號之一

 

然後我們隨便選擇一個站台來使用剛剛產生的憑證,選擇某某站台之後,選擇右方的(繫結)選項

然後按下(新增),類型就選擇https,ip就選本機,port就先用預設443就好,SSL憑證就選擇剛剛我們手動產生的憑證

 

然後繫結就設定完畢了,於是乎這個網站就有兩種繫結http + https

https的效能只有https的十分之一,所以如果不是很需要保密的需求

盡量使用http就好,這個可以在WebAPI的Controller裡面設定不同的

Action是否使用https,屆時測試的時候只要分別打入

https://127.0.0.1:443/WebAPI的使用路徑/

http://127.0.0.1:444/WebAPI的使用路徑/

便可使用https或是http來測試WebAPI

 

不過在測試階段的話,當然也可以用iis express去測試https就好

用iis express的話,不用手動建立憑證(因為系統已經自動建了)

也不用像剛剛一樣手動加入https的繫結,只要滑鼠左鍵點選專案之後

按下F4的屬性裡面,把SSL啟用以及Windows驗證啟用即可。

屆時測試的時候只要分別打入
 
https://localhost:44300/WebAPI的使用路徑/
 
http://localhost:2394/WebAPI的使用路徑/
 
便可使用https或是http來測試WebAPI
 
 
然後就跟MVC一慣的精神一樣,利用attribute來設定不同的action是否使用https
在專案另外新增一個Attributes資料夾,加入類別RequireHttps.cs

public class RequireHttpsAttribute : AuthorizationFilterAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps)
        {
            actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
            {
                ReasonPhrase = "HTTPS Required"
            };
        }
        else
        {
            base.OnAuthorization(actionContext);
        }
    }
}

然後要驗證的就加上[Authorize]與[RequireHttps]屬性,

依照需求自行設定,自己要確記https的效能只有http的十分之一

有必要才用,所以我也不是每一個Action都設定要驗證。

ps.有需要打帳號密碼的時候,帳密傳輸時才需要用https

沒人這麼無聊只加其中一樣屬性吧。

 
用Chrome測試https:
在網址列打入https://127.0.0.1:443/api/Employee/
然後忽略chrome的警告訊息,按下(繼續)的按鈕之後,就會出現要打入帳號密碼視窗
打入隨便一個本機電腦的登入帳號密碼(網域名稱記得不要打錯),就出現回傳的JSon資料了
不過~說實在我不知道微軟為什麼要在官方網站介紹windows驗證方式
如果有10個使用者需要存取這個webapi,就要為他們在windows建立10個帳號
如果有100個的話呢.....
 
 
接下來把驗證方式從Windows驗證改成把SQLServer驗證,這也是實務上最常被
接受的方式,之前先用Windows驗證介紹是為了要把重點放在IIS與WEBAPI如何做好相關設定:
然後我們在專案新增一個資料夾Modules,並且新增一個類別BasicAuthHttpModule.cs
內容就照微軟給的複製貼上後,修改CheckPassword函數的內容為自己檢查帳號密碼的方式
即可,下面是微軟給的,先複製貼上到我們專案,然後再來進行修改。

namespace WebHostBasicAuth.Modules
{
    public class BasicAuthHttpModule : IHttpModule
    {
        private const string Realm = "My Realm";

        public void Init(HttpApplication context)
        {
            // Register event handlers
            context.AuthenticateRequest += OnApplicationAuthenticateRequest;
            context.EndRequest += OnApplicationEndRequest;
        }

        private static void SetPrincipal(IPrincipal principal)
        {
            Thread.CurrentPrincipal = principal;
            if (HttpContext.Current != null)
            {
                HttpContext.Current.User = principal;
            }
        }

        // TODO: Here is where you would validate the username and password.
        private static bool CheckPassword(string username, string password)
        {
            return username == "user" && password == "password";
        }

        private static bool AuthenticateUser(string credentials)
        {
            bool validated = false;
            try
            {
                var encoding = Encoding.GetEncoding("iso-8859-1");
                credentials = encoding.GetString(Convert.FromBase64String(credentials));

                int separator = credentials.IndexOf(':');
                string name = credentials.Substring(0, separator);
                string password = credentials.Substring(separator + 1);

                validated = CheckPassword(name, password);
                if (validated)
                {
                    var identity = new GenericIdentity(name);
                    SetPrincipal(new GenericPrincipal(identity, null));
                }
            }
            catch (FormatException)
            {
                // Credentials were not formatted correctly.
                validated = false;

            }
            return validated;
        }

        private static void OnApplicationAuthenticateRequest(object sender, EventArgs e)
        {
            var request = HttpContext.Current.Request;
            var authHeader = request.Headers["Authorization"];
            if (authHeader != null)
            {
                var authHeaderVal = AuthenticationHeaderValue.Parse(authHeader);

                // RFC 2617 sec 1.2, "scheme" name is case-insensitive
                if (authHeaderVal.Scheme.Equals("basic",
                        StringComparison.OrdinalIgnoreCase) &&
                    authHeaderVal.Parameter != null)
                {
                    AuthenticateUser(authHeaderVal.Parameter);
                }
            }
        }

        // If the request was unauthorized, add the WWW-Authenticate header 
        // to the response.
        private static void OnApplicationEndRequest(object sender, EventArgs e)
        {
            var response = HttpContext.Current.Response;
            if (response.StatusCode == 401)
            {
                response.Headers.Add("WWW-Authenticate",
                    string.Format("Basic realm=\"{0}\"", Realm));
            }
        }

        public void Dispose() 
        {
        }
    }
}

然後呼叫我們改寫微軟提供的CheckPassword函數,去資料庫檢驗此組帳號密碼是否存在


private static bool CheckPassword(string username, string password)
        {
            WebAPIEntities db = new WebAPIEntities();
            //存在此筆帳號
            bool chkAccount = db.usp_chkLogin(username, password).First() == 1 ? true : false;
            return chkAccount;
            
        }

 

並且在Web.config裡面加上httpModule的設定來驗證

以下是順便紀錄如何在EF的情況下,呼叫預存程序來檢查使用者傳入的帳號密碼

要把帳號密碼存在資料庫,當然要先開一個資料表table存帳號密碼,姑且把資料表名稱取為Accounts

 

然後隨便新增一筆帳號


 insert into Accounts(uid,pwd)
 values('ironman',pwdencrypt('iampassword'))

 

新增一個預存程序檢查user輸入的帳號密碼是否存在於資料庫

create procedure usp_chkLogin
 @uid varchar(50),
 @pwd varchar(150)
as
 declare @exist int
 set @exist = 0
 set @exist = (
 select PWDCOMPARE(@pwd, Accounts.pwd)
 from Accounts
 where Accounts.uid = @uid)  
 select isnull(@exist,0)

最後再記得更新Model資料夾裡面的EF物件(這裡的情況是Ado.net Entity Data Model),

就可以像上面CheckPassword函數一樣直接呼叫預存程序db.usp_chkLogin(ooxx)來用了

 

最後記得把IIS Express或IIS的 windows驗證還有匿名驗證關掉

 

寫到最後,發現一個很嚴重的問題,就是使用httpModule驗證的話

會導致http 以及 https都需要輸入帳號密碼

如此一來就會綁死只能用https

還是得花時間研究一下http message handler來實作這個驗證吧....聽說http message handler比較彈性

可以達成雙通道http + https

先這樣

 

以下改用http message handler做驗證,

並主要參考

Basic HTTP authentication in ASP.NET Web API using message handlers

好處就是:可以藉由[Authorize]屬性動態設定哪個Action,

哪個Controller需不需要驗證(驗證務必搭配[RequireHttps]屬性)了

先加入一個Message Handler類別,比較要注意的就是.CreatePrincipal(ooxx)的部分寫了兩次的原因,

是因為第一個是WinForm去取得驗證,第二個是WebForm去取得驗證,我們無法確定Client是哪一種

所以都要加上

 

using System;

using System.Collections.Generic;

using System.Linq;

using System.Net;

using System.Net.Http;

using System.Net.Http.Headers;

using System.Text;

using System.Threading;

using System.Web;

 

namespace WebAPIDataCenter

{

    public class BasicAuthMessageHandler : DelegatingHandler

    {

        private const string BasicAuthResponseHeader = "WWW-Authenticate";

        private const string BasicAuthResponseHeaderValue = "Basic";

 

        public IProvidePrincipal PrincipalProvider { get; set; }

 

        protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(

            HttpRequestMessage request,

            CancellationToken cancellationToken)

        {

            AuthenticationHeaderValue authValue = request.Headers.Authorization;

            if (authValue != null && !String.IsNullOrWhiteSpace(authValue.Parameter))

            {

                Credentials parsedCredentials = ParseAuthorizationHeader(authValue.Parameter);

                if (parsedCredentials != null)

                {

                    //這個是winform用的

                    Thread.CurrentPrincipal = PrincipalProvider

                        .CreatePrincipal(parsedCredentials.Username, parsedCredentials.Password);

                    //這是WebForm用的

                    request.GetRequestContext().Principal =

                        PrincipalProvider.CreatePrincipal(parsedCredentials.Username, parsedCredentials.Password);

                }

            }

            return base.SendAsync(request, cancellationToken)

               .ContinueWith(task =>

               {

                   var response = task.Result;

                   if (response.StatusCode == HttpStatusCode.Unauthorized

                       && !response.Headers.Contains(BasicAuthResponseHeader))

                   {

                       response.Headers.Add(BasicAuthResponseHeader

                           , BasicAuthResponseHeaderValue);

                   }

                   return response;

               });

        }

 

        private Credentials ParseAuthorizationHeader(string authHeader)

        {

            string[] credentials = Encoding.ASCII.GetString(Convert

                                                            .FromBase64String(authHeader))

                                                            .Split(

                                                            new[] { ':' });

            if (credentials.Length != 2 || string.IsNullOrEmpty(credentials[0])

                || string.IsNullOrEmpty(credentials[1])) return null;

            return new Credentials()

            {

                Username = credentials[0],

                Password = credentials[1],

            };

        }

    }

}

然後加入一個簡單的類別,以供上面的Message Handler傳入帳號密碼


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace WebAPIDataCenter
{
    public class Credentials
    {
        public string Username { get; set; }
        public string Password { get; set; }
    }
}

 

然後加入一個interface,讓developer自行實作客戶需求的驗證方式


public interface IProvidePrincipal
{
    IPrincipal CreatePrincipal(string username, string password);
}

 

以下就是簡單實作interface的類別內容,到資料庫檢查帳號密碼是否存在

並順便註記Emprepository.cs的CheckAccountExist的內容(透過EF呼叫預存程序)


using System.Security.Principal;
using WebAPIDataCenter.Services;

namespace WebAPIDataCenter
{
    public class DummyPrincipalProvider : IProvidePrincipal
    {        
        private EmployeeRepository empRepository = new EmployeeRepository();

        public IPrincipal CreatePrincipal(string uid, string pwd)
        {
            if (empRepository.CheckAccountExist(uid,pwd)== false)
            {
                return null;
            }
            
            //GenericIdentity??
            var identity = new GenericIdentity(uid);
            //GenericPrincipal??
            IPrincipal principal = new GenericPrincipal(identity, new[] { "User" });
            return principal;
        }
    }
}

public Boolean CheckAccountExist( string uid , string pwd)
        {
            WebAPIEntities db = new WebAPIEntities();
            Boolean accountExisted = db.usp_chkLogin(uid, pwd).First() == 1 ? true : false;            
            return accountExisted;
        }

並且於Gloabal.asax加入這個message handler, 之後只要在Controller或任何Action加入[Authorize]屬性,即可輸入帳密進行驗證, 如下


GlobalConfiguration.Configuration
                .MessageHandlers.Add(new BasicAuthMessageHandler()
                {
                    PrincipalProvider = new DummyPrincipalProvider()
                });

 

 

 

 

參考資料:

Basic Authentication

Working with SSL in Web API

How to Set Up SSL on IIS 7

HASHBYTES (Transact-SQL)

Entity Framwork 使用 SQL的PWDCOMPARE函數

ASP.NET MVC4 - Web API 開發系列 [從無到有,建立 CRUD 的應用程式]

Create request with POST, which response codes 200 or 201 and content

Using the ASP.NET Web API UrlHelper

Basic HTTP authentication in ASP.NET Web API using message handlers