前言
在有一些所有請求都可能需要的操作時,我們可以添加過濾器來完成對請求的攔截,然后進行我們的操作,從而減少代碼的冗余。
關鍵字: 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);
}
}
}
}
總結
許久之后再看到這個問題,只感嘆當時的積累不足,沒有弄清楚過濾器的正確用法。