在开发Web应用时,经常会遇到各种问题,其中一些与用户认证和处理客户端请求有关。这些问题可能会让头疼不已。本文将探讨这些问题,并提供一些解决方案。
假设有一个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($("").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调用时,解析这样的响应就变得非常困难(这是不好的)。
需要解决的问题是:
为了实现这一点,可以覆盖FormsAuthenticationModule的逻辑,使其不要将HTTP 401请求替换为302。要做到这一点,可以使用SuppressFormsAuthenticationRedirect属性:
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.");
});