SpringSecurity原理
主要過濾器鏈
SpringSecurity的功能主要是由一系列的過濾器鏈相互配合完成的。驗證一個過濾器之後放行到下一個過濾器鏈,然後到最後。
認證流程
過濾器作用
-
SecurityContextPersistenceFilter:會在每次請求處理之前從配置好的SecurityContextRepository中獲取SecurityContext安全上下文資訊,然後載入到SecurityContextHolder中,然後在該次請求處理完成之後,將SecurityContextHolder中關於這次請求的資訊儲存到一個“倉庫”中,然後將SecurityContextHolder中的資訊清除,例如在Session中維護一個使用者的安全資訊就是這個過濾器處理的。
-
DefaultLoginPageGeneratingFilter:如果沒有配置自定義登入頁面,那系統初始化時就會配置這個過濾器,並且用於在需要進行登入時生成一個登入表單頁面。
-
BasicAuthenticationFilter:檢測和處理http basic認證。
-
UsernamePasswordAuthenticationFilter:用於處理基於表單的登入請求,從表單中獲取使用者名稱和密碼。預設情況下處理來自/login的表單action。從表單中獲取使用者名稱和密碼時,預設使用的表單name屬性值為username和password,這倆個值也可以通過usernameParameter和passwordParameter在配置中自定義。
這個過濾器在表單提交登入請求之時會起作用。那麼假設現在採用SpringSecurity整合Jwt,那麼我需要配置一個Jwt登入認證類(繼承BasicAuthenticationFilter或者繼承OncePerRequestFilter都可以,因為BasicAuthenticationFilter繼承了OncePerRequestFilter),重寫過濾器方法。Jwt的token認證登入是需要在在採用使用者名稱密碼登入認證之前,所以在配置Jwt登入認證類的時候需要在UsernamePasswordAuthenticationFilter之前新增過濾器。
//配置自定義過濾器 新增jwt登入授權過濾器 //在過濾器UsernamePasswordAuthenticationFilter之前 http.addFilterBefore(jwtAuthenticationFilter,UsernamePasswordAuthenticationFilter.class);
-
RequestCacheAwareFilter:用來處理請求的快取。
-
SecurityContextHolderAwareRequestFilter:主要是包裝請求物件request。
-
AnonymousAuthenticationFilter:檢測SecurityContextHolder中是否存在Authentication物件,如果不存在則為其提供一個匿名Authentication。
-
SessionManagementFilter:管理Session的過濾器
-
ExceptionTranslationFilter:捕獲來自過濾器鏈的所有異常,並進行處理。但是隻處理兩類異常:AccessDeniedException和AuthenticationException 異常,其他的異常會繼續丟擲。
如果捕獲到的AuthenticationException,那麼將會使用其對應的AuthenticationEntryPoint的commence()方法處理。在處理之前,ExceptionTranslationFilter先使用RequestCache將當前的HTTPServletRequest的資訊儲存起來,方便使用者登入成功後可以跳轉到之前的頁面。
可以自定義AuthenticationException的處理方法。需要實現AuthenticationEntryPoint介面,然後重寫commence()方法。
/** * 當未登入或者token失效時訪問介面自定義的返回結果 */ @Component public class RestfulAuthorizationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { response.setCharacterEncoding("utf-8"); response.setContentType("application/json"); PrintWriter writer = response.getWriter(); RespBean bean = RespBean.error("請先登入!"); bean.setCode(401); writer.write(new ObjectMapper().writeValueAsString(bean)); writer.flush(); writer.close(); } }
如果捕獲的AuthenticationDeniedException,那麼將會根據當前訪問的使用者是否已經登入認證做不同的處理,如果未登入,則會使用關聯的AuthenticationEntryPoint的commence()方法進行處理,否則將使用關聯的AccessDeniedHandler的handle()方法進行處理。
可以進行自定義AuthenticationDeniedException的處理方法。需要實現AccessDeniedHandler介面,然後重寫handle()方法。
@Component public class RestfulAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { response.setCharacterEncoding("utf-8"); response.setContentType("application/json"); PrintWriter writer = response.getWriter(); RespBean error = RespBean.error("許可權不足,聯絡管理員!"); writer.write(new ObjectMapper().writeValueAsString(error)); error.setCode(403); writer.flush(); writer.close(); } }
-
FilterSecurityInterceptor:可以看做過濾器鏈的出口
-
RememberMeAuthenticationFilter:當使用者沒有登入而直接訪問資源時, 從 cookie 裡找出使用者的資訊, 如果 Spring Security 能夠識別出使用者提供的remember me cookie, 使用者將不必填寫使用者名稱和密碼, 而是直接登入進入系統,該過濾器預設不開啟。
SecurityContextHolder
SecurityContext物件是安全上下文資訊,包括當前使用系統的使用者的資訊。每個使用者都會有它的安全上下文物件,所以把每一個使用者的SecurityContext儲存到SecurityContextHolder中。
SecurityContextHolder儲存SecurityContext的方式根據應用場景不同也有區別:
(1)單機系統,即應用從開啟到關閉的整個生命週期只有一個使用者在使用。由於整個應用只需要儲存一個SecurityContext(安全上下文即可)
(2)多使用者系統,比如典型的Web系統,整個生命週期可能同時有多個使用者在使用。這時候應用需要儲存多個SecurityContext(安全上下文),需要利用ThreadLocal進行儲存,每個執行緒都可以利用ThreadLocal獲取其自己的SecurityContext,及安全上下文。ThreadLocal內部會用陣列來儲存多個物件的。原理是,ThreadLocal會為每個執行緒開闢一個儲存區域,來儲存相應的物件。
Authentication:使用者資訊的表示
在SecurityContextHolder中儲存了當前與系統互動的使用者的資訊。Spring Security使用一個Authentication 物件來表示這些資訊。
Authentication 主要包含了:
- 使用者許可權集合
- 使用者證照(密碼)
- 細節(Details)
- Principal(就是這個使用者的賬戶資訊)
在自定義登入認證過濾器的時候,記得需要把使用者的資訊(Authentication )儲存到SecurityContextHolder中,以便後續使用者的正常使用。比如我在做和Jwt認證的整合的時候,繼承OncePerRequestFilter,重寫doFilterInternal方法,認證完token之後,就需要把使用者的資訊存入安全上下文Holder中。
UsernamePasswordAuthenticationToken authenticationToken
=new UsernamePasswordAuthenticationToken(user,null, null);
authenticationToken.setDetails(new WebAuthenticationDetailsSource()
.buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
關於SecurityContextHolder大概就這樣,有一些關於SecurityContextHolder具體的原始碼的細節可以參考一篇部落格: