如何在jQuery Ajax调用后处理重定向请求

1531
我使用 $.post() 方法通过Ajax调用servlet,然后使用返回的HTML片段替换用户当前页面中的一个 div 元素。但是,如果会话超时,服务器会发送重定向指令将用户发送到登录页面。在这种情况下,jQuery会将 div 元素替换为登录页面的内容,使用户看到一个罕见的场景。
如何在使用jQuery 1.2.6时处理来自Ajax调用的重定向指令?

1
我过去曾通过编辑jQuery库并在每个XHR完成时添加对登录页面的检查来解决此问题。这不是最好的解决方案,因为每次升级都必须这样做,但它确实解决了问题。 - Sugendran
1
请参考相关问题:https://dev59.com/nG025IYBdhLWcg3wjGpO - Nutel
HttpContext.Response.AddHeader 和检查 ajaxsetup 的成功是可行的方法。 - LCJ
6
为什么服务器不能返回401状态码?如果是这样,你可以使用全局的$.ajaxSetup并使用状态码来重定向页面。 - Vishal
1
这个链接 http://doanduyhai.wordpress.com/2012/04/21/spring-security-part-vi-session-timeout-handling-for-ajax-calls/ 给了我正确的解决方案。 - pappu_kutty
请查看此答案 - Chris
34个回答

761

我阅读了这个问题,并实现了已经提到的方法,将响应HTTP状态码设置为278,以避免浏览器透明地处理重定向。尽管这种方法有效,但我还是有点不满意,因为它有点像一个hack。

经过更多的研究,我放弃了这种方法,改用JSON。在这种情况下,所有针对AJAX请求的响应都具有状态码200,响应正文包含在服务器上构建的JSON对象。然后客户端的JavaScript可以使用JSON对象来决定它需要做什么。

我遇到了类似于你的问题。我执行了一个AJAX请求,它有两个可能的响应:一个将浏览器重定向到一个新页面,另一个用一个新的HTML表单替换当前页面上的现有表单。处理这个的jQuery代码看起来像:

$.ajax({
    type: "POST",
    url: reqUrl,
    data: reqBody,
    dataType: "json",
    success: function(data, textStatus) {
        if (data.redirect) {
            // data.redirect contains the string URL to redirect to
            window.location.href = data.redirect;
        } else {
            // data.form contains the HTML for the replacement form
            $("#myform").replaceWith(data.form);
        }
    }
});

服务器上构建JSON对象"data",包含两个成员:data.redirectdata.form。我发现这种方法要好得多。


70
如https://dev59.com/_3RB5IYBdhLWcg3wz6Wd中的解决方案所述,最好使用window.location.replace(data.redirect);而不是window.location.href = data.redirect; - Carles Barrobés

267
我通过以下方式解决了这个问题:
  1. 向响应添加自定义标头:

    public ActionResult Index(){
        if (!HttpContext.User.Identity.IsAuthenticated)
        {
            HttpContext.Response.AddHeader("REQUIRES_AUTH","1");
        }
        return View();
    }
    
  2. 将JavaScript函数绑定到 ajaxSuccess 事件,并检查标头是否存在:

    $(document).ajaxSuccess(function(event, request, settings) {
        if (request.getResponseHeader('REQUIRES_AUTH') === '1') {
           window.location = '/';
        }
    });
    

7
多好的解决方案啊。我喜欢一站式解决方案的想法。我需要检查403状态,但我可以使用ajaxSuccess绑定到body来完成这个操作(这正是我真正想要的)。谢谢。 - Bretticus
4
我刚刚做了这件事,发现在使用$.get()函数时需要用到ajaxComplete方法,否则任何状态码除了200以外都不会触发。实际上,我可能只需要绑定ajaxError方法就可以了。有关更多详细信息,请参见下面的答案。 - Bretticus
2
这个解决方案需要浏览器知道认证的URL。虽然这解决了问题,但在我看来,这不是一个可维护的解决方案。如果认证控制器发生变化,现在你需要维护路由文件和JavaScript。也就是说,这是一种糟糕的编程实践。 - mwoods79
14
我喜欢标题的方式,但也像@mwoods79一样认为不应该重复记录重定向的位置。我通过添加一个头文件REDIRECT_LOCATION而不是布尔值来解决了这个问题。 - rintcius
3
请注意,在重定向之后设置响应头。正如本页面其他答案所述,重定向可能对ajaxSuccess处理程序透明。因此,在我的情况下,我将响应头包含在登录页面的GET响应中(最终只触发ajaxSuccess)。 - sieppl
显示剩余4条评论

126
没有任何浏览器能正确处理301和302响应。实际上,标准甚至规定它们应该被“透明地”处理,这对Ajax库供应商来说是一个巨大的头疼问题。在Ra-Ajax中,我们不得不使用HTTP响应状态码278(只是一些“未使用”的成功代码)来处理从服务器透明重定向...
这真的很让我恼火,如果这里有人在W3C内部有影响力,我将非常感激您能让W3C知道我们真的需要自己处理301和302代码…! ;)

3
我建议将278纳入官方HTTP规范。 - Chris Marisic
2
它不已经透明地处理它们了吗?如果资源已移动,透明地处理它意味着在提供的URL上重复请求。这就是我使用XMLHttpRequest API所期望的。 - Philippe Rathé
@PhilippeRathé 我同意。透明地处理正是我想要的。而且我不知道为什么这被认为是不好的。 - smwikipedia
@smwikipedia 安排在主要部分重定向标记而不重定向页面。 - cmc
1
浏览器从一开始就透明地处理它。什么是ajax请求?当您想要浏览器发出请求并返回结果时,就会发生这种情况。而你想要的是一个特殊情况:“...返回结果,但在重定向的情况下不返回结果。”这可能会让您作为库开发人员感到烦恼,因为您正在尝试处理一般情况,改变浏览器的工作方式。但既然您这样做了,就应该预料到烦恼的出现。 - x-yuri
显示剩余4条评论

105

最终实现的解决方案是使用Ajax调用的回调函数的包装器,并在该包装器中检查返回的HTML代码块上是否存在特定元素。如果找到了该元素,则包装器执行重定向操作。否则,包装器将调用转发给实际的回调函数。

例如,我们的包装器函数如下:

function cbWrapper(data, funct){
    if($("#myForm", data).length > 0)
        top.location.href="login.htm";//redirection
    else
        funct(data);
}

然后,在进行Ajax调用时,我们使用类似以下的代码:

$.post("myAjaxHandler", 
       {
        param1: foo,
        param2: bar
       },
       function(data){
           cbWrapper(data, myActualCB);
       }, 
       "html"
);

这个方法对我们有效,因为所有的Ajax调用都会在一个DIV元素内返回HTML,我们使用该元素替换页面上的一部分。此外,我们只需要重定向到登录页面。


4
请注意,这段代码可以简写为函数cbWrapper(funct) { return function(data) { if($("#myForm", data).size() > 0) top.location.href="login"; else funct(data); } }。在调用.post时,只需要使用cbWrapper(myActualCB)即可。是的,注释中的代码混乱,但需要注意 :) - Simen Echholt
"size已经被弃用,所以你可以在这里使用.length来代替size。" - sunil

75

我喜欢Timmerz的方法,稍微加点柠檬味。如果你期望得到JSON格式但实际上返回了text/html类型,很可能是因为发生了重定向。在我的情况中,我只需简单地重新加载页面,它就会被重定向到登录页面。同时要检查jqXHR状态是否为200,这似乎很愚蠢,因为你正在error函数中,对吗?否则,合法的错误情况将强制进行迭代式重新加载(糟糕)。

$.ajax(
   error:  function (jqXHR, timeout, message) {
    var contentType = jqXHR.getResponseHeader("Content-Type");
    if (jqXHR.status === 200 && contentType.toLowerCase().indexOf("text/html") >= 0) {
        // assume that our login has expired - reload our current page
        window.location.reload();
    }

});

1
非常感谢你的回答,Brian。对于我的情况来说,你的回答是最好的。不过,如果有一个更安全的检查方法,比如比较重定向到哪个URL/页面,而不是简单的“内容类型”检查,那就更好了。我无法从jqXHR对象中找到重定向到哪个页面。 - Johnny
我检查了401状态码,然后进行了重定向。完美运行。 - Eric

59

使用低级别的$.ajax()调用:

$.ajax({
  url: "/yourservlet",
  data: { },
  complete: function(xmlHttp) {
    // xmlHttp is a XMLHttpRquest object
    alert(xmlHttp.status);
  }
});

尝试以下代码进行重定向:

if (xmlHttp.code != 200) {
  top.location.href = '/some/other/page';
}

4
顺便说一下,$.ajax() 并不是非常非常低级的。它在 jQuery 中只是相对较低级的,因为有 $.get、$.post 等比 $.ajax 和其所有选项更简单的方法。 - Till
17
哎呀!抱歉我不得不“取消接受”你的答案,它仍然非常有帮助。问题是,重定向是由XMLHttpRequest自动管理的,因此在重定向后我总是得到一个200状态码(叹气!)。我想我将不得不做一些不好的事情,比如解析HTML并寻找标记。 - Elliot Vargas
1
只是好奇,如果会话在服务器端结束,那么这是否意味着服务器发送了不同的SESSIONID?我们不能检测到吗? - Salamander2007
10
注意:此方法无法用于重定向。Ajax会跳转到新页面并返回其状态码。 - user34537
如果服务器返回302响应,则始终会得到200响应代码。浏览器会透明地处理302并向新位置发出第二个请求。 - Sai Dubbaka
显示剩余3条评论

36

我想分享一下我的方法,希望能帮到有需要的人:

我主要在代码中引入了一个JavaScript模块,用于处理身份验证相关的内容,例如显示用户名,以及在这种情况下处理重定向到登录页面的问题

我的情况是:我们基本上有一个ISA服务器处于中间位置,它监听所有请求并响应302和位置标头到我们的登录页面。

在我的JavaScript模块中,我的初始方法如下:

$(document).ajaxComplete(function(e, xhr, settings){
    if(xhr.status === 302){
        //check for location header and redirect...
    }
});

问题(正如许多人在这里已经提到的)是浏览器自己处理了重定向,因此我的ajaxComplete回调从未被调用,而是得到了已经重定向的登录页面的响应,显然这是一个状态200。 问题是:如何检测成功的200响应是您实际的登录页面还是其他任意页面?

解决方案

由于我无法捕获302重定向响应,因此我在我的登录页面上添加了一个名为LoginPage的头文件,其中包含登录页面本身的URL。 现在在模块中监听该标头并进行重定向:

if(xhr.status === 200){
    var loginPageRedirectHeader = xhr.getResponseHeader("LoginPage");
    if(loginPageRedirectHeader && loginPageRedirectHeader !== ""){
        window.location.replace(loginPageRedirectHeader);
    }
}

......这个方法非常有效 :). 你可能会想知道为什么我将url包含在LoginPage头信息中...基本上是因为我找不到一种确定xhr对象自动重定向后的GET请求的URL的方法...


+1 - 但是自定义头部应该以"X-"开头,所以更好的头部应该是"X-LoginPage: http://example.com/login"。 - uınbɐɥs
6
不再需要了。我没有使用X-前缀,因为在2011年6月,一个ITEF文件提议弃用它们,确实,到了2012年6月,正式规定自定义头不再需要以“X-”为前缀。 - Juri
我们还有一个ISA服务器,我刚遇到了同样的问题。与其在代码中解决它,我们使用kb2596444中的说明来配置ISA停止重定向。 - scott stone

31

我知道这个主题很久了,但是我会提供另一种方法,之前在这里描述过。基本上,我正在使用ASP.MVC和WIF (但这对于此主题的上下文并不重要 - 无论使用哪些框架,答案都是合适的。关键是解决与执行ajax请求时出现的身份验证失败相关的问题)

下面展示的方法可以直接应用于所有ajax请求(如果它们没有重新定义beforeSend事件)。

$.ajaxSetup({
    beforeSend: checkPulse,
    error: function (XMLHttpRequest, textStatus, errorThrown) {
        document.open();
        document.write(XMLHttpRequest.responseText);
        document.close();
    }
});
在执行任何ajax请求之前,都会调用CheckPulse方法(控制器方法可以是任何最简单的形式):
[Authorize]
public virtual void CheckPulse() {}
如果用户未经身份验证(令牌已过期),则无法访问此方法(受Authorize属性保护)。因为框架处理身份验证,而令牌过期时,它会将HTTP状态302放入响应中。如果您不希望浏览器透明地处理302响应,请在Global.asax中捕获它并更改响应状态 - 例如更改为200 OK。此外,添加一个标题,指示您以特殊方式处理此类响应(稍后在客户端上):
protected void Application_EndRequest()
{
    if (Context.Response.StatusCode == 302
        && (new HttpContextWrapper(Context)).Request.IsAjaxRequest())
    {                
        Context.Response.StatusCode = 200;
        Context.Response.AddHeader("REQUIRES_AUTH", "1");
    }
}

最后在客户端检查这样的自定义头。如果存在-应该进行完全重定向到登录页面(在我的情况下,window.location被请求中处理的url替换,这由我的框架自动处理)。

function checkPulse(XMLHttpRequest) {
    var location = window.location.href;
    $.ajax({
        url: "/Controller/CheckPulse",
        type: 'GET',
        async: false,
        beforeSend: null,
        success:
            function (result, textStatus, xhr) {
                if (xhr.getResponseHeader('REQUIRES_AUTH') === '1') {
                    XMLHttpRequest.abort(); // terminate further ajax execution
                    window.location = location;
                }
            }
    });
}

我通过使用PostAuthenticateRequest事件而不是EndRequest事件来解决了这个问题。 - Frinavale
@JaroslawWaliszko 我在上一条回复中贴错了事件!我指的是 PreSendRequestHeaders 事件.....而不是 PostAuthenticateRequest!>>脸红<< 谢谢你指出我的错误。 - Frinavale
@JaroslawWaliszko 当使用 WIF 时,您还可以针对 AJAX 请求返回 401 响应,并让您的 JavaScript 处理这些响应。另外,您假设所有的 302 都需要身份验证,但在某些情况下可能并不是真实情况。如果有人感兴趣,我已经添加了一个答案。 - Rob

30

我认为更好的处理方式是利用现有的HTTP协议响应代码,特别是401 Unauthorized

这是我的解决方法:

  1. 服务器端:如果会话过期,并且请求是ajax,则发送401响应代码头。
  2. 客户端:绑定到ajax事件。

$('body').bind('ajaxSuccess',function(event,request,settings){
if (401 == request.status){
    window.location = '/users/login';
}
}).bind('ajaxError',function(event,request,settings){
if (401 == request.status){
    window.location = '/users/login';
}
});

在我看来,这种方法更加通用,而且你不需要编写新的自定义规范/头文件。此外,您也不需要修改任何现有的ajax调用。

编辑:根据@Rob下面的评论,401(身份验证错误的HTTP状态代码)应该是指示器。有关更多详细信息,请参见403 Forbidden vs 401 Unauthorized HTTP responses。尽管如此,一些Web框架将403用于身份验证和授权错误 - 因此请相应地进行调整。谢谢,Rob。


我使用相同的方法。jQuery在403错误代码上真的调用ajaxSuccess吗?我认为只有ajaxError部分实际上是必要的。 - Marius Balčytis

25

我是这样解决这个问题的:

添加一个中间件来处理响应,如果响应是一个针对Ajax请求的重定向,则将响应更改为普通响应,并提供重定向URL。

class AjaxRedirect(object):
  def process_response(self, request, response):
    if request.is_ajax():
      if type(response) == HttpResponseRedirect:
        r = HttpResponse(json.dumps({'redirect': response['Location']}))
        return r
    return response

然后在ajaxComplete中,如果响应包含重定向,则必须是重定向,因此更改浏览器的位置。

$('body').ajaxComplete(function (e, xhr, settings) {
   if (xhr.status == 200) {
       var redirect = null;
       try {
           redirect = $.parseJSON(xhr.responseText).redirect;
           if (redirect) {
               window.location.href = redirect.replace(/\?.*$/, "?next=" + window.location.pathname);
           }
       } catch (e) {
           return;
       }
   }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接