Cómo administrar una solicitud de redireccionamiento después de una llamada jQuery Ajax


1370

Estoy usando $.post()para llamar a un servlet usando Ajax y luego usando el fragmento HTML resultante para reemplazar un divelemento en la página actual del usuario. Sin embargo, si la sesión agota el tiempo de espera, el servidor envía una directiva de redireccionamiento para enviar al usuario a la página de inicio de sesión. En este caso, jQuery está reemplazando eldiv elemento con el contenido de la página de inicio de sesión, lo que obliga a los ojos del usuario a presenciar una escena rara.

¿Cómo puedo administrar una directiva de redireccionamiento desde una llamada Ajax con jQuery 1.2.6?


1
(no es una respuesta como tal): he hecho esto en el pasado editando la biblioteca jquery y agregando una marca de verificación para la página de inicio de sesión en cada XHR completo. No es la mejor solución porque tendría que hacerse cada vez que actualiza, pero resuelve el problema.
Sugendran

1
Vea la pregunta relacionada: stackoverflow.com/questions/5941933/…
Nutel

El HttpContext.Response.AddHeadery comprobar en el éxito de ajaxsetup es el camino a seguir
LCJ

44
¿Por qué el servidor no puede devolver 401? En ese caso, puede tener un $ .ajaxSetup global y usar el código de estado para redirigir la página.
Vishal

1
este enlace doanduyhai.wordpress.com/2012/04/21/… me da la solución correcta
pappu_kutty

Respuestas:


697

Leí esta pregunta e implementé el enfoque que se ha establecido con respecto a la configuración del código de estado HTTP de respuesta a 278 para evitar que el navegador maneje de forma transparente las redirecciones. Aunque esto funcionó, estaba un poco insatisfecho ya que es un poco hack.

Después de investigar más, abandoné este enfoque y usé JSON . En este caso, todas las respuestas a las solicitudes AJAX tienen el código de estado 200 y el cuerpo de la respuesta contiene un objeto JSON que se construye en el servidor. El JavaScript en el cliente puede usar el objeto JSON para decidir qué necesita hacer.

Tuve un problema similar al tuyo. Realizo una solicitud AJAX que tiene 2 respuestas posibles: una que redirige el navegador a una nueva página y otra que reemplaza un formulario HTML existente en la página actual por uno nuevo. El código jQuery para hacer esto se parece a:

$.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);
        }
    }
});

El objeto "datos" JSON se construye en el servidor para tener 2 miembros: data.redirecty data.form. Encontré este enfoque mucho mejor.


58
Como se indica en la solución en stackoverflow.com/questions/503093/… , es mejor usar window.location.replace (data.redirect); que window.location.href = data.redirect;
Carles Barrobés

8
Cualquier razón por la cual no sería mejor usar códigos HTTP en la acción. Por ejemplo, un código 307 que es HTTP Redirect temporal?
Sergei Golos

15
@Sergei Golos, la razón es que si realiza una redirección HTTP, la redirección en realidad nunca llega a la devolución de llamada de éxito de ajax. El navegador procesa la redirección entregando un código 200 con el contenido del destino de la redirección.
Miguel Silva

2
¿Cómo se vería el código del servidor? tener un valor de retorno de data.form y data.redirect. Básicamente, ¿cómo puedo determinar si pongo una redirección?
Vnge

66
Esta respuesta sería más útil si hubiera mostrado cómo se hace esto en el servidor.
Pedro Hoehl Carvalho

244

Resolví este problema de la siguiente manera:

  1. Agregar un encabezado personalizado a la respuesta:

    public ActionResult Index(){
        if (!HttpContext.User.Identity.IsAuthenticated)
        {
            HttpContext.Response.AddHeader("REQUIRES_AUTH","1");
        }
        return View();
    }
  2. Vinculando una función de JavaScript al ajaxSuccessevento y verificando si existe el encabezado:

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

66
Qué solución tan asombrosa. Me gusta la idea de una solución única. Necesito verificar un estado 403, pero puedo usar el enlace ajaxSuccess en el cuerpo para esto (que es lo que realmente estaba buscando). Gracias.
Bretticus

44
Acabo de hacer esto y descubrí que necesitaba ajaxComplete donde estaba usando la función $ .get () y cualquier estado que no sea 200 no se activaba. De hecho, probablemente podría haber limitado a ajaxError en su lugar. Vea mi respuesta a continuación para más detalles.
Bretticus

2
Está bien en muchos casos, pero ¿qué pasa si su marco maneja la autorización?
jwaliszko

13
Me gusta el enfoque de encabezado pero también pienso, como @ mwoods79, que el conocimiento de hacia dónde redirigir no debe duplicarse. Lo resolví agregando un encabezado REDIRECT_LOCATION en lugar de un booleano.
rintcius

3
Asegúrese de establecer el encabezado en la respuesta DESPUÉS de la redirección. Como se describe en otras respuestas en esta página, la redirección puede ser transparente para el controlador ajaxSucces. Por lo tanto, incluí el encabezado en la respuesta GET de la página de inicio de sesión (que eventualmente desencadenó ajaxSuccess en mi escenario).
sieppl

118

Ningún navegador maneja las respuestas 301 y 302 correctamente. Y, de hecho, el estándar incluso dice que deberían manejarlos "transparentemente", lo cual es un dolor de cabeza MASIVO para los vendedores de la Biblioteca Ajax. En Ra-Ajax nos vimos obligados a usar el código de estado de respuesta HTTP 278 (solo un código de éxito "no utilizado") para manejar redirecciones transparentes desde el servidor ...

Esto realmente me molesta, y si alguien aquí tiene algo de "atracción" en el W3C, ¡agradecería que pudieras hacerle saber al W3C que realmente necesitamos manejar nosotros mismos los códigos 301 y 302 ...! ;)


3
Por un lado, 278 debería separarse de la especificación HTTP oficial.
Chris Marisic

2
¿Ya no los maneja de manera transparente? Si un recurso se ha movido, manejarlo de manera transparente significa repetir la solicitud en la URL proporcionada. Eso es lo que esperaría usando la API XMLHttpRequest.
Philippe Rathé

@ PhilippeRathé De acuerdo. El manejo transparente es justo lo que quiero. Y no sé por qué se considera malo.
smwikipedia

@smwikipedia Para organizar una redirección del marcado en la sección principal sin redirigir la página.
cmc

si alguien aquí tiene algo de "atracción" en el W3C, agradecería que le hiciera saber al W3C que realmente necesitamos manejar los códigos 301 y 302 nosotros mismos ...! ;)
Tommy.Tang

100

La solución que finalmente se implementó fue utilizar un contenedor para la función de devolución de llamada de la llamada Ajax y, en este contenedor, verificar la existencia de un elemento específico en el fragmento HTML devuelto. Si se encontró el elemento, el reiniciador ejecutó una redirección. De lo contrario, el reiniciador reenvió la llamada a la función de devolución de llamada real.

Por ejemplo, nuestra función de contenedor era algo así como:

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

Luego, al hacer la llamada Ajax, usamos algo como:

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

Esto funcionó para nosotros porque todas las llamadas de Ajax siempre devolvieron HTML dentro de un elemento DIV que usamos para reemplazar una parte de la página. Además, solo necesitábamos redirigir a la página de inicio de sesión.


44
Tenga en cuenta que esto se puede acortar a la función cbWrapper (funct) {return function (data) {if ($ ("# myForm", data) .size ()> 0) top.location.href = "login"; más funct (datos); }}. Entonces solo necesita cbWrapper (myActualCB) cuando llame a .post. Sí, el código en los comentarios es un desastre, pero debe tenerse en cuenta :)
Simen Echholt

el tamaño se deprecia para que pueda usar .length aquí en lugar del tamaño
sunil

66

Me gusta el método de Timmerz con un ligero toque de limón. Si alguna vez te devuelven contentType de text / html cuando esperas JSON , lo más probable es que estés siendo redirigido. En mi caso, simplemente vuelvo a cargar la página y se redirige a la página de inicio de sesión. Ah, y compruebe que el estado de jqXHR es 200, lo que parece una tontería, porque está en la función de error, ¿verdad? De lo contrario, los casos de error legítimos forzarán una recarga iterativa (oops)

$.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
muchas gracias Brian, tu respuesta fue la mejor para mi situación, aunque me gustaría si hubiera una verificación más segura, como comparar a qué url / página se redirige en lugar de la simple verificación de "tipo de contenido". No pude encontrar a qué página se redirige desde el objeto jqXHR.
Johnny

Verifiqué el estado 401 y luego redirigí. Funciona como un campeón.
Eric

55

Use la $.ajax()llamada de bajo nivel :

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

Pruebe esto para una redirección:

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

Estaba tratando de evitar las cosas de bajo nivel. De todos modos, supongamos que uso algo como lo que usted describe, ¿cómo forzo una redirección del navegador una vez que detecto que el código HTTP es 3xx? Mi objetivo es redirigir al usuario, no solo anunciar que su sesión ha expirado.
Elliot Vargas

44
Por cierto, $ .ajax () no es de muy, muy bajo nivel. Es de bajo nivel en términos de jQuery porque hay $ .get, $ .post, etc., que son mucho más simples que $ .ajax y todas sus opciones.
Hasta el

16
¡Oh chico! Lo siento, tuve que "no aceptar" su respuesta, todavía es muy útil. La cuestión es que las redirecciones son administradas automáticamente por XMLHttpRequest, por lo tanto, SIEMPRE obtengo un código de estado 200 después de la redirección (¡suspiro!). Creo que tendré que hacer algo desagradable como analizar el HTML y buscar un marcador.
Elliot Vargas

1
Solo por curiosidad, si la sesión termina en el servidor, ¿eso no significa que el servidor envía un SESSIONID diferente? ¿No podríamos detectar eso?
Salamander2007

10
NOTA: Esto no funciona para redireccionamientos. ajax irá a la nueva página y devolverá su código de estado.

33

Solo quería compartir mi enfoque ya que esto podría ayudar a alguien:

Básicamente incluí un módulo de JavaScript que maneja las cosas de autenticación como mostrar el nombre de usuario y también este caso manejando la redirección a la página de inicio de sesión .

Mi escenario: básicamente tenemos un servidor ISA en el medio que escucha todas las solicitudes y responde con un 302 y un encabezado de ubicación a nuestra página de inicio de sesión.

En mi módulo JavaScript mi enfoque inicial fue algo así como

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

El problema (como ya se mencionó aquí) es que el navegador maneja la redirección por sí mismo, por lo ajaxCompleteque nunca se llamó a mi devolución de llamada, sino que recibí la respuesta de la página de inicio de sesión ya redirigida, que obviamente era unstatus 200 . El problema: ¿cómo detecta si la respuesta 200 exitosa es su página de inicio de sesión real o simplemente alguna otra página arbitraria?

La solución

Como no pude capturar las respuestas de redireccionamiento 302, agregué un LoginPageencabezado en mi página de inicio de sesión que contenía la URL de la página de inicio de sesión. En el módulo ahora escucho el encabezado y hago una redirección:

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

... y eso funciona como encanto :). Quizás se pregunte por qué incluyo la url en el LoginPageencabezado ... bueno, básicamente porque no encontré ninguna forma de determinar la url GETresultante de la redirección de ubicación automática desde el xhrobjeto ...


+1 - pero se supone que los encabezados personalizados comienzan X-, por lo que sería mejor usar un encabezado X-LoginPage: http://example.com/login.
uınbɐɥs

66
@ShaquinTrifonoff Ya no más. No utilicé el prefijo X porque en junio de 2011 un documento de ITEF propuso su desaprobación y, de hecho, en junio de 2012 no es oficial que los encabezados personalizados ya no tengan el prefijo X-.
Juri

También tenemos un servidor ISA y me encontré con el mismo problema. En lugar de solucionarlo en código, usamos las instrucciones en kb2596444 para configurar ISA para que detenga la redirección.
Scott Stone

29

Sé que este tema es antiguo, pero daré otro enfoque que he encontrado y descrito anteriormente aquí . Básicamente, estoy usando ASP.MVC con WIF (pero esto no es realmente importante para el contexto de este tema; la respuesta es adecuada sin importar qué marcos se usen. La pista permanece sin cambios: lidia con problemas relacionados con fallas de autenticación al realizar solicitudes ajax ) .

El enfoque que se muestra a continuación se puede aplicar a todas las solicitudes de ajax listas para usar (si obviamente no se redefinen antes del evento Enviar).

$.ajaxSetup({
    beforeSend: checkPulse,
    error: function (XMLHttpRequest, textStatus, errorThrown) {
        document.open();
        document.write(XMLHttpRequest.responseText);
        document.close();
    }
});

Antes de realizar cualquier solicitud ajax CheckPulse, se invoca el método (el método del controlador, que puede ser cualquier cosa más simple):

[Authorize]
public virtual void CheckPulse() {}

Si el usuario no está autenticado (el token ha caducado), no se puede acceder a dicho método (protegido por Authorizeatributo). Debido a que el marco maneja la autenticación, mientras que el token expira, pone el estado http 302 a la respuesta. Si no desea que su navegador maneje la respuesta 302 de manera transparente, póngala en Global.asax y cambie el estado de la respuesta, por ejemplo, a 200 OK. Además, agregue el encabezado, que le indica que procese dicha respuesta de manera especial (más adelante en el lado del cliente):

protected void Application_EndRequest()
{
    if (Context.Response.StatusCode == 302
        && (new HttpContextWrapper(Context)).Request.IsAjaxRequest())
    {                
        Context.Response.StatusCode = 200;
        Context.Response.AddHeader("REQUIRES_AUTH", "1");
    }
}

Finalmente, en el lado del cliente, verifique ese encabezado personalizado. Si está presente, se debe realizar una redirección completa a la página de inicio de sesión (en mi caso, window.locationse reemplaza por la url de la solicitud, que mi marco maneja automáticamente).

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;
                }
            }
    });
}

Solucioné el problema usando el evento PostAuthenticateRequest en lugar del evento EndRequest.
Frinavale

@JaroslawWaliszko ¡Pegué el evento incorrecto en mi última respuesta! Me refería al evento PreSendRequestHeaders ..... ¡no a PostAuthenticateRequest! >> rubor << gracias por señalar mi error.
Frinavale

@JaroslawWaliszko cuando usa WIF también puede devolver 401 respuestas para solicitudes AJAX y dejar que su javascript las maneje. También está asumiendo que todos los 302 requieren autenticación, lo que podría no ser cierto en todos los casos. He agregado una respuesta si alguien está interesado.
Rob

26

Creo que una mejor manera de manejar esto es aprovechar los códigos de respuesta del protocolo HTTP existentes, específicamente 401 Unauthorized.

Así es como lo resolví:

  1. Lado del servidor: si la sesión caduca y la solicitud es ajax. enviar un encabezado de código de respuesta 401
  2. Lado del cliente: enlace a los eventos 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';
    }
    });

OMI, esto es más genérico y no está escribiendo algunas nuevas especificaciones / encabezados personalizados. Tampoco debería tener que modificar ninguna de sus llamadas ajax existentes.

Editar: según el comentario de @ Rob a continuación, 401 (el código de estado HTTP para errores de autenticación) debería ser el indicador. Consulte 403 Prohibido vs 401 Respuestas HTTP no autorizadas para más detalles. Dicho esto, algunos marcos web usan 403 tanto para errores de autenticación como de autorización, por lo que debe adaptarse en consecuencia. Gracias Rob


Yo uso el mismo enfoque. ¿Realmente jQuery llama a ajaxSuccess en el código de error 403? Creo que solo se necesita una parte de ajaxError
Marius Balčytis

24

Resolví este problema así:

Agregue un middleware para procesar la respuesta, si es una redirección para una solicitud ajax, cambie la respuesta a una respuesta normal con la URL de redireccionamiento.

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

Luego, en ajaxComplete, si la respuesta contiene redirección, debe ser una redirección, por lo tanto, cambie la ubicación del navegador.

$('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;
       }
   }
}

20

Tengo una solución simple que funciona para mí, no es necesario cambiar el código del servidor ... solo agregue una cucharadita de nuez moscada ...

$(document).ready(function ()
{
    $(document).ajaxSend(
    function(event,request,settings)
    {
        var intercepted_success = settings.success;
        settings.success = function( a, b, c ) 
        {  
            if( request.responseText.indexOf( "<html>" ) > -1 )
                window.location = window.location;
            else
                intercepted_success( a, b, c );
        };
    });
});

Verifico la presencia de la etiqueta html, pero puede cambiar el indexOf para buscar cualquier cadena única que exista en su página de inicio de sesión ...


Esto no parece funcionar para mí, sigue llamando a la función definida con una llamada ajax, es como si no anulara el método de éxito.
adriaanp

No parece funcionar para mí, al menos más. Posible explicación aquí: stackoverflow.com/a/12010724/260665
Raj Pawan Gumdal

20

Otra solución que encontré (especialmente útil si desea establecer un comportamiento global) es utilizar el $.ajaxsetup()método junto con la statusCodepropiedad . Como otros señalaron, no use un código de estado de redireccionamiento ( 3xx), en su lugar use un 4xxcódigo de estado y maneje el lado del cliente de redireccionamiento.

$.ajaxSetup({ 
  statusCode : {
    400 : function () {
      window.location = "/";
    }
  }
});

Reemplace 400con el código de estado que desea manejar. Como ya se mencionó 401 Unauthorizedpodría ser una buena idea. Lo uso 400ya que es muy inespecífico y puedo usarlo 401para casos más específicos (como credenciales de inicio de sesión incorrectas). Entonces, en lugar de redirigir directamente, su backend debería devolver un 4xxcódigo de error cuando la sesión expiró y usted maneja la redirección del lado del cliente. Funciona perfecto para mí incluso con frameworks como backbone.js


¿Dónde mencionar la función en la página?
Vikrant

@Vikrant Si entiendo su pregunta correctamente, puede llamar a la función justo después de que se cargue jQuery y antes de hacer sus solicitudes reales.
morten.c

19

La mayoría de las soluciones dadas usan una solución alternativa, usando un encabezado adicional o un código HTTP inapropiado. Es probable que esas soluciones funcionen, pero se sienten un poco 'hacky'. Se me ocurrió otra solución.

Estamos utilizando WIF que está configurado para redirigir (passiveRedirectEnabled = "true") en una respuesta 401. La redirección es útil cuando se manejan solicitudes normales, pero no funcionará para solicitudes AJAX (ya que los navegadores no ejecutarán el 302 / redirect).

Usando el siguiente código en su global.asax puede deshabilitar la redirección para solicitudes AJAX:

    void WSFederationAuthenticationModule_AuthorizationFailed(object sender, AuthorizationFailedEventArgs e)
    {
        string requestedWithHeader = HttpContext.Current.Request.Headers["X-Requested-With"];

        if (!string.IsNullOrEmpty(requestedWithHeader) && requestedWithHeader.Equals("XMLHttpRequest", StringComparison.OrdinalIgnoreCase))
        {
            e.RedirectToIdentityProvider = false;
        }
    }

Esto le permite devolver 401 respuestas para solicitudes AJAX, que su javascript puede manejar volviendo a cargar la página. Recargar la página arrojará un 401 que será manejado por WIF (y WIF redirigirá al usuario a la página de inicio de sesión).

Un ejemplo de JavaScript para manejar errores 401:

$(document).ajaxError(function (event, jqxhr, settings, exception) {

    if (jqxhr.status == 401) { //Forbidden, go to login
        //Use a reload, WIF will redirect to Login
        location.reload(true);
    }
});

Buena solución. Gracias.
DanMan

19

Este problema puede aparecer usando el método ASP.NET MVC RedirectToAction. Para evitar que el formulario muestre la respuesta en div, simplemente puede hacer algún tipo de filtro de respuesta ajax para las respuestas entrantes con $ .ajaxSetup . Si la respuesta contiene redirección MVC, puede evaluar esta expresión en el lado JS. Código de ejemplo para JS a continuación:

$.ajaxSetup({
    dataFilter: function (data, type) {
        if (data && typeof data == "string") {
            if (data.indexOf('window.location') > -1) {
                eval(data);
            }
        }
        return data;
    }
});

Si los datos son: "window.location = '/ Acount / Login'", el filtro anterior capturará eso y evaluará la redirección en lugar de permitir que se muestren los datos.


dataestá en el cuerpo de respuesta o encabezado?
GMsoF

16

Reuniendo lo que dijeron Vladimir Prudnikov y Thomas Hansen:

  • Cambie su código del lado del servidor para detectar si es un XHR. Si es así, establezca el código de respuesta de la redirección en 278. En django:
   if request.is_ajax():
      response.status_code = 278

Esto hace que el navegador trate la respuesta como un éxito y la entregue a su Javascript.

  • En su JS, asegúrese de que el envío del formulario sea a través de Ajax, verifique el código de respuesta y redirija si es necesario:
$('#my-form').submit(function(event){ 

  event.preventDefault();   
  var options = {
    url: $(this).attr('action'),
    type: 'POST',
    complete: function(response, textStatus) {    
      if (response.status == 278) { 
        window.location = response.getResponseHeader('Location')
      }
      else { ... your code here ... } 
    },
    data: $(this).serialize(),   
  };   
  $.ajax(options); 
});

13
    <script>
    function showValues() {
        var str = $("form").serialize();
        $.post('loginUser.html', 
        str,
        function(responseText, responseStatus, responseXML){
            if(responseStatus=="success"){
                window.location= "adminIndex.html";
            }
        });     
    }
</script>

12

Tratar

    $(document).ready(function () {
        if ($("#site").length > 0) {
            window.location = "<%= Url.Content("~") %>" + "Login/LogOn";
        }
    });

Ponlo en la página de inicio de sesión. Si se cargó en un div en la página principal, se redirigirá hasta la página de inicio de sesión. "#site" es una identificación de un div que se encuentra en todas las páginas, excepto en la página de inicio de sesión.


12

Si bien las respuestas parecen funcionar para las personas si está usando Spring Security, he encontrado que extender LoginUrlAuthenticationEntryPoint y agregar código específico para manejar AJAX es más robusto. La mayoría de los ejemplos interceptan todos los redireccionamientos, no solo las fallas de autenticación. Esto no fue deseable para el proyecto en el que trabajo. Es posible que también necesite ampliar ExceptionTranslationFilter y anular el método "sendStartAuthentication" para eliminar el paso de almacenamiento en caché si no desea que la solicitud AJAX fallida se almacene en caché.

Ejemplo AjaxAwareAuthenticationEntryPoint:

public class AjaxAwareAuthenticationEntryPoint extends
    LoginUrlAuthenticationEntryPoint {

    public AjaxAwareAuthenticationEntryPoint(String loginUrl) {
        super(loginUrl);
    }

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        if (isAjax(request)) {
            response.sendError(HttpStatus.UNAUTHORIZED.value(), "Please re-authenticate yourself");
        } else {
        super.commence(request, response, authException);
        }
    }

    public static boolean isAjax(HttpServletRequest request) {
        return request != null && "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
    }
}

Fuentes: 1 , 2


3
Sería útil (para mí) si los votantes con baja explicación explicaran por qué están con baja en la votación. Si hay algo malo con esta solución, me gustaría aprender de mis errores. Gracias.
John

Si usa Spring y también JSF, compruebe también esto: ("parcial / ajax"). EqualsIgnoreCase (request.getHeader ("faces-request"));
J Slick

Los usuarios podrían haber rechazado porque no mencionó: (1) las modificaciones necesarias del lado del cliente para detectar su respuesta de error; (2) las modificaciones necesarias a la configuración de Spring para agregar su LoginUrlAuthenticationEntryPoint personalizado a la cadena de filtros.
J Slick

Su respuesta es similar a esta de @Arpad. A mí me funcionó; utilizando Spring Security 3.2.9. stackoverflow.com/a/8426947/4505142
Darren Parker

11

Resolví esto poniendo lo siguiente en mi página login.php.

<script type="text/javascript">
    if (top.location.href.indexOf('login.php') == -1) {
        top.location.href = '/login.php';
    }
</script>

10

Permítanme citar nuevamente el problema como lo describe @Steg

Tuve un problema similar al tuyo. Realizo una solicitud ajax que tiene 2 respuestas posibles: una que redirige el navegador a una nueva página y otra que reemplaza un formulario HTML existente en la página actual por uno nuevo.

En mi humilde opinión, este es un verdadero desafío y tendrá que extenderse oficialmente a los estándares HTTP actuales.

Creo que el nuevo estándar Http será utilizar un nuevo código de estado. significado: actualmente 301/302le dice al navegador que vaya y busque el contenido de esta solicitud a uno nuevo location.

En el estándar extendido, dirá que si la respuesta status: 308(solo un ejemplo), el navegador debe redirigir la página principal alocation proporcionada.

Habiendo dicho eso; Me inclino a imitar este comportamiento futuro y, por lo tanto, cuando se necesita un document.redirect, hago que el servidor responda como:

status: 204 No Content
x-status: 308 Document Redirect
x-location: /login.html

Cuando JS obtiene el " status: 204", comprueba la existencia del x-status: 308encabezado y realiza un document.redirect a la página proporcionada en el locationencabezado.

¿Esto tiene algún sentido para usted?


7

Algunos pueden encontrar útil lo siguiente:

Quería que los clientes fueran redirigidos a la página de inicio de sesión para cualquier acción de descanso que se envíe sin un token de autorización. Dado que todas mis acciones de descanso están basadas en Ajax, necesitaba una buena forma genérica para redirigir a la página de inicio de sesión en lugar de manejar la función de éxito de Ajax.

Esto es lo que he hecho:

En cualquier solicitud de Ajax, mi servidor devolverá una respuesta Json 200 "NECESITA AUTENTICARSE" (si el cliente necesita autenticarse).

Ejemplo simple en Java (lado del servidor):

@Secured
@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {

    private final Logger m_logger = LoggerFactory.getLogger(AuthenticationFilter.class);

    public static final String COOKIE_NAME = "token_cookie"; 

    @Override
    public void filter(ContainerRequestContext context) throws IOException {        
        // Check if it has a cookie.
        try {
            Map<String, Cookie> cookies = context.getCookies();

            if (!cookies.containsKey(COOKIE_NAME)) {
                m_logger.debug("No cookie set - redirect to login page");
                throw new AuthenticationException();
            }
        }
        catch (AuthenticationException e) {
            context.abortWith(Response.ok("\"NEED TO AUTHENTICATE\"").type("json/application").build());
        }
    }
}

En mi Javascript he agregado el siguiente código:

$.ajaxPrefilter(function(options, originalOptions, jqXHR) {
    var originalSuccess = options.success;

    options.success = function(data) {
        if (data == "NEED TO AUTHENTICATE") {
            window.location.replace("/login.html");
        }
        else {
            originalSuccess(data);
        }
    };      
});

Y eso es todo.


5

en el servlet deberías poner response.setStatus(response.SC_MOVED_PERMANENTLY); para enviar el estado '301' xmlHttp que necesita para una redirección ...

y en la función $ .ajax no deberías usar la .toString()función ..., solo

if (xmlHttp.status == 301) { top.location.href = 'xxxx.jsp'; }

el problema es que no es muy flexible, no puedes decidir hacia dónde quieres redirigir ...

redireccionar a través de los servlets debería ser la mejor manera. pero todavía no puedo encontrar la manera correcta de hacerlo.


5

Solo quería conectarme a cualquier solicitud de ajax para toda la página. @SuperG me ayudó a comenzar. Esto es lo que terminé con:

// redirect ajax requests that are redirected, not found (404), or forbidden (403.)
$('body').bind('ajaxComplete', function(event,request,settings){
        switch(request.status) {
            case 301: case 404: case 403:                    
                window.location.replace("http://mysite.tld/login");
                break;
        }
});

Quería verificar específicamente ciertos códigos de estado http para basar mi decisión. Sin embargo, puedes unirte a ajaxError para obtener algo más que éxito (¿200 solo quizás?) Podría haber escrito:

$('body').bind('ajaxError', function(event,request,settings){
    window.location.replace("http://mysite.tld/login");
}

1
este último podría ocultar cualquier otros errores que hacen problemática la solución de problemas
Tim Abell

1
Un 403 no significa que el usuario no esté autenticado, significa que el usuario (probablemente autenticado) no tiene permiso para ver el recurso solicitado. Por lo tanto, no debe redirigirse a la página de inicio de sesión
Rob

5

Si también desea pasar los valores, también puede establecer las variables de sesión y acceder, por ejemplo: en su jsp puede escribir

<% HttpSession ses = request.getSession(true);
   String temp=request.getAttribute("what_you_defined"); %>

Y luego puede almacenar este valor temporal en su variable de JavaScript y jugar


5

No tuve éxito con la solución de encabezado: nunca se recogieron en mi método ajaxSuccess / ajaxComplete. Usé la respuesta de Steg con la respuesta personalizada, pero modifiqué un poco el lado JS. Configuré un método al que llamo en cada función para poder usar el estándar $.gety los $.postmétodos.

function handleAjaxResponse(data, callback) {
    //Try to convert and parse object
    try {
        if (jQuery.type(data) === "string") {
            data = jQuery.parseJSON(data);
        }
        if (data.error) {
            if (data.error == 'login') {
                window.location.reload();
                return;
            }
            else if (data.error.length > 0) {
                alert(data.error);
                return;
            }
        }
    }
    catch(ex) { }

    if (callback) {
        callback(data);
    }
}

Ejemplo de uso ...

function submitAjaxForm(form, url, action) {
    //Lock form
    form.find('.ajax-submit').hide();
    form.find('.loader').show();

    $.post(url, form.serialize(), function (d) {
        //Unlock form
        form.find('.ajax-submit').show();
        form.find('.loader').hide();

        handleAjaxResponse(d, function (data) {
            // ... more code for if auth passes ...
        });
    });
    return false;
}

5

Finalmente, resuelvo el problema agregando una costumbre HTTP Header. Justo antes de la respuesta para cada solicitud en el lado del servidor, agrego la URL solicitada actual al encabezado de la respuesta.

Mi tipo de aplicación en el servidor es Asp.Net MVC, y tiene un buen lugar para hacerlo. en Global.asaximplementado el Application_EndRequestevento así:

    public class MvcApplication : System.Web.HttpApplication
    {

    //  ...
    //  ...

        protected void Application_EndRequest(object sender, EventArgs e)
        {
            var app = (HttpApplication)sender;
            app.Context.Response.Headers.Add("CurrentUrl",app.Context. Request.CurrentExecutionFilePath);
        }

    }

¡Funciona perfecto para mí! Ahora, en cada respuesta de JQuery $.posti, tengo los urlencabezados de respuesta solicitados y también otros que vienen como resultado del POSTmétodo por estado 302,303 , ....

y otra cosa importante es que no hay necesidad de modificar el código del lado del servidor ni del lado del cliente.

y el siguiente es la capacidad de obtener acceso a la otra información de la acción posterior, tales errores, mensajes y ..., de esta manera.

Publiqué esto, tal vez ayudar a alguien :)


4

Estaba teniendo este problema en una aplicación de django con la que estoy jugando (descargo de responsabilidad: estoy jugando para aprender, y de ninguna manera soy un experto). Lo que quería hacer era usar jQuery ajax para enviar una solicitud DELETE a un recurso, eliminarla en el lado del servidor y luego enviar una redirección a (básicamente) la página de inicio. Cuando envié HttpResponseRedirect('/the-redirect/')desde el script de Python, el método ajax de jQuery estaba recibiendo 200 en lugar de 302. Entonces, lo que hice fue enviar una respuesta de 300 con:

response = HttpResponse(status='300')
response['Location'] = '/the-redirect/' 
return  response

Luego envié / manejé la solicitud en el cliente con jQuery.ajax así:

<button onclick="*the-jquery*">Delete</button>

where *the-jquery* =
$.ajax({ 
  type: 'DELETE', 
  url: '/resource-url/', 
  complete: function(jqxhr){ 
    window.location = jqxhr.getResponseHeader('Location'); 
  } 
});

Quizás usar 300 no sea "correcto", pero al menos funcionó tal como yo quería.

PD: esto fue un gran dolor para editar en la versión móvil de SO. ¡El estúpido ISP envió mi solicitud de cancelación de servicio justo cuando terminé con mi respuesta!


4

También puede conectar el prototipo de envío XMLHttpRequest. Esto funcionará para todos los envíos (jQuery / dojo / etc) con un controlador.

Escribí este código para manejar un error de 500 páginas caducadas, pero debería funcionar igual de bien para atrapar una redirección de 200. Prepare la entrada de wikipedia en XMLHttpRequest onreadystatechange sobre el significado de readyState.

// Hook XMLHttpRequest
var oldXMLHttpRequestSend = XMLHttpRequest.prototype.send;

XMLHttpRequest.prototype.send = function() {
  //console.dir( this );

  this.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 500 && this.responseText.indexOf("Expired") != -1) {
      try {
        document.documentElement.innerHTML = this.responseText;
      } catch(error) {
        // IE makes document.documentElement read only
        document.body.innerHTML = this.responseText;
      }
    }
  };

  oldXMLHttpRequestSend.apply(this, arguments);
}

2

Además, es probable que desee redirigir al usuario a la URL dada en los encabezados. Así que finalmente se verá así:

$.ajax({
    //.... other definition
    complete:function(xmlHttp){
        if(xmlHttp.status.toString()[0]=='3'){
        top.location.href = xmlHttp.getResponseHeader('Location');
    }
});

UPD: Opps. Tiene la misma tarea, pero no funciona. Haciendo esto Te mostraré la solución cuando la encuentre.


44
El autor debe eliminar esta respuesta, ya que él mismo dice que esto no funciona. Puede publicar una solución de trabajo más tarde. -1
TheCrazyProgrammer

La idea es buena, pero el problema es que el encabezado "Ubicación" nunca se pasa.
Martin Zvarík

Mi edición fue rechazada, así que ... debe usar "var xmlHttp = $ .ajax ({...." la variable no puede estar dentro ... luego use: console.log (xmlHttp.getAllResponseHeaders ()) ;
Martin Zvarík

2

Obtuve una solución funcional usando las respuestas de @John y @Arpad link y @RobWinch link

Yo uso Spring Security 3.2.9 y jQuery 1.10.2.

Extienda la clase de Spring para causar una respuesta 4XX solo de solicitudes AJAX:

public class CustomLoginUrlAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {

    public CustomLoginUrlAuthenticationEntryPoint(final String loginFormUrl) {
        super(loginFormUrl);
    }

    // For AJAX requests for user that isn't logged in, need to return 403 status.
    // For normal requests, Spring does a (302) redirect to login.jsp which the browser handles normally.
    @Override
    public void commence(final HttpServletRequest request,
                         final HttpServletResponse response,
                         final AuthenticationException authException)
            throws IOException, ServletException {
        if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
        } else {
            super.commence(request, response, authException);
        }
    }
}

applicationContext-security.xml

  <security:http auto-config="false" use-expressions="true" entry-point-ref="customAuthEntryPoint" >
    <security:form-login login-page='/login.jsp' default-target-url='/index.jsp'                             
                         authentication-failure-url="/login.jsp?error=true"
                         />    
    <security:access-denied-handler error-page="/errorPage.jsp"/> 
    <security:logout logout-success-url="/login.jsp?logout" />
...
    <bean id="customAuthEntryPoint" class="com.myapp.utils.CustomLoginUrlAuthenticationEntryPoint" scope="singleton">
        <constructor-arg value="/login.jsp" />
    </bean>
...
<bean id="requestCache" class="org.springframework.security.web.savedrequest.HttpSessionRequestCache">
    <property name="requestMatcher">
      <bean class="org.springframework.security.web.util.matcher.NegatedRequestMatcher">
        <constructor-arg>
          <bean class="org.springframework.security.web.util.matcher.MediaTypeRequestMatcher">
            <constructor-arg>
              <bean class="org.springframework.web.accept.HeaderContentNegotiationStrategy"/>
            </constructor-arg>
            <constructor-arg value="#{T(org.springframework.http.MediaType).APPLICATION_JSON}"/>
            <property name="useEquals" value="true"/>
          </bean>
        </constructor-arg>
      </bean>
    </property>
</bean>

En mis JSP, agregue un controlador de errores AJAX global como se muestra aquí

  $( document ).ajaxError(function( event, jqxhr, settings, thrownError ) {
      if ( jqxhr.status === 403 ) {
          window.location = "login.jsp";
      } else {
          if(thrownError != null) {
              alert(thrownError);
          } else {
              alert("error");
          }
      }
  });

Además, elimine los controladores de errores existentes de las llamadas AJAX en las páginas JSP:

        var str = $("#viewForm").serialize();
        $.ajax({
            url: "get_mongoDB_doc_versions.do",
            type: "post",
            data: str,
            cache: false,
            async: false,
            dataType: "json",
            success: function(data) { ... },
//            error: function (jqXHR, textStatus, errorStr) {
//                 if(textStatus != null)
//                     alert(textStatus);
//                 else if(errorStr != null)
//                     alert(errorStr);
//                 else
//                     alert("error");
//            }
        });

Espero que ayude a los demás.

Actualización1 Descubrí que necesitaba agregar la opción (always-use-default-target = "true") a la configuración de inicio de sesión de formulario. Esto era necesario ya que después de que una solicitud AJAX se redirige a la página de inicio de sesión (debido a una sesión expirada), Spring recuerda la solicitud AJAX anterior y se redirige automáticamente después de iniciar sesión. Esto hace que el JSON devuelto se muestre en la página del navegador. Por supuesto, no es lo que quiero.

Actualización2 En lugar de usar always-use-default-target="true", use el ejemplo @RobWinch de bloquear solicitudes AJAX de requstCache. Esto permite que los enlaces normales se redirijan a su destino original después de iniciar sesión, pero AJAX va a la página de inicio después de iniciar sesión.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.