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实例中处理。
提供了三个默认实现:SimpleUrlAuthenticationSuccessHandler,SavedRequestAwareAuthenticationSuccessHandler和ForwardAuthenticationSuccessHandler。我们将关注前两个实现。
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上找到。