Spring?Security?核心過濾器鏈講解
前言:
在熟悉Spring Security的使用和基本操作后,有時(shí)根據(jù)項(xiàng)目需求,我們需要在security原有的過濾器鏈中,添加符合我們自己的過濾器來實(shí)現(xiàn)功能時(shí),我們就必須得先了解security的核心過濾鏈的流程和每個(gè)過濾器的各自功能,以此,我們才可以在特點(diǎn)的過濾器前后加入屬于我們項(xiàng)目需求的過濾器。
一、Filter Chain 圖解
在配置了spring security了之后,會(huì)在運(yùn)行項(xiàng)目的時(shí)候,DefaultSecurityFilterChain會(huì)輸出相關(guān)log:
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List<Filter> filters){ logger.info("Creating filter chain: " + requestMatcher + ", " + filters); this.requestMatcher = requestMatcher; this.filters = new ArrayList<Filter>(filters); }
輸出以下Log:
[main] o.s.s.web.DefaultSecurityFilterChain???? :
Creating filter chain:
org.springframework.security.web.util.matcher.AnyRequestMatcher@1,
[
??? org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@184de357,
??? org.springframework.security.web.context.SecurityContextPersistenceFilter@521ba38f,
??? org.springframework.security.web.header.HeaderWriterFilter@77bb916f,
??? org.springframework.security.web.csrf.CsrfFilter@76b305e1,
??? org.springframework.security.web.authentication.logout.LogoutFilter@17c53dfb,
??? org.springframework.security.web.savedrequest.RequestCacheAwareFilter@2086d469,
??? org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@b1d19ff,
??? org.springframework.security.web.authentication.AnonymousAuthenticationFilter@efe49ab,
??? org.springframework.security.web.session.SessionManagementFilter@5a48d186,
??? org.springframework.security.web.access.ExceptionTranslationFilter@273aaab7
]
也可以從Debug進(jìn)行查看:
二、過濾器逐一解析
在解析前,先說說兩個(gè)至關(guān)重要的類:OncePerRequestFilter和GenericFilterBean,在過濾器鏈的過濾器中,或多或少間接或直接繼承到
- OncePerRequestFilter顧名思義,能夠確保在一次請(qǐng)求只通過一次filter,而不需要重復(fù)執(zhí)行。
- GenericFilterBean是javax.servlet.Filter接口的一個(gè)基本的實(shí)現(xiàn)類
- GenericFilterBean將web.xml中filter標(biāo)簽中的配置參數(shù)-init-param項(xiàng)作為bean的屬性
- GenericFilterBean可以簡(jiǎn)單地成為任何類型的filter的父類
- GenericFilterBean的子類可以自定義一些自己需要的屬性
- GenericFilterBean,將實(shí)際的過濾工作留給他的子類來完成,這就導(dǎo)致了他的子類不得不實(shí)現(xiàn)doFilter方法
- GenericFilterBean不依賴于Spring的ApplicationContext,F(xiàn)ilters通常不會(huì)直接讀取他們的容器信息(ApplicationContext concept)而是通過訪問spring容器(Spring root application context)中的service beans來獲取,通常是通過調(diào)用filter里面的getServletContext() 方法來獲取
2.1.WebAsyncManagerIntegrationFilter
public final class WebAsyncManagerIntegrationFilter extends OncePerRequestFilter { ...... @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager .getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY); if (securityProcessingInterceptor == null) { asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY, new SecurityContextCallableProcessingInterceptor()); } filterChain.doFilter(request, response); } }
從源碼中,我們可以分析出WebAsyncManagerIntegrationFilter相關(guān)功能:
- 根據(jù)請(qǐng)求封裝獲取WebAsyncManager
- 從WebAsyncManager獲取/注冊(cè)SecurityContextCallableProcessingInterceptor
2.2.SecurityContextPersistenceFilter
public class SecurityContextPersistenceFilter extends GenericFilterBean { ...... public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (request.getAttribute(FILTER_APPLIED) != null) { // ensure that filter is only applied once per request chain.doFilter(request, response); return; } final boolean debug = logger.isDebugEnabled(); request.setAttribute(FILTER_APPLIED, Boolean.TRUE); if (forceEagerSessionCreation) { HttpSession session = request.getSession(); if (debug && session.isNew()) { logger.debug("Eagerly created session: " + session.getId()); } } HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); SecurityContext contextBeforeChainExecution = repo.loadContext(holder); try { SecurityContextHolder.setContext(contextBeforeChainExecution); chain.doFilter(holder.getRequest(), holder.getResponse()); } finally { SecurityContext contextAfterChainExecution = SecurityContextHolder .getContext(); // Crucial removal of SecurityContextHolder contents - do this before anything // else. SecurityContextHolder.clearContext(); repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); request.removeAttribute(FILTER_APPLIED); if (debug) { logger.debug("SecurityContextHolder now cleared, as request processing completed"); } } } ...... }
從源碼中,我們可以分析出SecurityContextPersistenceFilter相關(guān)功能:
- 先實(shí)例SecurityContextHolder->HttpSessionSecurityContextRepository(下面以repo代替).作用:其會(huì)從Session中取出已認(rèn)證用戶的信息,提高效率,避免每一次請(qǐng)求都要查詢用戶認(rèn)證信息。
- 根據(jù)請(qǐng)求和響應(yīng)構(gòu)建HttpRequestResponseHolder
- repo根據(jù)HttpRequestResponseHolder加載context獲取SecurityContext
- SecurityContextHolder將獲得到的SecurityContext設(shè)置到Context中,然后繼續(xù)向下執(zhí)行其他過濾器
- finally-> SecurityContextHolder獲取SecurityContext,然后清除,并將其和請(qǐng)求信息保存到repo,從請(qǐng)求中移除FILTER_APPLIED屬性
2.3.HeaderWriterFilter
public class HeaderWriterFilter extends OncePerRequestFilter { ...... @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { for (HeaderWriter headerWriter : headerWriters) { headerWriter.writeHeaders(request, response); } filterChain.doFilter(request, response); } }
從源碼中,我們可以分析HeaderWriterFilter相關(guān)功能:
- 往該請(qǐng)求的Header中添加相應(yīng)的信息,在http標(biāo)簽內(nèi)部使用security:headers來控制
2.4.CsrfFilter
public final class CsrfFilter extends OncePerRequestFilter { ...... @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(HttpServletResponse.class.getName(), response); CsrfToken csrfToken = this.tokenRepository.loadToken(request); final boolean missingToken = csrfToken == null; if (missingToken) { csrfToken = this.tokenRepository.generateToken(request); this.tokenRepository.saveToken(csrfToken, request, response); } request.setAttribute(CsrfToken.class.getName(), csrfToken); request.setAttribute(csrfToken.getParameterName(), csrfToken); if (!this.requireCsrfProtectionMatcher.matches(request)) { filterChain.doFilter(request, response); return; } String actualToken = request.getHeader(csrfToken.getHeaderName()); if (actualToken == null) { actualToken = request.getParameter(csrfToken.getParameterName()); } if (!csrfToken.getToken().equals(actualToken)) { if (this.logger.isDebugEnabled()) { this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)); } if (missingToken) { this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken)); } else { this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken)); } return; } filterChain.doFilter(request, response); } ...... }
從源碼中,我們可以分析出CsrfFilter相關(guān)功能:
- csrf又稱跨域請(qǐng)求偽造,攻擊方通過偽造用戶請(qǐng)求訪問受信任站點(diǎn)。
- 對(duì)需要驗(yàn)證的請(qǐng)求驗(yàn)證是否包含csrf的token信息,如果不包含,則報(bào)錯(cuò)。這樣攻擊網(wǎng)站無法獲取到token信息,則跨域提交的信息都無法通過過濾器的校驗(yàn)。
2.5.LogoutFilter
public class LogoutFilter extends GenericFilterBean { ...... public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (requiresLogout(request, response)) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (logger.isDebugEnabled()) { logger.debug("Logging out user '" + auth + "' and transferring to logout destination"); } this.handler.logout(request, response, auth); logoutSuccessHandler.onLogoutSuccess(request, response, auth); return; } chain.doFilter(request, response); } ...... }
從源碼中,我們可以分析出LogoutFilter相關(guān)功能:
- 匹配URL,默認(rèn)為/logout
- 匹配成功后則用戶退出,清除認(rèn)證信息
2.6.RequestCacheAwareFilter
public class RequestCacheAwareFilter extends GenericFilterBean { ...... public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest( (HttpServletRequest) request, (HttpServletResponse) response); chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest, response); } }
從源碼中,我們可以分析出RequestCacheAwareFilter相關(guān)功能:
通過HttpSessionRequestCache內(nèi)部維護(hù)了一個(gè)RequestCache,用于緩存HttpServletRequest
2.7.SecurityContextHolderAwareRequestFilter
public class SecurityContextHolderAwareRequestFilter extends GenericFilterBean { ...... public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { chain.doFilter(this.requestFactory.create((HttpServletRequest) req, (HttpServletResponse) res), res); } ...... }
從源碼中,我們可以分析出SecurityContextHolderAwareRequestFilter相關(guān)功能:
- 針對(duì)ServletRequest進(jìn)行了一次包裝,使得request具有更加豐富的API
2.8.AnonymousAuthenticationFilter
public class AnonymousAuthenticationFilter extends GenericFilterBean implements InitializingBean { ...... public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { if (SecurityContextHolder.getContext().getAuthentication() == null) { SecurityContextHolder.getContext().setAuthentication( createAuthentication((HttpServletRequest) req)); if (logger.isDebugEnabled()) { logger.debug("Populated SecurityContextHolder with anonymous token: '" + SecurityContextHolder.getContext().getAuthentication() + "'"); } } else { if (logger.isDebugEnabled()) { logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'"); } } chain.doFilter(req, res); } ...... }
從源碼中,我們可以分析出AnonymousAuthenticationFilter相關(guān)功能:
- 當(dāng)SecurityContextHolder中認(rèn)證信息為空,則會(huì)創(chuàng)建一個(gè)匿名用戶存入到SecurityContextHolder中。匿名身份過濾器,這個(gè)過濾器個(gè)人認(rèn)為很重要,需要將它與UsernamePasswordAuthenticationFilter 放在一起比較理解,spring security為了兼容未登錄的訪問,也走了一套認(rèn)證流程,只不過是一個(gè)匿名的身份。
- 匿名認(rèn)證過濾器,可能有人會(huì)想:匿名了還有身份?個(gè)人對(duì)于Anonymous匿名身份的理解是Spirng Security為了整體邏輯的統(tǒng)一性,即使是未通過認(rèn)證的用戶,也給予了一個(gè)匿名身份。而AnonymousAuthenticationFilter該過濾器的位置也是非常的科學(xué)的,它位于常用的身份認(rèn)證過濾器(如UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter、RememberMeAuthenticationFilter)之后,意味著只有在上述身份過濾器執(zhí)行完畢后,SecurityContext依舊沒有用戶信息,AnonymousAuthenticationFilter該過濾器才會(huì)有意義—-基于用戶一個(gè)匿名身份。
2.9.SessionManagementFilter
public class SessionManagementFilter extends GenericFilterBean { ...... public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (request.getAttribute(FILTER_APPLIED) != null) { chain.doFilter(request, response); return; } request.setAttribute(FILTER_APPLIED, Boolean.TRUE); if (!securityContextRepository.containsContext(request)) { Authentication authentication = SecurityContextHolder.getContext() .getAuthentication(); if (authentication != null && !trustResolver.isAnonymous(authentication)) { // The user has been authenticated during the current request, so call the // session strategy try { sessionAuthenticationStrategy.onAuthentication(authentication, request, response); } catch (SessionAuthenticationException e) { // The session strategy can reject the authentication logger.debug( "SessionAuthenticationStrategy rejected the authentication object", e); SecurityContextHolder.clearContext(); failureHandler.onAuthenticationFailure(request, response, e); return; } // Eagerly save the security context to make it available for any possible // re-entrant // requests which may occur before the current request completes. // SEC-1396. securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response); } else { // No security context or authentication present. Check for a session // timeout if (request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) { if (logger.isDebugEnabled()) { logger.debug("Requested session ID " + request.getRequestedSessionId() + " is invalid."); } if (invalidSessionStrategy != null) { invalidSessionStrategy .onInvalidSessionDetected(request, response); return; } } } } chain.doFilter(request, response); } ...... }
從源碼中,我們可以分析出SessionManagementFilter相關(guān)功能:
- securityContextRepository限制同一用戶開啟多個(gè)會(huì)話的數(shù)量
- SessionAuthenticationStrategy防止session-fixation protection attack(保護(hù)非匿名用戶)
2.10.ExceptionTranslationFilter
public class ExceptionTranslationFilter extends GenericFilterBean { ...... public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; try { chain.doFilter(request, response); logger.debug("Chain processed normally"); } catch (IOException ex) { throw ex; } catch (Exception ex) { // Try to extract a SpringSecurityException from the stacktrace Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); RuntimeException ase = (AuthenticationException) throwableAnalyzer .getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase == null) { ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType( AccessDeniedException.class, causeChain); } if (ase != null) { handleSpringSecurityException(request, response, chain, ase); } else { // Rethrow ServletExceptions and RuntimeExceptions as-is if (ex instanceof ServletException) { throw (ServletException) ex; } else if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } // Wrap other Exceptions. This shouldn't actually happen // as we've already covered all the possibilities for doFilter throw new RuntimeException(ex); } } } ...... }
從源碼中,我們可以分析出ExceptionTranslationFilter相關(guān)功能:
- ExceptionTranslationFilter異常轉(zhuǎn)換過濾器位于整個(gè)springSecurityFilterChain的后方,用來轉(zhuǎn)換整個(gè)鏈路中出現(xiàn)的異常
- 此過濾器的作用是處理中FilterSecurityInterceptor拋出的異常,然后將請(qǐng)求重定向到對(duì)應(yīng)頁面,或返回對(duì)應(yīng)的響應(yīng)錯(cuò)誤代碼
2.11.FilterSecurityInterceptor
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { ...... public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); } public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() { return this.securityMetadataSource; } public SecurityMetadataSource obtainSecurityMetadataSource() { return this.securityMetadataSource; } public void invoke(FilterInvocation fi) throws IOException, ServletException { if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) { // filter already applied to this request and user wants us to observe // once-per-request handling, so don't re-do security checking fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { // first time this request being called, so perform security checking if (fi.getRequest() != null) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } } ...... }
從源碼中,我們可以分析出FilterSecurityInterceptor相關(guān)功能:
- 獲取到所配置資源訪問的授權(quán)信息
- 根據(jù)SecurityContextHolder中存儲(chǔ)的用戶信息來決定其是否有權(quán)限
- 主要一些實(shí)現(xiàn)功能在其父類AbstractSecurityInterceptor中
2.12.UsernamePasswordAuthenticationFilter
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { ...... public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } ...... }
從源碼中,我們可以分析出UsernamePasswordAuthenticationFilter相關(guān)功能:
- 表單認(rèn)證是最常用的一個(gè)認(rèn)證方式,一個(gè)最直觀的業(yè)務(wù)場(chǎng)景便是允許用戶在表單中輸入用戶名和密碼進(jìn)行登錄,而這背后的UsernamePasswordAuthenticationFilter,在整個(gè)Spring Security的認(rèn)證體系中則扮演著至關(guān)重要的角色
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
springboot加載命令行參數(shù)ApplicationArguments的實(shí)現(xiàn)
本文主要介紹了springboot加載命令行參數(shù)ApplicationArguments的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04mybatis plus 的動(dòng)態(tài)表名的配置詳解
這篇文章主要介紹了mybatis plus 的動(dòng)態(tài)表名的配置詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09springboot操作阿里云OSS實(shí)現(xiàn)文件上傳,下載,刪除功能
這篇文章主要介紹了springboot操作阿里云OSS實(shí)現(xiàn)文件上傳,下載,刪除功能,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11SpringBoot實(shí)現(xiàn)本地存儲(chǔ)文件上傳及提供HTTP訪問服務(wù)的方法
這篇文章主要介紹了SpringBoot實(shí)現(xiàn)本地存儲(chǔ)文件上傳及提供HTTP訪問服務(wù),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08SpringBoot 如何自定義項(xiàng)目啟動(dòng)信息打印
這篇文章主要介紹了SpringBoot 如何自定義項(xiàng)目啟動(dòng)信息打印方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09Zookeeper中如何解決zookeeper.out文件輸出位置問題
這篇文章主要介紹了Zookeeper中如何解決zookeeper.out文件輸出位置問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04解決myBatis中openSession()自動(dòng)提交的問題
在學(xué)習(xí)MySQL過程中,發(fā)現(xiàn)插入操作自動(dòng)提交,問題原因可能是myBatis中的openSession()方法設(shè)置了自動(dòng)提交,或者是MySQL的默認(rèn)引擎設(shè)置為不支持事務(wù)的MyISAM,解決辦法包括更改myBatis的提交設(shè)置或?qū)ySQL表的引擎改為InnoDB2024-09-09使用Spring CROS解決項(xiàng)目中的跨域問題詳解
這篇文章主要介紹了使用Spring CROS解決項(xiàng)目中的跨域問題詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01