[ASP.MVC] 當jQuery Ajax呼叫遇上Login Timeout的處理

[ASP.MVC] 當jQuery Ajax呼叫遇上Login Timeout的處理

前言

一般而言,不管是.net的表單驗證登入機制,或傳統的Session登入機制

如果Login Timeout的話,我們會把程式流程做一個Redirect到登入頁的動作

但對於Ajax發到Server端的Request,如果也是照平常處理,可能會發生以下詭異情況…

image

當Login timeout時,取回來的值變成登入頁了

image

以上情況的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]

範例:

image

最後一步驟則把以下程式碼貼到可能發出Ajax的View或_Layout.cshtml的<head>的<script>區段內

image

 

※需注意,畢竟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改寫

例如一開始前言提到的程式碼,改為

image

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時的處理方式

image

結語

如此一來,當畫面上呼叫ajax時,若遇到Login timeout時…

image

 

另外補充,在$.ajaxSetup裡的導頁程式碼,如果想額外加上ReturnUrl的QueryString讓登入頁的程式碼判斷使用者原先是停留在哪裡的話…

參考文章

Handling session timeout in ajax calls

Handling session time out when ajax call to C# mvc controller not working