1. 概述

本文将重点介绍如何在用户登录后重定向回原始请求的URL

在此之前,我们已经了解了如何使用Spring Security根据用户类型在登录后重定向到不同的页面,并且涵盖了Spring MVC的各种重定向和转发方式。

这篇文章基于Spring Security登录教程

2. 常见做法

实现登录后重定向逻辑的常见方法有:

  • 使用HTTP Referer
  • 将原始请求保存在会话中
  • 在重定向登录URL后面附加原始URL

使用HTTP Referer是一种直接的方法,大多数浏览器和HTTP客户端会自动设置Referer。然而,由于Referer可被伪造,并依赖于客户端实现,通常不建议使用HTTP Referer头来实现重定向。

将原始请求保存在会话中是安全且稳健的方式来实现这种重定向。除了原始URL,我们还可以在会话中存储原始请求属性和任何自定义属性。

在重定向登录URL后面附加原始URL通常在SSO(单点登录)实现中可以看到。当通过SSO服务验证身份时,用户会被重定向回他们原本请求的页面,其中包含了URL。我们必须确保附加的URL已正确编码。

另一种类似实现是在登录表单中添加一个隐藏的原始请求URL字段。但这并不比使用HTTP Referer更好。

在Spring Security中,前两种方法原生支持。

需要注意的是,对于较新的Spring Boot版本,默认情况下,Spring Security能够在登录后重定向到我们尝试访问的安全资源。如果我们需要始终重定向到特定的URL,可以通过特定的HttpSecurity配置强制执行。

3. AuthenticationSuccessHandler

在基于表单的身份验证中,登录后的重定向立即发生,这在Spring Security中的AuthenticationSuccessHandler实例中处理。

提供了三个默认实现:SimpleUrlAuthenticationSuccessHandlerSavedRequestAwareAuthenticationSuccessHandlerForwardAuthenticationSuccessHandler。我们将关注前两个实现。

3.1. SavedRequestAwareAuthenticationSuccessHandler

SavedRequestAwareAuthenticationSuccessHandler利用会话中保存的原始请求。登录成功后,用户将被重定向到原始请求中的URL。

对于表单登录,SavedRequestAwareAuthenticationSuccessHandler通常是默认的AuthenticationSuccessHandler

@Configuration
@EnableWebSecurity
public class RedirectionSecurityConfig {

    //...

    @Override
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
          .authorizeRequests()
          .antMatchers("/login*")
          .permitAll()
          .anyRequest()
          .authenticated()
          .and()
          .formLogin();
        return http.build();
    }
    
}

对应的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为我们提供了更多关于重定向决策的选择。

我们可以通过setUserReferer(true)启用基于Referer的重定向:

public class RefererRedirectionAuthenticationSuccessHandler 
  extends SimpleUrlAuthenticationSuccessHandler
  implements AuthenticationSuccessHandler {

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

}

然后在RedirectionSecurityConfig中使用它作为AuthenticationSuccessHandler

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
      .antMatchers("/login*")
      .permitAll()
      .anyRequest()
      .authenticated()
      .and()
      .formLogin()
      .successHandler(new RefererAuthenticationSuccessHandler());
    return http.build(); 
}

对于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](https://github.com/spring-projects/spring-security/blob/master/web/src/main/java/org/springframework/security/web/access/ExceptionTranslationFilter.java)中被捕获,然后启动一个身份验证过程,导致重定向到登录页面。

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上找到