«1. Обзор

В этой статье основное внимание будет уделено тому, как перенаправить пользователя обратно на первоначально запрошенный URL-адрес — после того, как он войдет в систему.

Ранее мы видели, как перенаправлять на разные страницы после входа в систему с помощью Spring Security. для разных типов пользователей и охватывает различные типы перенаправлений с помощью Spring MVC.

Эта статья основана на учебнике Spring Security Login.

2. Общепринятая практика

Наиболее распространенными способами реализации логики перенаправления после входа в систему являются:

    использование заголовка HTTP Referer сохранение исходного запроса в сеансе добавление исходного URL-адреса к перенаправленному URL-адресу входа

Использование HTTP Заголовок Referer — это простой способ для большинства браузеров и HTTP-клиентов установить Referer автоматически. Однако, поскольку Referer можно подделать и он зависит от реализации клиента, использование заголовка HTTP Referer для реализации перенаправления обычно не рекомендуется.

Сохранение исходного запроса в сеансе — это безопасный и надежный способ реализации такого перенаправления. Помимо исходного URL-адреса, мы можем хранить исходные атрибуты запроса и любые настраиваемые свойства в сеансе.

И добавление исходного URL-адреса к URL-адресу перенаправленного входа обычно наблюдается в реализациях SSO. При аутентификации через службу единого входа пользователи будут перенаправлены на исходно запрошенную страницу с добавленным URL-адресом. Мы должны убедиться, что добавленный URL-адрес правильно закодирован.

Другая аналогичная реализация заключается в том, чтобы поместить исходный URL-адрес запроса в скрытое поле внутри формы входа. Но это не лучше, чем использование HTTP Referer

В Spring Security изначально поддерживаются первые два подхода.

3. AuthenticationSuccessHandler

При аутентификации на основе форм перенаправление происходит сразу после входа в систему, что обрабатывается экземпляром AuthenticationSuccessHandler в Spring Security.

Предусмотрены три реализации по умолчанию: SimpleUrlAuthenticationSuccessHandler, SavedRequestAwareAuthenticationSuccessHandler и ForwardAuthenticationSuccessHandler. Мы сосредоточимся на первых двух реализациях.

3.1. SavedRequestAwareAuthenticationSuccessHandler

SavedRequestAwareAuthenticationSuccessHandler использует сохраненный запрос, сохраненный в сеансе. После успешного входа пользователи будут перенаправлены на URL-адрес, сохраненный в исходном запросе.

Для входа в форму SavedRequestAwareAuthenticationSuccessHandler используется как AuthenticationSuccessHandler по умолчанию.

@Configuration
@EnableWebSecurity
public class RedirectionSecurityConfig extends WebSecurityConfigurerAdapter {

    //...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
          .authorizeRequests()
          .antMatchers("/login*")
          .permitAll()
          .anyRequest()
          .authenticated()
          .and()
          .formLogin();
    }
    
}

И эквивалентный XML будет:

<http>
    <intercept-url pattern="/login" access="permitAll"/>
    <intercept-url pattern="/**" access="isAuthenticated()"/>
    <form-login />
</http>

Предположим, у нас есть защищенный ресурс в местоположении «/secured». При первом доступе к ресурсу нас перенаправит на страницу авторизации; после заполнения учетных данных и публикации формы входа мы будем перенаправлены обратно к нашему первоначально запрошенному местоположению ресурса:

@Test
public void givenAccessSecuredResource_whenAuthenticated_thenRedirectedBack() 
  throws Exception {
 
    MockHttpServletRequestBuilder securedResourceAccess = get("/secured");
    MvcResult unauthenticatedResult = mvc
      .perform(securedResourceAccess)
      .andExpect(status().is3xxRedirection())
      .andReturn();

    MockHttpSession session = (MockHttpSession) unauthenticatedResult
      .getRequest()
      .getSession();
    String loginUrl = unauthenticatedResult
      .getResponse()
      .getRedirectedUrl();
    mvc
      .perform(post(loginUrl)
        .param("username", userDetails.getUsername())
        .param("password", userDetails.getPassword())
        .session(session)
        .with(csrf()))
      .andExpect(status().is3xxRedirection())
      .andExpect(redirectedUrlPattern("**/secured"))
      .andReturn();

    mvc
      .perform(securedResourceAccess.session(session))
      .andExpect(status().isOk());
}

3.2. SimpleUrlAuthenticationSuccessHandler

По сравнению с SavedRequestAwareAuthenticationSuccessHandler, SimpleUrlAuthenticationSuccessHandler дает нам больше возможностей для принятия решений о перенаправлении.

Мы можем включить перенаправление на основе Referer с помощью setUserReferer(true):

public class RefererRedirectionAuthenticationSuccessHandler 
  extends SimpleUrlAuthenticationSuccessHandler
  implements AuthenticationSuccessHandler {

    public RefererRedirectionAuthenticationSuccessHandler() {
        super();
        setUseReferer(true);
    }

}

Затем использовать его как AuthenticationSuccessHandler в RedirectionSecurityConfig:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
      .antMatchers("/login*")
      .permitAll()
      .anyRequest()
      .authenticated()
      .and()
      .formLogin()
      .successHandler(new RefererAuthenticationSuccessHandler());
}

И для конфигурации XML:

<http>
    <intercept-url pattern="/login" access="permitAll"/>
    <intercept-url pattern="/**" access="isAuthenticated()"/>
    <form-login authentication-success-handler-ref="refererHandler" />
</http>

<beans:bean 
  class="RefererRedirectionAuthenticationSuccessHandler" 
  name="refererHandler"/>

3.3. Под капотом

В этих простых в использовании функциях Spring Security нет ничего волшебного. Когда запрашивается защищенный ресурс, запрос будет фильтроваться цепочкой различных фильтров. Принципы аутентификации и разрешения будут проверены. Если сеанс запроса еще не аутентифицирован, будет выдано исключение AuthenticationException.

Исключение AuthenticationException будет перехвачено в ExceptionTranslationFilter, в котором будет запущен процесс аутентификации, что приведет к перенаправлению на страницу входа.

public class ExceptionTranslationFilter extends GenericFilterBean {

    //...

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
        //...

        handleSpringSecurityException(request, response, chain, ase);

        //...
    }

    private void handleSpringSecurityException(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain, RuntimeException exception)
      throws IOException, ServletException {

        if (exception instanceof AuthenticationException) {

            sendStartAuthentication(request, response, chain,
              (AuthenticationException) exception);

        }

        //...
    }

    protected void sendStartAuthentication(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain,
      AuthenticationException reason) throws ServletException, IOException {
       
       SecurityContextHolder.getContext().setAuthentication(null);
       requestCache.saveRequest(request, response);
       authenticationEntryPoint.commence(request, response, reason);
    }

    //... 

}

После входа в систему мы можем настроить поведение в AuthenticationSuccessHandler, как показано выше.

4. Заключение

В этом примере Spring Security мы обсудили распространенную практику перенаправления после входа в систему и объяснили реализации с использованием Spring Security.

«Обратите внимание, что все упомянутые нами реализации уязвимы для определенных атак, если не применяются проверки или дополнительные элементы управления методами. В результате таких атак пользователи могут быть перенаправлены на вредоносный сайт.

OWASP предоставил шпаргалку, которая поможет нам справиться с непроверенными переадресациями и перенаправлениями. Это очень поможет, если нам нужно будет создавать реализации самостоятельно.

Полный код реализации этой статьи можно найти на Github.