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 配置強制執行該重定向。
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();
}
}
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);
}
}
然後將其用作 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. Under the Hood
這些易於使用的功能在 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 提供了 指南,以幫助我們處理未驗證的重定向和轉發。 如果我們需要自己構建實現,這將非常有幫助。
@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();
}
}<http>
<intercept-url pattern="/login" access="permitAll"/>
<intercept-url pattern="/**" access="isAuthenticated()"/>
<form-login />
</http>@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());
}public class RefererRedirectionAuthenticationSuccessHandler
extends SimpleUrlAuthenticationSuccessHandler
implements AuthenticationSuccessHandler {
public RefererRedirectionAuthenticationSuccessHandler() {
super();
setUseReferer(true);
}
}@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login*")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.successHandler(new RefererAuthenticationSuccessHandler());
return http.build();
}<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"/>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);
}
//...
}