Как обрабатывать просроченный сеанс с помощью Spring Security и jQuery?

Я использую Spring Security и jQuery в своем приложении. Главная страница использует динамическую загрузку контента во вкладки через AJAX. И все в порядке, однако иногда у меня есть страница входа в мою вкладку, и если я ввожу учетные данные, я буду перенаправлен на страницу содержимого без вкладок.

Так что я хотел бы справиться с этой ситуацией. Я знаю, что некоторые люди используют аутентификацию AJAX, но я не уверен, что это подходит для меня, потому что это выглядит довольно сложно для меня, и мое приложение не разрешает доступ без входа в систему до этого. Я хотел бы просто написать глобальный обработчик для всех ответов AJAX, который будет выполнять window.location.reload(), если нам нужно пройти аутентификацию. Я думаю, что в этом случае лучше получить ошибку 401 вместо стандартной формы входа, потому что с ней проще работать.

So,

1) Можно ли написать глобальный обработчик ошибок для всех запросов jQuery AJAX?

2) Как я могу настроить поведение Spring Security для отправки ошибки 401 для запросов AJAX, но для обычных запросов, чтобы показывать стандартную страницу входа в систему, как обычно?

3) Может у вас есть более изящное решение? Пожалуйста, поделитесь им.

Спасибо.


person viator    schedule 26.07.2010    source источник
comment
Прошло некоторое время с тех пор, как вы спросили об этом. Вы сами придумали хорошее решение?   -  person Glen Little    schedule 06.10.2010
comment
Недавно я написал сообщение в блоге по этому вопросу: to-string.com/2012/08/03/   -  person craftsman    schedule 09.08.2012
comment
Мне нравится решение @craftsman. Я даже упростил (по крайней мере мне так кажется). См. gedrox.blogspot.com/2013/03/blog-post.html.   -  person Gedrox    schedule 08.03.2013


Ответы (7)


Вот подход, который я считаю довольно простым. Это комбинация подходов, которые я наблюдал на этом сайте. Я написал об этом сообщение в блоге: http://yoyar.com/blog/2012/06/dealing-with-the-spring-security-ajax-session-timeout-problem/

Основная идея заключается в использовании префикса URL-адреса API (например, /api/secured), как было предложено выше, вместе с точкой входа для аутентификации. Это просто и работает.

Вот точка входа аутентификации:

package com.yoyar.yaya.config;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.*;
import java.io.IOException;

public class AjaxAwareAuthenticationEntryPoint 
             extends LoginUrlAuthenticationEntryPoint {

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

    @Override
    public void commence(
        HttpServletRequest request, 
        HttpServletResponse response, 
        AuthenticationException authException) 
            throws IOException, ServletException {

        boolean isAjax 
            = request.getRequestURI().startsWith("/api/secured");

        if (isAjax) {
            response.sendError(403, "Forbidden");
        } else {
            super.commence(request, response, authException);
        }
    }
}

И вот что происходит в вашем весеннем контексте xml:

<bean id="authenticationEntryPoint"
  class="com.yoyar.yaya.config.AjaxAwareAuthenticationEntryPoint">
    <constructor-arg name="loginUrl" value="/login"/>
</bean>

<security:http auto-config="true"
  use-expressions="true"
  entry-point-ref="authenticationEntryPoint">
    <security:intercept-url pattern="/api/secured/**" access="hasRole('ROLE_USER')"/>
    <security:intercept-url pattern="/login" access="permitAll"/>
    <security:intercept-url pattern="/logout" access="permitAll"/>
    <security:intercept-url pattern="/denied" access="hasRole('ROLE_USER')"/>
    <security:intercept-url pattern="/" access="permitAll"/>
    <security:form-login login-page="/login"
                         authentication-failure-url="/loginfailed"
                         default-target-url="/login/success"/>
    <security:access-denied-handler error-page="/denied"/>
    <security:logout invalidate-session="true"
                     logout-success-url="/logout/success"
                     logout-url="/logout"/>
</security:http>
person Matt Friedman    schedule 19.06.2012
comment
Я реализовал этот подход, и я поймал тайм-аут, а затем перенаправил на страницу входа в систему, чтобы снова войти в систему. После входа в систему я вижу URL-адрес ajax и строку запроса. Есть ли способ вернуться на страницу, с которой был инициирован запрос ajax? Спасибо - person blong824; 26.06.2012
comment
Это отличный момент. Это особенность пружинной безопасности и ортогональна этому решению. Тем не менее, это проблема. В настоящее время я испытываю ту же проблему и обновлю этот пост, как только разберусь с ней. Пожалуйста, дайте мне знать, если вы разберетесь с этим за это время. - person Matt Friedman; 27.06.2012
comment
Re: комментарий blong824, эта страница поучительна: static.springsource.org/spring-security/site/docs/3.1.x/. Если вы установите для параметра always-use-default-target значение true, вы можете сделать так, чтобы система всегда перенаправляла после входа на нужную страницу. Также ищите решения, связанные с типом компонента: SimpleUrlAuthenticationSuccessHandler. Думаю, более сложные решения лучше описывать в отдельном посте. - person Matt Friedman; 27.06.2012
comment
Снова о: комментарий blong824. Обратите внимание на комментарий Рагурама здесь: string-for-the-login-pag?rq=1" title="make spring security добавить возврат к URL-адресу в строке запроса для страницы входа"> stackoverflow.com/questions/4696905/, который показывает, как настроить SimpleUrlAuthenticationSuccessHandler - person Matt Friedman; 27.06.2012
comment
В настоящее время я пытаюсь сделать следующее: расширить HttpSessionRequestCache и передать строку пропуска, которая начинается с пути для моих запросов ajax. Затем я игнорирую метод saveRequest, и если currentRequest не начинается со строки пропуска, я вызываю super.saveRequest. У меня есть класс, расширяющий SavedRequestAwareAuthenticationSuccessHandler, который проверяет HttpSessionRequestCache. Это еще не работает, но я приближаюсь. Если вы хотите, чтобы я отправил код, мы должны начать новый вопрос. - person blong824; 28.06.2012
comment
Это хорошее решение, особенно в сочетании со стандартным шаблоном проектирования ошибок остатка blog.apigee.com/detail/ restful_api_design_what_about_errors в моем случае мой внешний пользовательский интерфейс просто распечатывает сообщение пользователя и может отображать всплывающее окно с просьбой снова войти в систему и продолжить исходный запрос, чтобы им не нужно было возвращаться на исходную весеннюю страницу входа. . - person ams; 13.08.2012

Я использовал следующее решение.

Весенняя безопасность определила неверный URL-адрес сеанса

<security:session-management invalid-session-url="/invalidate.do"/>

Для этой страницы добавлен следующий контроллер

@Controller
public class InvalidateSession
{
    /**
     * This url gets invoked when spring security invalidates session (ie timeout).
     * Specific content indicates ui layer that session has been invalidated and page should be redirected to logout. 
     */
    @RequestMapping(value = "invalidate.do", method = RequestMethod.GET)
    @ResponseBody
    public String invalidateSession() {
        return "invalidSession";
    }
}

А для ajax используется ajaxSetup для обработки всех запросов ajax:

// Checks, if data indicates that session has been invalidated.
// If session is invalidated, page is redirected to logout
   $.ajaxSetup({
    complete: function(xhr, status) {
                if (xhr.responseText == 'invalidSession') {
                    if ($("#colorbox").count > 0) {
                        $("#colorbox").destroy();
                    }
                    window.location = "logout";
                }
            }
        });
person andro83    schedule 03.09.2012
comment
В моем случае, чтобы это сработало, мне пришлось добавить invalidate-session="false" к <security:logout logout-url="/logout" logout-success-url="/home" />, иначе Spring перенаправил меня на /invalidate.do после нажатия на кнопку выхода. - person matthaeus; 21.10.2013

Взгляните на http://forum.springsource.org/showthread.php?t=95881, я думаю, что предлагаемое решение намного понятнее, чем другие ответы здесь:

  1. Добавьте собственный заголовок в ваши вызовы jquery ajax (используя хук «beforeSend»). Вы также можете использовать заголовок «X-Requested-With», который отправляет jQuery.
  2. Настройте Spring Security для поиска этого заголовка на стороне сервера, чтобы вернуть код ошибки HTTP 401 вместо того, чтобы направлять пользователя на страницу входа.
person Guido    schedule 15.01.2011
comment
А что, если кто-то использует $.getJSON(){...}? beforeSend тогда невозможен. - person zygimantus; 13.01.2016

Я только что придумал решение этой проблемы, но не проверял его полностью. Я также использую Spring, Spring Security и jQuery. Во-первых, с моего контроллера входа в систему я установил код состояния 401:

LoginController {

public ModelAndView loginHandler(HttpServletRequest request, HttpServletResponse response) {

...
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
... 
return new ModelAndView("login", model);
}

В своих методах onload() все мои страницы вызывают функцию в моем глобальном файле javascript:

function initAjaxErrors() {

jQuery(window).ajaxError(function(event, xmlHttpRequest, ajaxOptions, thrownError) {
    if (403 == xmlHttpRequest.status)
        showMessage("Permission Denied");
    else
        showMessage("An error occurred: "+xmlHttpRequest.status+" "+xmlHttpRequest.statusText);
});

}

На этом этапе вы можете обрабатывать ошибку 401 любым удобным для вас способом. В одном проекте я обрабатывал аутентификацию jQuery, размещая диалоговое окно jQuery вокруг iframe, содержащего форму входа.

person Hank    schedule 30.11.2010

Вот как я обычно это делаю. При каждом вызове AJAX проверяйте результат перед его использованием.

$.ajax({ type: 'GET',
    url: GetRootUrl() + '/services/dosomething.ashx',
    success: function (data) {
      if (HasErrors(data)) return;

      // process data returned...

    },
    error: function (xmlHttpRequest, textStatus) {
      ShowStatusFailed(xmlHttpRequest);
    }
  });

И тогда функция HasErrors() выглядит так, и ее можно использовать на всех страницах.

function HasErrors(data) {
  // check for redirect to login page
  if (data.search(/login\.aspx/i) != -1) {
    top.location.href = GetRootUrl() + '/login.aspx?lo=TimedOut';
    return true;
  }
  // check for IIS error page
  if (data.search(/Internal Server Error/) != -1) {
    ShowStatusFailed('Server Error.');
    return true;
  }
  // check for our custom error handling page
  if (data.search(/Error.aspx/) != -1) {
    ShowStatusFailed('An error occurred on the server. The Technical Support Team has been provided with the error details.');
    return true;
  }
  return false;
}
person Glen Little    schedule 06.10.2010

Итак, здесь есть 2 проблемы. 1) Безопасность Spring работает, но ответ возвращается в браузер в вызове ajax. 2) Безопасность Spring отслеживает первоначально запрошенную страницу, чтобы она могла перенаправить вас на нее ПОСЛЕ входа в систему (если вы не укажете, что всегда хотите использовать определенную страницу после входа в систему). В этом случае запрос был строкой Ajax, поэтому вы будете перенаправлены на эту строку, и это то, что вы увидите в браузере.

Простое решение состоит в том, чтобы обнаружить ошибку Ajax, и если запрос, отправленный обратно, относится к вашей странице входа (Spring отправит обратно HTML-код страницы входа, это будет свойство запроса «responseText») обнаружить его. Затем просто перезагрузите текущую страницу, что удалит пользователя из контекста вызова Ajax. Затем Spring автоматически отправит их на страницу входа. (Я использую значение j_username по умолчанию, которое представляет собой строковое значение, уникальное для моей страницы входа).

$(document).ajaxError( function(event, request, settings, exception) {
    if(String.prototype.indexOf.call(request.responseText, "j_username") != -1) {
        window.location.reload(document.URL);
    }
});
person MattC    schedule 04.06.2014

Когда происходит тайм-аут, пользователь перенаправляется на страницу входа после запуска любого действия ajax, в то время как сеанс уже очищен

контекст безопасности:

<http use-expressions="true" entry-point-ref="authenticationEntryPoint">
    <logout invalidate-session="true" success-handler-ref="logoutSuccessBean" delete-cookies="JSESSIONID" />
    <custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />
    <custom-filter position="FORM_LOGIN_FILTER" ref="authFilter" />
    <session-management invalid-session-url="/logout.xhtml" session-authentication-strategy-ref="sas"/>
</http>

<beans:bean id="concurrencyFilter"
  class="org.springframework.security.web.session.ConcurrentSessionFilter">
    <beans:property name="sessionRegistry" ref="sessionRegistry" />
    <beans:property name="expiredUrl" value="/logout.xhtml" />
</beans:bean>

<beans:bean id="authenticationEntryPoint"  class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
    <beans:property name="loginFormUrl" value="/login.xhtml" />
</beans:bean>

<beans:bean id="authFilter"
  class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
    <beans:property name="sessionAuthenticationStrategy" ref="sas" />
    <beans:property name="authenticationManager" ref="authenticationManager" />
    <beans:property name="authenticationSuccessHandler" ref="authenticationSuccessBean" />
    <beans:property name="authenticationFailureHandler" ref="authenticationFailureBean" />
</beans:bean>

<beans:bean id="sas" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy">
    <beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" />
    <beans:property name="maximumSessions" value="1" />
    <beans:property name="exceptionIfMaximumExceeded" value="1" />
</beans:bean>

Прослушиватель входа:

public class LoginListener implements PhaseListener {

@Override
public PhaseId getPhaseId() {
    return PhaseId.RESTORE_VIEW;
}

@Override
public void beforePhase(PhaseEvent event) {
    // do nothing
}

@Override
public void afterPhase(PhaseEvent event) {
    FacesContext context = event.getFacesContext();
    HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
    String logoutURL = request.getContextPath() + "/logout.xhtml";
    String loginURL = request.getContextPath() + "/login.xhtml";

    if (logoutURL.equals(request.getRequestURI())) {
        try {
            context.getExternalContext().redirect(loginURL);
        } catch (IOException e) {
            throw new FacesException(e);
        }
    }
}

}

person Thomas Crook    schedule 22.09.2014