很多情况下目标Action方法都要求在一个安全上下文中被执行,这里所谓的安全上下文主要指的是当前请求者是一个经过授权的用户。授权的本质就是让用户在他许可的权限范围内做他能够做的事情,授权的前提是请求者是一个经过认证的用户。质询-应答(Chanllenge-Response)”是用户认证采用的一种常用的形式,认证方向被认证方发出质询以要求其提供用于实施认证的用户凭证,而被认证方提供相应的凭证以作为对质询的应答。旨在目标Action方法执行之前实施身分认证的AuthenticationFilter也对这种认证方法提供了支持。

一、IAuthenticationFilter接口

所有的AuthenticationFilter类型均实现了IAuthenticationFilter接口,该接口定义在命名空间“System.Web.Mvc.Filters”下(其他四种过滤器接口都定义在“System.Web.Mvc”命名空间下)。如下面的代码片断所示,OnAuthentication和OnAuthenticationChallenge这两个方法被定义在此接口中,前者用于对请求实施认证,后者则负责将相应的认证质询发送给请求者。

   1: public interface IAuthenticationFilter

   2: {

   3:     void OnAuthentication(AuthenticationContext filterContext);

   4:     void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext);

   5: }

定义在IAuthenticationFilter接口的两个方法都将一个上下文对象作为其唯一参数。OnAuthentication方法的这个参数类型为AuthenticationContext,如下面的代码片断所示,它是ControllerContext的子类。AuthenticationContext的ActionDescriptor返回的自然是用于描述目标Action方法的ActionDescriptor对象。借助于Principal属性,我们可以获取或设置代表当前用户的Principal对象。如果我们在执行OnAuthentication方法的过程中设置了AuthenticationContext的Result属性,提供的ActionResult将直接用于响应当前请求。

   1: public class ActionExecutingContext : ControllerContext

   2: {    

   3:     public ActionExecutingContext();

   4:     public ActionExecutingContext(ControllerContext controllerContext, ActionDescriptor actionDescriptor,IDictionary<string, object> actionParameters);

   5:  

   6:     public virtual ActionDescriptor             ActionDescriptor { get; set; }

   7:     public virtual IDictionary<string, object>  ActionParameters { get; set; }

   8:     public ActionResult                         Result { get; set; }

   9: }

OnAuthenticationChallenge方法的参数类型为AuthenticationChallengeContext。如下面的代码片断所示,它依然是ControllerContext的子类。它同样具有一个用于描述目标Action方法的ActionDescriptor属性,其Result属性代表的ActionResult对象将用于响应当前请求。

   1: public class ActionExecutedContext : ControllerContext

   2: {    

   3:     public ActionExecutedContext();   

   4:     public ActionExecutedContext(ControllerContext controllerContext, ActionDescriptor actionDescriptor, bool canceled, Exception exception);

   5:  

   6:     public virtual ActionDescriptor     ActionDescriptor { get; set; }

   7:     public virtual bool                 Canceled { get; set; }

   8:     public virtual Exception            Exception { get; set; }

   9:     public bool                         ExceptionHandled { get; set; }

  10:     public ActionResult                 Result { get; set; }

  11: }

二、AuthenticationFilter的执行流程

我们知道身份认证总是对请求处理的第一个步骤,因为只有确定了请求者的真实身份,安全才能得到保障,所以AuthenticationFilter是最先被执行的一类过滤器。所有过滤器的执行都是ActionInvoker来驱动的,ASP.NET MVC在默认情况下采用的ActionInvoker是一个AsyncControllerActionInvoker对象,后者类型派生于ControllerActionInvoker。ControllerActionInvoker针对AuthenticationFilter的执行体现在如下两个方法(InvokeAuthenticationFilters和InvokeAuthenticationFiltersChallenge)上。

   1: public class ControllerActionInvoker : IActionInvoker

   2: {

   3:     //其他成员

   4:     protected virtual AuthenticationContext InvokeAuthenticationFilters(ControllerContext controllerContext,IList<IAuthenticationFilter> filters, ActionDescriptor actionDescriptor);

   5:     protected virtual AuthenticationChallengeContext InvokeAuthenticationFiltersChallenge(ControllerContext controllerContext, IList<IAuthenticationFilter> filters, ActionDescriptor actionDescriptor, ActionResult result);   

   6: }

如果多个AuthenticationFilter同时被应用到目标Action方法上,ControllerActionInvoker会根据对应Filter的Order/Scope属性对它们进行排序。随后ControllerActionInvoker会根据当前ControllerContext、描述目标Action方法的ActionDescriptor对象以及原始的Principal(对应于当前HttpContext的User属性)创建一个AuthenticationContext对象,并以此作为参数以此调用每个AuthenticationFilter对象的OnAuthentication对象实施认证。

在目标Action方法被执行之后,通过本书第11章“View的呈现”我们知道最终执行的结果会被封装为一个ActionResult对象。ControllerActionInvoker会利用当前ControllerContext、描述目标Action方法的ActionDescriptor对象和这个ActionResult创建一个AuthenticationChallengeContext对象,并将其作为参数依次调用每个AuthenticationFilter的OnAuthenticationChallenge方法。这个AuthenticationChallengeContext对象的Result属性最终返回的ActionResult对象将被用来对请求予以响应。

右图基本反映了整个“AuthenticationFilter链”的执行流程,但是如果在执行某个AuthenticationFilter对象的OnAuthenticatio方法时对作为参数的AuthenticationContext对象的Result属性作了相应的设置,针对整个“AuthenticationFilter链”的执行将会立即中止,指定的这个ActionResult对象将用于响应当前请求。如果在执行过程中对AuthenticationContext对象的Principal属性作了相应的设置,该属性值将会作为当前HttpContext和当前线程的Principal。

三、实例演示:通过自定义AuthenticationFilter实现Basic认证

在ASP.NET MVC的应用编程接口中,我们找不到IAuthenticationFilter接口的实现者。为了让大家对这个在ASP.NET MVC 5才引入的过滤器具有更加深刻的认识,我们接下来会通过一个实例来演示如何通过自定义的AuthenticationFilter实现针对Basic方案的认证。不过在这之前,我们有必要对Basic这种基本的认证方法作一个基本的了解。Basic和Digest是两种典型的HTTP认证方案。对于前者,虽然客户端提供的认证凭证(用户名+密码)仅仅是被Base64编码而没有被加密,但是我们可以通过采用HTTPS传输利用SSL来解决机密性的问题,所以Basic认证也不失为一种不错的认证方案。左图体现了Basic认证的基本流程,可以看出这也是一种典型的采用“质询-应答”模式的认证方案,整个流程包含如下两个基本步骤。

  • 客户端向服务端发送一个HTTP请求,服务端返回一个状态为“401, Unauthorized”的响应。该响应具有一个“WWW-Authenticate”的报头标明采用的是Basic认证方案。Basic认证是在一个“领域(Realm)”限定的上下文中进行的,该报头还可以执行认证的领域,左图所示的WWW-Authenticate报头值为:Basic realm="localhost"。
  • · 客户端向服务端发送一个携带基于用户名/密码的认证凭证的请求。认证凭证的格式为“{UserName}:{Password}”,并采用Base64编码(编码的目的不是为了保护提供的密码)。这样一个经过编码的认证凭证被存放在请求报头Authorization中,相应的认证方案类型(Basic)依然需要在该报头中指定,左图所示的Authorization报头值为:Basic YcdfaYsss==。服务端接收到请求之后,从Authorization报头中提取凭证并对其进行解码,最后采用提取的用户名和密码实施认证。认证成功之后,该请求会得到正常的处理,并回复一个正常的响应。

在正式介绍如果定义这个实现Basic认证的AuthenticationFilter之前,我们不妨先来看看使用了这个自定义AuthenticationFilter会产生怎样的效果。我们在一个ASP.NET MVC应用中定义了如下一个HomeController,定义其中的默认Action方法Index会输出以三种形式体现的“当前用户名”。HomeController类型上应用的AuthenticateAttribute特性正是我们自定义的AuthenticationFilter。

   1: [Authenticate]

   2: public class HomeController : Controller

   3: {

   4:     public void Index()

   5:     {

   6:         Response.Write(string.Format("Controller.User: {0}<br/>", this.User.Identity.Name));

   7:         Response.Write(string.Format("HttpContext.User: {0}<br/>", this.ControllerContext.HttpContext.User.Identity.Name));

   8:         Response.Write(string.Format("Thread.CurrentPrincipal: {0}", Thread.CurrentPrincipal.Identity.Name));

   9:     }

  10: }

由于浏览器默认提供对Basic认证的支持,所以当我们运行该程序后如下图所示的登录对话框会自动弹出,当我们输入正确的用户名和密码(用户名和密码直接维护在AuthenticateAttribute上)后,当前登录用户名会呈现在浏览器上。

这个用于实现Basic认证的AuthenticateAttribute定义如下,简单起见我们将帐号采用的用户名和密码保存在一个静态字段中。具体的认证实现在实现的OnAuthentication方法中,我们在该方法中调用IsAuthenticated判断请是否经过认证,并在认证成功的情况下得到代表请求用户的Principal对象,然对作为参数的AuthenticationContext对象的Principal属性进行赋值。对于没有经过认证的请求,我们会调用另一个方法ProcessUnauthenticatedRequest对其进行处理。

   1: public class AuthenticateAttribute:FilterAttribute,IAuthenticationFilter

   2: {

   3:     public const string AuthorizationHeaderName           ="Authorization";

   4:     public const string WwwAuthenticationHeaderName       ="WWW-Authenticate";

   5:     public const string BasicAuthenticationScheme         ="Basic";

   6:     private static Dictionary<string, string> userAccounters;

   7:  

   8:     static AuthenticateAttribute()

   9:     {

  10:         userAccounters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

  11:  

  12:         userAccounters.Add("Foo", "Password");

  13:         userAccounters.Add("Bar", "Password");

  14:         userAccounters.Add("Baz", "Password");

  15:     }

  16:  

  17:     public void OnAuthentication(AuthenticationContext filterContext)

  18:     {

  19:         IPrincipal user;

  20:         if (this.IsAuthenticated(filterContext, out user))

  21:         {

  22:             filterContext.Principal = user;

  23:         }

  24:         else

  25:         {

  26:             this.ProcessUnauthenticatedRequest(filterContext);

  27:         }

  28:     }

  29:  

  30:     protected virtual AuthenticationHeaderValue GetAuthenticationHeaderValue(AuthenticationContext filterContext)

  31:     {

  32:         string rawValue = filterContext.RequestContext.HttpContext.Request.Headers[AuthorizationHeaderName];

  33:         if (string.IsNullOrEmpty(rawValue))

  34:         {

  35:             return null;

  36:         }

  37:         string[] split = rawValue.Split(' ');

  38:         if (split.Length != 2)

  39:         {

  40:             return null;

  41:         }

  42:         return new AuthenticationHeaderValue(split[0], split[1]);

  43:     }

  44:  

  45:     protected virtual bool IsAuthenticated(AuthenticationContext filterContext, out IPrincipal user)

  46:     {

  47:         user = filterContext.Principal;

  48:         if (null != user & user.Identity.IsAuthenticated)

  49:         {

  50:             return true;

  51:         }

  52:  

  53:         AuthenticationHeaderValue token = this.GetAuthenticationHeaderValue(filterContext);

  54:         if (null != token && token.Scheme ==  BasicAuthenticationScheme)

  55:         {

  56:             string credential = Encoding.Default.GetString(Convert.FromBase64String(token.Parameter));

  57:             string[] split = credential.Split(':');

  58:             if (split.Length == 2)

  59:             {

  60:                 string userName = split[0];

  61:                 string password;

  62:                 if (userAccounters.TryGetValue(userName, out password))

  63:                 {

  64:                     if (password == split[1])

  65:                     {

  66:                         GenericIdentity identity = new GenericIdentity(userName);

  67:                         user = new GenericPrincipal(identity, new string[0]);

  68:                         return true;

  69:                     }

  70:                 }

  71:             }

  72:         }

  73:         return false;

  74:     }

  75:  

  76:     protected virtual void ProcessUnauthenticatedRequest(AuthenticationContext filterContext)

  77:     {

  78:         string parameter = string.Format("realm=\"{0}\"", filterContext.RequestContext.HttpContext.Request.Url.DnsSafeHost);

  79:         AuthenticationHeaderValue challenge = new AuthenticationHeaderValue(BasicAuthenticationScheme, parameter);

  80:         filterContext.HttpContext.Response.Headers[WwwAuthenticationHeaderName] = challenge.ToString();

  81:         filterContext.Result = new HttpUnauthorizedResult();

  82:     }

  83:  

  84:     public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext) {}

  85: }

在对请求实施认证的IsAuthenticated方法中,我们会试图从请求的Authorization报头中提取安全凭证,并按照Basic凭证的格式解析出用户名和密码。只有在用户名和密码匹配的情况下,我们认为请求通过认证,并根据解析出来的用户名创建一个GenericPrincipal对象作为输出参数user的值。如果请求并为通过认证(它可以是一个匿名请求,或者提供的用户名与密码不匹配),方法ProcessUnauthenticatedRequest会被调用。在此情况下,它会对响应的WWW-Authenticate报头进行相应的设置,并创建一个HttpUnauthorizedResult对象作为AuthenticationContext对象的Result属性,那么客户端最终会接收到一个状态为“401, Unauthorized”的响应。

[ASP.NET MVC] 利用自定义的AuthenticationFilter实现Basic认证的更多相关文章

  1. 利用自定义的AuthenticationFilter实现Basic认证

    [ASP.NET MVC] 利用自定义的AuthenticationFilter实现Basic认证   很多情况下目标Action方法都要求在一个安全上下文中被执行,这里所谓的安全上下文主要指的是当前 ...

  2. 转:【译】Asp.net MVC 利用自定义RouteHandler来防止图片盗链

    [译]Asp.net MVC 利用自定义RouteHandler来防止图片盗链   你曾经注意过在你服务器请求日志中多了很多对图片资源的请求吗?这可能是有人在他们的网站中盗链了你的图片所致,这会占用你 ...

  3. Asp.net MVC 利用自定义RouteHandler来防止图片盗链

    你曾经注意过在你服务器请求日志中多了很多对图片资源的请求吗?这可能是有人在他们的网站中盗链了你的图片所致,这会占用你的服务器带宽.下面这种方法可以告诉你如何在ASP.NET MVC中实现一个自定义Ro ...

  4. [ASP.NET MVC] 利用动态注入HTML的方式来设计复杂页面

    原文:[ASP.NET MVC] 利用动态注入HTML的方式来设计复杂页面 随着最终用户对用户体验需求的不断提高,实际上我们很多情况下已经在按照桌面应用的标准来设计Web应用,甚至很多Web页面本身就 ...

  5. ASP.NET MVC利用PagedList分页(二)PagedList+Ajax+JsRender

    (原文) 昨天在ASP.NET MVC利用PagedList分页(一)的 最后一节提到,一个好的用户体验绝对不可能是点击下一页后刷新页面,所以今天来说说利用Ajax+PagedList实现无刷新(个人 ...

  6. 【转】Asp.net MVC 通过自定义ControllerFactory实现构造器注入(重写DefaultControllerFactory)

    [转]Asp.net MVC 通过自定义ControllerFactory实现构造器注入 一.重写ControllerFactory的GetControllerInstance ControllerF ...

  7. [转]Asp.net MVC 利用PartialView 构造自定义菜单

    本文转自:http://www.cnblogs.com/huyq2002/archive/2012/01/06/2314838.html 在VS2010中利用Asp.net MVC自带的模板生成的菜单 ...

  8. ASP.NET MVC 利用IRouteHandler, IHttpHandler实现图片防盗链

    你曾经注意过在你服务器请求日志中多了很多对图片资源的请求吗?这可能是有人在他们的网站中盗链了你的图片所致,这会占用你的服务器带宽.下面这种方法可以告诉你如何在ASP.NET MVC中实现一个自定义Ro ...

  9. ASP.NET MVC下自定义错误页和展示错误页的几种方式

    在网站运行中,错误是不可避免的,错误页的产生也是不可缺少的. 这几天看了博友的很多文章,自己想总结下我从中学到的和实际中配置的. 首先,需要知道产生错误页的来源,一种是我们的.NET平台抛出的,一种是 ...

随机推荐

  1. .net 4.0 ValidateRequest="false" 无效

    昨天安装了VisualStudio 2010 Ultimate,今天把最近的一个项目升级到了4.0下,结果跑了一下,发现关于页面启用 ValidateRequest="false" ...

  2. php清理当前目录下的指定文件和空目录(源码),建议服务器端执行

    <?php /** * @desc 解析当前目录并递归删除目录下的指定文件 * @author mengdj<mengdj@outlook.com> 2014.10.02 1530 ...

  3. spring里的controller之间的跳转

    未测试: this.getServletContext().getRequestDispatcher("/rentHouse.htm?method=display").forwar ...

  4. [LintCode] Trailing Zeroes 末尾零的个数

    Write an algorithm which computes the number of trailing zeros in n factorial. Have you met this que ...

  5. Centos7 Docker 多主机 容器互连--基于OVS

    来一张自己画的图,mark:2016年6月27日17:09:14 自己理解,如有错误 多谢指教. centos7, 部署OVS和docker.以及基于centos6.8的ssh images 命令. ...

  6. JSP页面以及JSP九大隐式对象

    €JSP全称是Java Server Pages,它和servle技术一样,都是SUN公司定义的一种用于开发动态web资源的技术. €JSP这门技术的最大的特点在于,写jsp就像在写html,但它相比 ...

  7. 谢欣伦 - OpenDev原创例程 - 串口助手Comm Assist

    前一段时间,一位博友发邮件给我.他跟我讲说没太看懂<化繁为简系列原创教程 - 通信专题 - 串口类CxComm的使用>,请我做一个DEMO工程给他.我抽了一天时间编写并上传了一个DEMO工 ...

  8. canvas初探2

    2.2 canvas的绘图环境 canvas仅仅只是一个绘图的容器,其内存在一个绘图环境,该环境对象提供了全部的绘图功能. 目前canvas的绘图环境是2d,但canvas规范在着手准备支持其他类型的 ...

  9. HTML打折计算价格

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <met ...

  10. Concurrency并发性

    今天看了有关性能的文章,性能也是客户所看重的. 文章推荐看了软件编程并发性. 就按书上敲了网址看:http://www.gotw.ca/publications/concurrency-ddj.htm ...