1. 概述
本文將重點介紹如何在用户登錄後,將用户重定向回他們最初請求的 URL – 即如何進行 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 得到正確編碼。
另一個類似的實現是在登錄表單內部的隱藏字段中放入原始請求 URL。但與使用HTTP Referer沒有本質區別。
在 Spring Security 中,前兩種方法是原生支持的。
需要注意的是,對於較新的 Spring Boot 版本,默認情況下,Spring Security 能夠登錄後重定向到我們嘗試訪問的受保護資源。如果需要始終重定向到特定的 URL,則可以通過特定的 HttpSecurity 配置強制執行該行為。
3. AuthenticationSuccessHandler
在基於表單的身份驗證中,重定向發生在登錄後立即進行,由一個在 AuthenticationSuccessHandler 實例中處理,位於 Spring Security。
提供了三個默認實現: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();
}
}<p>以下是對應的 XML:</p>
<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. <em>SimpleUrlAuthenticationSuccessHandler</em>
與 <em>SavedRequestAwareAuthenticationSuccessHandler</em> 相比,<em>SimpleUrlAuthenticationSuccessHandler</em> 提供了更多關於重定向決策的選項。
我們可以通過調用 <em>setUserReferer(true)</em> 啓用基於 Referer 的重定向。
public class RefererRedirectionAuthenticationSuccessHandler
extends SimpleUrlAuthenticationSuccessHandler
implements AuthenticationSuccessHandler {
public RefererRedirectionAuthenticationSuccessHandler() {
super();
setUseReferer(true);
}
}然後將其作為 AuthenticationSuccessHandler 在 RedirectionSecurityConfig 中使用:
@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 中被捕獲,從而啓動身份驗證流程,並導致重定向到登錄頁面。
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 解釋了實現方法。請注意,我們提到的所有實現方法如果在未進行驗證或額外方法控制的情況下,都可能存在安全漏洞,導致用户被重定向到惡意網站。
OWASP 提供了 一份指南,以幫助我們處理未驗證的重定向和轉發。 如果我們需要自己構建實現方案,這將非常有幫助。