[ASP.MVC] 當jQuery Ajax呼叫遇上Login Timeout的處理
前言
一般而言,不管是.net的表單驗證登入機制,或傳統的Session登入機制
如果Login Timeout的話,我們會把程式流程做一個Redirect到登入頁的動作
但對於Ajax發到Server端的Request,如果也是照平常處理,可能會發生以下詭異情況…
當Login timeout時,取回來的值變成登入頁了
以上情況的Source Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MvcApplicationlogintimeout.Controllers
{
public class HomeController : Controller
{
//預設進來的Action
public ActionResult Index()
{
Session["Test"] = "Test";
return View();
}
/// <summary>
/// Ajax 呼叫這個Action
/// </summary>
/// <returns></returns>
[HttpPost]
public ActionResult Test()
{
if (Session["Test"]==null)//Login timeout
{//重新導向
return RedirectToAction("Login","Account");
}
return Content("Ajax 取得值了");
}
}
}
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
<script src="~/Scripts/jquery-2.1.0.min.js"></script>
<script type="text/javascript">
$(document).ready(init);
function init()
{
$("input[type='button']").click(function () {
$.ajax({
url: "@Url.Action("Test","Home")",
data: {},
type: "post",
async: true,
cache: false,
success: function (result)
{ //顯示回傳結果
$("#divResult").html(result);
},
error: function ()
{
}
});
});
}
</script>
</head>
<body>
<input type="button" value="click me" />
@*ajax回傳的結果*@
<div id="divResult">
</div>
</body>
</html>
要解決這種情況發生的話,對於Ajax呼叫,Server端當然不能照常Redirect,以下分3種實務上可能碰到情形的各解法
實作
1.表單驗證Forms Authentication登入的改寫
承接之前寫過的文章:[ASP.net MVC] ASP.net MVC整合FormsAuthentication表單驗證登入 - 簡易範例程式碼
這次先新增一個自訂類別名為:AjaxAuthorizeAttribute去繼承AuthorizeAttribute
public class AjaxAuthorizeAttribute : AuthorizeAttribute
{
/// <summary>
/// 處理未授權的請求
/// </summary>
/// <param name="filterContext"></param>
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())//是Ajax的話
{
filterContext.HttpContext.Response.StatusCode = 440;//Login timeout
filterContext.HttpContext.Response.End();
}
else
{//不是Ajax的話
//預設 Http StatusCode 302 表單驗證自動重新導向
base.HandleUnauthorizedRequest(filterContext);
}
}
}
然後把原本檢查登入的Controller或Action上方的[Authorize]屬性換為剛寫的[AjaxAuthorize]
範例:
最後一步驟則把以下程式碼貼到可能發出Ajax的View或_Layout.cshtml的<head>的<script>區段內
※需注意,畢竟Status Code 440,算是error的一種,$.ajax() 方法內的error callback function會先執行才再執行上述$.ajaxSetup()的重新導向
※另外,網路上看網友討論,有人設Status Code為401,我執行會出現以下popup視窗,所以改用440 Status Code表示Login timeout避免此問題 (見Wiki: http://en.wikipedia.org/wiki/List_of_HTTP_status_codes)
2. Ajax執行的Action,使用到的Session改寫
例如一開始前言提到的程式碼,改為
View的方面,為每個可能呼叫ajax的頁面(加在_Layout.cshtml也可以)
加上以下jQuery程式碼(標註※※※的區段)
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
<script src="~/Scripts/jquery-2.1.0.min.js"></script>
<script type="text/javascript">
$(document).ready(init);
function init()
{
//※※※為所有的$.ajax呼叫設定預設值,當遇到StatusCode為440時,頁面導至登入頁
$.ajaxSetup({
statusCode: {
440: function () {
window.location.href = "@Url.Action("Login","Account")";
}
}
});
//按鈕click事件
$("input[type='button']").click(function () {
$.ajax({
url: "@Url.Action("Test","Home")",
data: {},
type: "post",
async: true,
cache: false,
success: function (result)
{ //顯示回傳結果
$("#divResult").html(result);
},
error: function (xhr)
{
}
});
});
}
</script>
</head>
<body>
<input type="button" value="click me" />
@*ajax回傳的結果*@
<div id="divResult">
</div>
</body>
</html>
3. 採用傳統Session機制做登入的系統
先加入一個自訂Filter類別(CheckLoginSessionExpired.cs)去繼承ActionFilterAttribute類別
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MvcApplicationlogintimeout.Filters
{
/// <summary>
/// 會員未登入 Session過期就導至Login頁
/// </summary>
public class CheckLoginSessionExpired : ActionFilterAttribute
{
//Action執行前
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//未登入
//Session["loginUser"]是登入後會塞進去的一個使用者資料物件
if (filterContext.RequestContext.HttpContext.Session["loginUser"] == null)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())//是Ajax的話
{
filterContext.HttpContext.Response.StatusCode = 440;//Login timeout
filterContext.HttpContext.Response.End();
}
else
{
//正在要求的Url
string sourceUrlString = HttpUtility.UrlEncode(filterContext.HttpContext.Request.Url.OriginalString);
//導至登入頁
filterContext.HttpContext.Response.Redirect("~/Account/Login?ReturnUrl=" + sourceUrlString);
}
}
}
}
}
再把需要檢查是否登入的Action或Controller加上[CheckLoginSessionExpirec]屬性
例如以下
using MvcApplicationlogintimeout.Filters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MvcApplicationlogintimeout.Controllers
{
public class HomeController : Controller
{
//預設進來的Action
public ActionResult Index()
{
return View();
}
/// <summary>
/// Ajax 呼叫這個Action
/// </summary>
/// <returns></returns>
[CheckLoginSessionExpired]
[HttpPost]
public ActionResult Test()
{
//正常的Ajax呼叫會直接回傳以下文字
return Content("取得Ajax值了");
}
}
}
最後同樣,為所有可能呼叫ajax的頁面或_Layout.cshtml加上$.ajaxSetup設定當遇到StatusCode為440時的處理方式
結語
如此一來,當畫面上呼叫ajax時,若遇到Login timeout時…
另外補充,在$.ajaxSetup裡的導頁程式碼,如果想額外加上ReturnUrl的QueryString讓登入頁的程式碼判斷使用者原先是停留在哪裡的話…