深入淺析 Spring Security 緩存請求問題
為什么要緩存?
為了更好的描述問題,我們拿使用表單認證的網(wǎng)站舉例,簡化后的認證過程分為7步:
- 用戶訪問網(wǎng)站,打開了一個鏈接(origin url)。
- 請求發(fā)送給服務器,服務器判斷用戶請求了受保護的資源。
- 由于用戶沒有登錄,服務器重定向到登錄頁面
- 填寫表單,點擊登錄
- 瀏覽器將用戶名密碼以表單形式發(fā)送給服務器
- 服務器驗證用戶名密碼。成功,進入到下一步。否則要求用戶重新認證(第三步)
- 服務器對用戶擁有的權限(角色)判定: 有權限,重定向到origin url; 權限不足,返回狀態(tài)碼403("forbidden").
從第3步,我們可以知道,用戶的請求被中斷了。
用戶登錄成功后(第7步),會被重定向到origin url,spring security通過使用緩存的request,使得被中斷的請求能夠繼續(xù)執(zhí)行。
使用緩存
用戶登錄成功后,頁面重定向到origin url。瀏覽器發(fā)出的請求優(yōu)先被攔截器RequestCacheAwareFilter攔截,RequestCacheAwareFilter通過其持有的RequestCache對象實現(xiàn)request的恢復。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // request匹配,則取出,該操作同時會將緩存的request從session中刪除 HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest( (HttpServletRequest) request, (HttpServletResponse) response); // 優(yōu)先使用緩存的request chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest, response); }
何時緩存
首先,我們需要了解下RequestCache以及ExceptionTranslationFilter。
RequestCache
RequestCache接口聲明了緩存與恢復操作。默認實現(xiàn)類是HttpSessionRequestCache。HttpSessionRequestCache的實現(xiàn)比較簡單,這里只列出接口的聲明:
public interface RequestCache { // 將request緩存到session中 void saveRequest(HttpServletRequest request, HttpServletResponse response); // 從session中取request SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response); // 獲得與當前request匹配的緩存,并將匹配的request從session中刪除 HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response); // 刪除緩存的request void removeRequest(HttpServletRequest request, HttpServletResponse response); }
ExceptionTranslationFilter
ExceptionTranslationFilter 是Spring Security的核心filter之一,用來處理AuthenticationException和AccessDeniedException兩種異常。
在我們的例子中,AuthenticationException指的是未登錄狀態(tài)下訪問受保護資源,AccessDeniedException指的是登陸了但是由于權限不足(比如普通用戶訪問管理員界面)。
ExceptionTranslationFilter 持有兩個處理類,分別是AuthenticationEntryPoint和AccessDeniedHandler。
ExceptionTranslationFilter 對異常的處理是通過這兩個處理類實現(xiàn)的,處理規(guī)則很簡單:
- 規(guī)則1. 如果異常是 AuthenticationException,使用 AuthenticationEntryPoint 處理
- 規(guī)則2. 如果異常是 AccessDeniedException 且用戶是匿名用戶,使用 AuthenticationEntryPoint 處理
- 規(guī)則3. 如果異常是 AccessDeniedException 且用戶不是匿名用戶,如果否則交給 AccessDeniedHandler 處理。
對應以下代碼
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { if (exception instanceof AuthenticationException) { logger.debug( "Authentication exception occurred; redirecting to authentication entry point", exception); sendStartAuthentication(request, response, chain, (AuthenticationException) exception); } else if (exception instanceof AccessDeniedException) { if (authenticationTrustResolver.isAnonymous(SecurityContextHolder .getContext().getAuthentication())) { logger.debug( "Access is denied (user is anonymous); redirecting to authentication entry point", exception); sendStartAuthentication( request, response, chain, new InsufficientAuthenticationException( "Full authentication is required to access this resource")); } else { logger.debug( "Access is denied (user is not anonymous); delegating to AccessDeniedHandler", exception); accessDeniedHandler.handle(request, response, (AccessDeniedException) exception); } } }
AccessDeniedHandler 默認實現(xiàn)是 AccessDeniedHandlerImpl。該類對異常的處理是返回403錯誤碼。
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { if (!response.isCommitted()) { if (errorPage != null) { // 定義了errorPage // errorPage中可以操作該異常 request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException); // 設置403狀態(tài)碼 response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 轉(zhuǎn)發(fā)到errorPage RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage); dispatcher.forward(request, response); } else { // 沒有定義errorPage,則返回403狀態(tài)碼(Forbidden),以及錯誤信息 response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); } } }
AuthenticationEntryPoint 默認實現(xiàn)是 LoginUrlAuthenticationEntryPoint, 該類的處理是轉(zhuǎn)發(fā)或重定向到登錄頁面
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { String redirectUrl = null; if (useForward) { if (forceHttps && "http".equals(request.getScheme())) { // First redirect the current request to HTTPS. // When that request is received, the forward to the login page will be // used. redirectUrl = buildHttpsRedirectUrlForRequest(request); } if (redirectUrl == null) { String loginForm = determineUrlToUseForThisRequest(request, response, authException); if (logger.isDebugEnabled()) { logger.debug("Server side forward to: " + loginForm); } RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm); // 轉(zhuǎn)發(fā) dispatcher.forward(request, response); return; } } else { // redirect to login page. Use https if forceHttps true redirectUrl = buildRedirectUrlToLoginPage(request, response, authException); } // 重定向 redirectStrategy.sendRedirect(request, response, redirectUrl); }
了解完這些,回到我們的例子。
第3步時,用戶未登錄的情況下訪問受保護資源,ExceptionTranslationFilter會捕獲到AuthenticationException異常(規(guī)則1)。頁面需要跳轉(zhuǎn),ExceptionTranslationFilter在跳轉(zhuǎn)前使用requestCache緩存request。
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { // SEC-112: Clear the SecurityContextHolder's Authentication, as the // existing Authentication is no longer considered valid SecurityContextHolder.getContext().setAuthentication(null); // 緩存 request requestCache.saveRequest(request, response); logger.debug("Calling Authentication entry point."); authenticationEntryPoint.commence(request, response, reason); }
一些坑
在開發(fā)過程中,如果不理解Spring Security如何緩存request,可能會踩一些坑。
舉個簡單例子,如果網(wǎng)站認證是信息存放在header中。第一次請求受保護資源時,請求頭中不包含認證信息 ,驗證失敗,該請求會被緩存,之后即使用戶填寫了信息,也會因為request被恢復導致信息丟失從而認證失敗(問題描述可以參見這里。
最簡單的方案當然是不緩存request。
spring security 提供了NullRequestCache, 該類實現(xiàn)了 RequestCache 接口,但是沒有任何操作。
public class NullRequestCache implements RequestCache { public SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response) { return null; } public void removeRequest(HttpServletRequest request, HttpServletResponse response) { } public void saveRequest(HttpServletRequest request, HttpServletResponse response) { } public HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response) { return null; } }
配置requestCache,使用如下代碼即可:
http.requestCache().requestCache(new NullRequestCache());
補充
默認情況下,三種request不會被緩存。
- 請求地址以/favicon.ico結尾
- header中的content-type值為application/json
- header中的X-Requested-With值為XMLHttpRequest
可以參見:RequestCacheConfigurer類中的私有方法createDefaultSavedRequestMatcher。
附上實例代碼: https://coding.net/u/tanhe123/p/SpringSecurityRequestCache
以上所述是小編給大家介紹的Spring Security 緩存請求問題,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
如果你覺得本文對你有幫助,歡迎轉(zhuǎn)載,煩請注明出處,謝謝!
相關文章
springboot讀取yml文件中的list列表、數(shù)組、map集合和對象方法實例
在平時的yml配置文件中,我們經(jīng)常使用到配置基本數(shù)據(jù)類型的字符串,下面這篇文章主要給大家介紹了關于springboot讀取yml文件中的list列表、數(shù)組、map集合和對象的相關資料,需要的朋友可以參考下2023-02-02spring聲明式事務@Transactional開發(fā)常犯的幾個錯誤及最新解決方案
使用聲明式事務@Transactional進行事務一致性的管理,在開發(fā)過程中,發(fā)現(xiàn)很多開發(fā)同學都用錯了spring聲明式事務@Transactional或使用不規(guī)范,導致出現(xiàn)各種事務問題,這篇文章主要介紹了spring聲明式事務@Transactional開發(fā)常犯的幾個錯誤及解決辦法,需要的朋友可以參考下2024-02-02