使用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

相关文章
相关标签/搜索