65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2013 年 9 月 18 日

CPOL

4分钟阅读

viewsIcon

89254

对于现代 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 Found

显然,在这种情况下,客户端代码并未预料到这一点,结果就是应用程序无法正常工作。

原因

在我们修复代码之前,先来理解一下发生了什么。为什么服务器发送 HTTP 302?很自然地会 假设 应该是 HTTP 401。关键在于,当我们使用 FormsAuthentication 时,后台是 FormsAuthenticationModule(默认在全局 web.config 文件中注册)。深入了解该模块,你会很容易理解,如果当前的 HTTP 代码是 401,它就会执行重定向,即将其替换为 302。

Redirect

可以很容易地猜到,目的是在请求未成功处理(状态码为 401)时将用户重定向到登录页面。在这种情况下,用户将看到良好的登录表单,而不是 IIS 错误代码。这很有道理,不是吗?

从我们的 ASP.NET MVC 应用程序的角度来看,管道大致如下:

  1. 一个请求进入应用程序,遇到一个名为 AuthorizeAttribute 的过滤器。
  2. 由于用户未通过身份验证,该过滤器将返回 HTTP 401,这是符合逻辑的(使用反射很容易查看此行为,并检查该过滤器的实现)。
  3. 好了,之后我们的 FormsAuthenticationModule 就会介入,并将 HTTP 401 替换为重定向。

结果——当我们尝试使用常规 HTTP 请求访问页面时,我们会看到登录页面(这是好的),但当我们使用 AJAX 调用时,解析这样的响应(这是糟糕的)会非常困难。

解决方案

那么,我们需要解决的问题是——

  1. 服务器应为 AJAX 调用返回 HTTP 401/403,为常规 HTTP 调用返回 HTTP 302。
  2. 在客户端处理 HTTP 401/403。

说实话,你可以重写 FormsAuthenticationModule 的逻辑,使其不将 HTTP 401 请求替换为 302。为此,你可以使用 **SuppressFormsAuthenticationRedirect** 属性。

SuppressFormsAuthenticationRedirect

唯一的问题是何时修改此属性,以及由谁来修改?

在回答这个问题之前,让我们先关注一下客户端如何处理 HTTP 错误情况。有两种情况——

  1. 用户未通过应用程序身份验证(**401**)。在这种情况下,我们应该将他重定向到登录页面。
  2. 用户已通过应用程序身份验证,但没有足够的权限访问资源(**403**)。例如,用户不在当前 HTTP 请求所必需的角色中。在这种情况下,将其重定向到登录页面是很愚蠢的。更可取的做法是告知他没有必要的权限。

因此,我们应该处理这两种情况。让我们再次看看 AuthorizeAttribute

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 上找到。

© . All rights reserved.