处理 AJAX 调用的特定于身份验证的问题





5.00/5 (12投票s)
对于现代 Web 应用程序而言,使用 AJAX 创建用户界面已成为常态。然而,这有时也会让我们头疼。而这些困难往往与身份验证和客户端对这类请求的处理有关。
引言
对于现代 Web 应用程序而言,使用 AJAX 创建用户界面已成为常态。然而,这有时也会让我们头疼。而这些困难往往与身份验证和客户端对这类请求的处理有关。
问题所在
设想一下,你有一个 Web 应用程序,它通过 jQuery 和 JSON 与服务器通信。
服务器端
[HttpPost] public ActionResult GetData() { return Json(new { Items = new[] { "Li Chen", "Abdullah Khamir", "Mark Schrenberg", "Katy Sullivan", "Erico Gantomaro", } }); }
客户端
var $list = $("#list"); var $status = $("#status"); $list.empty(); $status.text("Loading..."); $.post("/home/getdata") .always(function() { $status.empty(); }) .success(function(data) { for (var i = 0; i < data.Items.length; i++) { $list.append($("<li/>").text(data.Items[i])); } });
这很简单。让我们为 Web 应用程序添加一个简单的身份验证。这也很简单——只需使用 ASP.NET MVC 的 Forms Authentication 和 Authorize 属性。控制器代码将更改为
[HttpPost] [Authorize] public ActionResult GetData() { return Json(new { Items = new[] { "Li Chen", "Abdullah Khamir", "Mark Schrenberg", "Katy Sullivan", "Erico Gantomaro", } }); }
目前仍然没有问题。一旦用户在应用程序中通过身份验证,他就可以看到页面并检索数据。然而,在身份验证超时到期时会出现一些问题。在这种情况下,服务器将向用户发送……HTTP 302 Found。
显然,在这种情况下,客户端代码并未预料到这一点,结果就是应用程序无法正常工作。
原因
在我们修复代码之前,先来理解一下发生了什么。为什么服务器发送 HTTP 302?很自然地会 假设 应该是 HTTP 401。关键在于,当我们使用 FormsAuthentication 时,后台是 FormsAuthenticationModule(默认在全局 web.config 文件中注册)。深入了解该模块,你会很容易理解,如果当前的 HTTP 代码是 401,它就会执行重定向,即将其替换为 302。
可以很容易地猜到,目的是在请求未成功处理(状态码为 401)时将用户重定向到登录页面。在这种情况下,用户将看到良好的登录表单,而不是 IIS 错误代码。这很有道理,不是吗?
从我们的 ASP.NET MVC 应用程序的角度来看,管道大致如下:
- 一个请求进入应用程序,遇到一个名为 AuthorizeAttribute 的过滤器。
- 由于用户未通过身份验证,该过滤器将返回 HTTP 401,这是符合逻辑的(使用反射很容易查看此行为,并检查该过滤器的实现)。
- 好了,之后我们的 FormsAuthenticationModule 就会介入,并将 HTTP 401 替换为重定向。
结果——当我们尝试使用常规 HTTP 请求访问页面时,我们会看到登录页面(这是好的),但当我们使用 AJAX 调用时,解析这样的响应(这是糟糕的)会非常困难。
解决方案
那么,我们需要解决的问题是——
- 服务器应为 AJAX 调用返回 HTTP 401/403,为常规 HTTP 调用返回 HTTP 302。
- 在客户端处理 HTTP 401/403。
说实话,你可以重写 FormsAuthenticationModule 的逻辑,使其不将 HTTP 401 请求替换为 302。为此,你可以使用 **SuppressFormsAuthenticationRedirect** 属性。
唯一的问题是何时修改此属性,以及由谁来修改?
在回答这个问题之前,让我们先关注一下客户端如何处理 HTTP 错误情况。有两种情况——
- 用户未通过应用程序身份验证(**401**)。在这种情况下,我们应该将他重定向到登录页面。
- 用户已通过应用程序身份验证,但没有足够的权限访问资源(**403**)。例如,用户不在当前 HTTP 请求所必需的角色中。在这种情况下,将其重定向到登录页面是很愚蠢的。更可取的做法是告知他没有必要的权限。
因此,我们应该处理这两种情况。让我们再次看看 AuthorizeAttribute。
……即它将始终发送 HTTP 401。
不好。因此,我们应该稍微修复一下此行为。那么,让我们开始吧。
第一——让我们确定当前 HTTP 请求是否为 AJAX 请求。如果是,我们应该禁用将 HTTP 401 替换为 HTTP 302。
public class ApplicationAuthorizeAttribute : AuthorizeAttribute { protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { var httpContext = filterContext.HttpContext; var request = httpContext.Request; var response = httpContext.Response; if (request.IsAjaxRequest()) response.SuppressFormsAuthenticationRedirect = true; base.HandleUnauthorizedRequest(filterContext); } }
第二——让我们添加一个条件:如果用户已通过身份验证,我们将发送 HTTP 403;否则发送 HTTP 401。
public class ApplicationAuthorizeAttribute : AuthorizeAttribute { protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { var httpContext = filterContext.HttpContext; var request = httpContext.Request; var response = httpContext.Response; var user = httpContext.User; if (request.IsAjaxRequest()) { if (user.Identity.IsAuthenticated == false) response.StatusCode = (int)HttpStatusCode.Unauthorized; else response.StatusCode = (int)HttpStatusCode.Forbidden; response.SuppressFormsAuthenticationRedirect = true; response.End(); } base.HandleUnauthorizedRequest(filterContext); } }
干得好。现在我们应该用这个新的过滤器替换所有标准 AuthorizeAttribute 的用法。对于某些注重代码美观的人来说,这可能不适用。但我不知道有其他方法。如果你有,请到评论区交流。
最后,我们需要做的是——在客户端添加 HTTP 401/403 的处理。我们可以使用 jQuery 的 ajaxError 来避免代码重复。
$(document).ajaxError(function (e, xhr) { if (xhr.status == 401) window.location = "/Account/Login"; else if (xhr.status == 403) alert("You have no enough permissions to request this resource."); });
结果——
- 如果用户未通过身份验证,那么在任何 AJAX 调用后,他将被重定向到登录页面。
- 如果用户已通过身份验证,但没有足够的权限,那么他将看到用户友好的错误消息。
- 如果用户已通过身份验证并拥有足够的权限,那么不会有任何错误,HTTP 请求将按常规方式进行。
唯一的缺点是必须使用 ApplicationAuthorizeAttribute 而不是标准的 AuthorizeAttribute。所以,如果你有使用 AuthorizeAttribute 的代码,你需要修复它。
源代码可在 github 上找到。