使用ASP.NET Web Api構建基於REST風格的服務實戰系列教程【八】——Web Api的安全性

系列導航地址http://www.cnblogs.com/fzrain/p/3490137.htmlhtml

前言

這一篇文章咱們主要來探討一下Web Api的安全性,到目前爲止全部的請求都是走的Http協議(http://),所以客戶端與服務器之間的通訊是沒有加密的。在本篇中,咱們將在「StudentController」中添加身份驗證功能——經過驗證用戶名與密碼來判斷是不是合法用戶。衆所周知,對於機密信息的傳遞,咱們應該使用安全的Http協議(https://)來傳輸git

在Web Api中強制使用Https

咱們能夠在IIS級別配置整個Web Api來強制使用Https,可是在某些狀況下你可能只須要對某一個action強制使用Https,而其餘的方法仍使用http。github

爲了實現這一點,咱們將使用Web Api中的filters——filter(過濾器)的主要做用就是能夠在咱們執行方法以前執行一段代碼。沒接觸過得能夠經過下圖簡單理解下,大神跳過:web

無標題

咱們新建立的filter將用來檢測是不是安全的,若是不是安全的,filter將終止請求並返回相應:請求必須是https。數據庫

具體作法:建立一個filter繼承自AuthorizationFilterAttribute,重寫OnAuthorization來實現咱們的需求。api

在網站根目錄下建立「Filters」文件夾,新建一個類「ForceHttpsAttribute」繼承自「System.Web.Http.Filters.AuthorizationFilterAttribute」,下面上代碼:瀏覽器

public class ForceHttpsAttribute : AuthorizationFilterAttribute
    {
        public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
        {
            var request = actionContext.Request;
 
            if (request.RequestUri.Scheme != Uri.UriSchemeHttps)
            {
                var html = "<p>Https is required</p>";
 
                if (request.Method.Method == "GET")
                {
                    actionContext.Response = request.CreateResponse(HttpStatusCode.Found);
                    actionContext.Response.Content = new StringContent(html, Encoding.UTF8, "text/html");
 
                    UriBuilder httpsNewUri = new UriBuilder(request.RequestUri);
                    httpsNewUri.Scheme = Uri.UriSchemeHttps;
                    httpsNewUri.Port = 443;
 
                    actionContext.Response.Headers.Location = httpsNewUri.Uri;
                }
                else
                {
                    actionContext.Response = request.CreateResponse(HttpStatusCode.NotFound);
                    actionContext.Response.Content = new StringContent(html, Encoding.UTF8, "text/html");
                }
 
            }
        }
    }

在上面代碼中,咱們經過actionContext參數拿到request和response對象,咱們判斷客戶端的請求:若是不是https,那麼直接響應客戶端應該使用https。緩存

在這裏,咱們須要區分請求是Get仍是其餘(Post,Delete,Put),由於對於使用了Http的Get請求來訪問資源,咱們將使用https建立一個鏈接並添加在響應Header的Location中。這樣作了以後客戶端就會自動使用https來發送Get請求了。安全

對於非Get請求,直接返回404,並通知客戶端必須使用https來請求服務器

若是咱們打算在整個項目中使用,那麼在「WebAPIConfig」類中作以下設置:

public static void Register(HttpConfiguration config)
   {
       config.Filters.Add(new ForceHttpsAttribute());
   }

若是咱們相對具體的Controller或Action設置時,能夠作以下設置:

//對於整個Controller強制使用Https
[Learning.Web.Filters.ForceHttps()]
    public class CoursesController : BaseApiController
    {
    //僅對這個方法強制使用Https
        [Learning.Web.Filters.ForceHttps()]
            public HttpResponseMessage Post([FromBody] CourseModel courseModel)
            {
 
        }
}

使用Basic Authentication驗證用戶

到目前爲止,咱們提供的全部Api都是公開的,任何人都能訪問。但在真是場景中倒是不可取的,對於某些數據,只有經過認證的用戶才能訪問,咱們這裏有兩個地方剛好說明這一點:

1.當客戶端發送Get請求道「http://{your_port}/api/students/{userName}「的時候.例如:經過上述URI訪問userNme爲「TaiseerJoudeh」的信息時,咱們必須讓客戶端提供TaiseerJoudeh相應的用戶名和密碼,對於沒有提供驗證信息的用戶咱們就不讓訪問,由於學生信息包含一些重要的私人信息(email,birthday等)。

2.當客戶端發送Post請求到「http://{your_port}/api/courses/2/students/{userName}「的時候,這意味着給學生選課,咱們能夠想一下,這裏若是不作驗證,那麼全部人都能隨便給某個學生選課,那麼不就亂了麼。

對於上面的場景,咱們使用Basic Authentication來進行身份驗證,主要思路是使用filter從請求header部分獲取身份信息,校驗驗證類型是否爲「basic」,而後校驗內容,正確就放行,不然返回401 (Unauthorized)狀態碼。

在上代碼前,解釋一下下basic authentication:

什麼是basic authentication?

它意味着在正式處理Http請求以前對請求者身份的校驗,這能夠防止服務器受到DoS攻擊(Denial of service attacks)。原理是:客戶端在發送Http請求的時候在Header部分提供一個基於Base64編碼的用戶名和密碼,形式爲「username:password」,消息接收者(服務器)進行驗證,經過後繼續處理請求。

因爲用戶名和密碼僅適用base64編碼,所以爲了保證安全性,basic authentication一般是基於SSL鏈接(https)

爲了在咱們的api中使用,建立一個類「LearningAuthorizeAttribute」繼承自System.Web.Http.Filters.AuthorizationFilterAttribute

public class LearningAuthorizeAttribute : AuthorizationFilterAttribute
    {
 
        [Inject]
        public LearningRepository TheRepository { get; set; }
 
        public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
        {
            //forms authentication Case that user is authenticated using forms authentication
//so no need to check header for basic authentication.
            if (Thread.CurrentPrincipal.Identity.IsAuthenticated)
            {
                return;
            }
 
            var authHeader = actionContext.Request.Headers.Authorization;
 
            if (authHeader != null)
            {
                if (authHeader.Scheme.Equals("basic", StringComparison.OrdinalIgnoreCase) &&
                    !String.IsNullOrWhiteSpace(authHeader.Parameter))
                {
                    var credArray = GetCredentials(authHeader);
                    var userName = credArray[0];
                    var password = credArray[1];
 
                    if (IsResourceOwner(userName, actionContext))
                    {
                        //You can use Websecurity or asp.net memebrship provider to login, for
                        //for he sake of keeping example simple, we used out own login functionality
                        if (TheRepository.LoginStudent(userName, password))
                        {
                            var currentPrincipal = new GenericPrincipal(new GenericIdentity(userName), null);
                            Thread.CurrentPrincipal = currentPrincipal;
                            return;
                        }
                    }
                }
            }
 
            HandleUnauthorizedRequest(actionContext);
        }
 
        private string[] GetCredentials(System.Net.Http.Headers.AuthenticationHeaderValue authHeader)
        {
 
            //Base 64 encoded string
            var rawCred = authHeader.Parameter;
            var encoding = Encoding.GetEncoding("iso-8859-1");
            var cred = encoding.GetString(Convert.FromBase64String(rawCred));
 
            var credArray = cred.Split(':');
 
            return credArray;
        }
 
        private bool IsResourceOwner(string userName, System.Web.Http.Controllers.HttpActionContext actionContext)
        {
            var routeData = actionContext.Request.GetRouteData();
            var resourceUserName = routeData.Values["userName"] as string;
 
            if (resourceUserName == userName)
            {
                return true;
            }
            return false;
        }
 
        private void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext)
        {
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
 
            actionContext.Response.Headers.Add("WWW-Authenticate",
                                               "Basic Scheme='eLearning' location='http://localhost:8323/account/login'");
 
        }
    }

咱們重寫了「OnAuthorization」,實現以下功能:

1.從請求Header中獲取校驗數據

2.判斷驗證信息類型爲「basic」幷包含base64編碼

3.將base64編碼轉化爲string,並提取用戶名和密碼

4.校驗提供的驗證信息是否與訪問的資源信息相同(學生的詳細信息只能由他本身訪問)

5.去數據庫校驗用戶名及密碼

6.若是校驗經過,則設置Thread的CurrentPrincipal,使本次接下來的請求都是經過校驗的。

7.校驗沒經過,返回401(Unauthorized)並添加一個WWW-Authenticate響應頭,根據這個請求,客戶端能夠添加相應的驗證信息

在代碼中實現起來就很簡單了,上兩個Attribute就完了:

public class StudentsController : BaseApiController
    {
        [LearningAuthorizeAttribute]
        public HttpResponseMessage Get(string userName)
            {
 
            }
    }
public class EnrollmentsController : BaseApiController
    {
        [LearningAuthorizeAttribute]
        public HttpResponseMessage Post(int courseId, [FromUri]string userName, [FromBody]Enrollment enrollment)
            {
 
            }
    }

測試成果

使用測試工具發送以下請求:

image

因爲沒有提供身份驗證,因而獲得以下響應:

image

取消:

image

去數據庫找到對應的用戶名和密碼輸入,獲得以下結果:

image

總結

由於 Base Authentication 的安全性較差,但對於無 Cookie 的 Web Api 來講,應用上很是的簡單和方便。

Base Authentication 最大的缺點是憑據會被瀏覽器緩存——直到你關閉瀏覽器爲止。若是你已經對某個URI得到了受權,瀏覽器就會在受權頭髮送相應的憑據,這使其更容易受到跨站點請求僞造(CSRF)攻擊

Base Authentication 一般須要使用HTTPS方式進行加密處理。

源碼地址:https://github.com/fzrain/WebApi.eLearning

相關文章
相關標籤/搜索