[ASP.NET MVC] 利用自定义的AuthenticationFilter实现Basic认证
很多情况下目标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: }
在目标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”的响应。