前言
在有一些所有请求都可能需要的操作时,我们可以添加过滤器来完成对请求的拦截,然后进行我们的操作,从而减少代码的冗余。
关键字: Ajax
跨域
重定向
身份验证拦截
异常拦截
问题
我们在对所有的请求拦截以后,没有考虑 Url请求
和 ajax请求
的区别,就对所有请求一致返回一个页面(登录页、自定义错误页等等),于是 ajax
得到的请求结果为 html
,这不是我们期望的
场景
一、身份验证拦截,Ajax请求跨域重定向到页面后不加载,场景分析
业务场景
目前项目需要使用公司的OAuth身份验证平台做单点登录(Single Sign On,SSO),于是要在目前的MVC项目中添加身份验证相关的业务逻辑,项目中大量表格展示数据,使用了很多Ajax。我对项目大部分请求做了身份验证,Ajax请求也做了拦截。
问题
对未登录用户发起的的Ajax请求,返回重定向的登录页,但是实际页面并不会加载得到的登录页面。
分析
Ajax本就为页面局部刷新修改而做的,返回的页面只会被当做返回的普通数据。
在过滤器中身份验证未通过后,请求被重定向(302
)了项目的login方法,然后根据项目信息重定向(302
)到认证平台的对应登录页面(200
),浏览器拦截跨域请求资源,此时返回给Ajax的Http状态码为0
(就是没有返回状态码的情况)。于是,需要针对返回到Ajax的状态码为0时,做处理。
处理方法
通过强制刷新页面,利用页面请求拦截重新跳转。
在全局js中添加Ajax的请求错误回调设置方法(还有其他方法ajaxComplete之类的,见W3CSchool)
/*ajax请求失败回调默认方法*/
$(document).ajaxError(function (event, xhr, options, exc) {
if (xhr.status === 0) {
window.location.reload(); // 强制刷新页面
}
else if (xhr.status === 302) {
console.log(xhr);
}
else if (xhr.status === 401) {
window.location.href = '/login';
console.log(xhr);
}
else if (xhr.status === 404) {
console.log(xhr);
}
else if (xhr.status === 500) {
console.log(xhr);
}
});
小结
这里暂时处理,返回状态码0的时候直接强制刷新页面,然后对页面刷新的身份验证拦截重定向到登录页。
考虑修改,验证请求类型为Ajax请求时,身份验证失败直接返回失败响应状态码,并使用Js直接重定向到登录页面,省去无效重定向。
续:
使用Request.IsAjaxRequest();
可以判断请求类型,然后就方便处理了。可以对异步请求返回json对页面请求返回视图即可。
MVC身份验证过滤器
类似下面的,过滤器主要进行身份验证,跳转SSO的认证平台及存储获取的令牌等操作由/login
进行,不在此记录。
using System.Web;
using System.Web.Mvc;
namespace Filter
{
/// <summary>
/// 身份验证过滤器
/// 使用方法:[CustAuthorize]特性
/// 1.特性可以使用在Class和Method上,Class上表示内部所有方法需要验证
/// 2.可以Class上[CustAuthorize]并在不需要验证的方法上[AllowAnonymous]
/// </summary>
public class CustAuthorizeAttribute : AuthorizeAttribute
{
/// <summary>
/// 请求的地址
/// </summary>
private string requestUrl { get; set; }
/// <summary>
/// 请求授权时执行
/// </summary>
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext); //进入AuthorizeCore
}
/// <summary>
/// 自定义授权检查(返回False则授权失败)
/// </summary>
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext.Session["tk"] != null && !string.IsNullOrEmpty(httpContext.Session["tk"].ToString()))
{
string tk = httpContext.Session["tk"].ToString(); // 令牌,获取到以后存在Session了,
bool check = Check.check(tk); // 验证方法
if (check)
return true;
}
httpContext.Session["tk"] = null;
return false; // 进入HandleUnauthorizedRequest
}
/// <summary>
/// 处理授权失败的HTTP请求
/// </summary>
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest()) // ajax请求
{
filterContext.HttpContext.Response.StatusCode = 401; //返回前端 js对此类返回状态拦截响应或刷新页面(转页面请求重定向至登录)
var json = JsonConvert.SerializeObject(new { state = "0", message = "权限验证失败!请重新登录" });
filterContext.HttpContext.Response.Write(json);
filterContext.HttpContext.Response.End();
}
else // 未登录请求页面地址
{
filterContext.HttpContext.Session["requestUrl"] = filterContext.HttpContext.Request.Url.AbsolutePath;
// 重定向到登录
filterContext.Result = new RedirectResult("~/login");
}
}
}
}
登录及回调方法
public class OAuthController : Controller
{
[Route("login")]
public ActionResult Login()
{
string redirect = "~/index";
// 未登录
if (string.IsNullOrEmpty(Session["tk"]?.ToString()))
{
// 获取认证服务器页面地址(请求地址+参数:回调地址等)
redirect = SSOServer.GetLoginPageUrl(
$"http://{HttpContext.Request.Url?.Host}:{HttpContext.Request.Url?.Port}/loginCallBack");
}
return Redirect(redirect);
}
[Route("loginCallBack")]
public ActionResult LoginCallBack(string tk)
{
string redirect = "~/index";
if (string.IsNullOrEmpty(tk)) // 无令牌
{
redirect = "~/login";
}
else // 登录返回令牌
{
UserInfo user = SSOServer.GetUserInfo(tk); // 认证服务器获取用户信息
if (user == null || (user.userType != UserType.Admin)) // 角色限制
{
SSOServer.Logout(tk); // 登出
Session.Clear();
redirect = "~/Error/Forbidden"; //自定义错误页
}
else
{
Session["tk"] = tk;
string requestUrl = Session["requestUrl"]?.ToString(); // 跳转回登录前页面
Session["requestUrl"] = null;
if (!string.IsNullOrEmpty(requestUrl) && !requestUrl.Contains("login"))
redirect = requestUrl;
}
}
return Redirect(redirect);
}
[Route("logout")]
[CustAuthorize]
public ActionResult Logout()
{
SSOServer.Logout(Session["tk"].ToString());
Session.Abandon();
return Redirect("~/login");
}
}
二、MVC自定义动态错误页面后,Ajax错误回调方法接收不到对应状态码
关键字: Ajax
http状态码
业务场景
MVC项目的错误页面自定义(即通过控制器获取Razor视图而不是HTML静态页面)后,Ajax的请求接收到Http状态码只有200的情况。
(静态的页面,MVC可以在Web.config中配置状态码)
分析
我在Global.asax
里做了简单的500错误拦截,拦截后直接跳转了自定义的错误页面,动态重定向到指定控制器方法后,正常返回页面,所以状态码是200。
处理方法
-
需要返回错误状态
Response.StatusCode = 500;
控制器对应方法返回动态的错误页面时,先修改响应状态码,然后Ajax就可以收到错误了,可以像上面一个问题里拦截回调处理,做提示一下之类的操作。 -
正确的异常拦截姿势
这里是目前我觉得比较正确的处理方法,参考asp.net MVC 过滤器使用案例:统一处理异常顺道精简代码 —— 无恨星晨
using Utils.LogUtils;
using System;
using System.Web;
using System.Web.Mvc;
namespace Filter
{
public class HandleExceptionAttribute: HandleErrorAttribute
{
public override void OnException(ExceptionContext ctx)
{
// 异常及路由信息
Exception innerException = ctx.Exception;
string controllerName = (string)ctx.RouteData.Values["controller"];
string actionName = (string)ctx.RouteData.Values["action"];
//Logger.ErrorLog(typeof(HandleExceptionAttribute), innerException,
"OnException捕获:{0}->{1}:", controllerName, actionName);
if ((new HttpException(null, innerException).GetHttpCode() == 500))
{
ActionResult result = null;
if (!ctx.HttpContext.Request.IsAjaxRequest()) // 页面请求
{
HandleErrorInfo model = new HandleErrorInfo(innerException, controllerName, actionName);
result = new ViewResult
{
ViewName = "~/Views/Shared/Error.cshtml",
MasterName = this.Master,
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = ctx.Controller.TempData
};
}
else // 异步请求
{
result = new JsonResult
{
Data = new Model.VO.MessageVO {
state = "error",
message = $"OnException捕获:{controllerName}->{actionName}:{innerException}"
}
};
}
ctx.Result = result;
ctx.ExceptionHandled = true; // 避免再次进行默认异常处理
ctx.HttpContext.Response.Clear();
ctx.HttpContext.Response.StatusCode = 500;
ctx.HttpContext.Response.TrySkipIisCustomErrors = true;
}
else // 其他可能的异常,使用默认处理
{
base.OnException(ctx);
}
}
}
}
总结
许久之后再看到这个问题,只感叹当时的积累不足,没有弄清楚过滤器的正确用法。