<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          兩萬字詳解,帶你徹底搞懂 Spring Security 6.0 的實現(xiàn)原理

          共 38673字,需瀏覽 78分鐘

           ·

          2024-07-10 11:55

          大家好,我是胖虎,給首先大家分享兩個產(chǎn)品

          無需魔法,國內(nèi)可直接使用官方ChatGPT-4(Plus)、ChatGPT-4o!

          Java八股文面試小程序!

          導言

          Spring Security是一個功能強大且高度且可定制的身份驗證和訪問控制框架,除了標準的身份認證和授權(quán)之外,它還支持點擊劫持,CSRF,XSS,MITM(中間人)等常見攻擊手段的保護,并提供密碼編碼,LDAP認證,Session管理,Remember Me認證,JWT,OAuth 2.0等功能特性。

          由于安全領(lǐng)域本身的復雜性和豐富的安全特性支持,以及Spring Security高度的可定制性,使得它成為一個龐大且復雜的框架。每次升級可能帶來的破壞性更新,加上網(wǎng)絡上的陳舊教程,更是加重了Spring Security非常難用的印象。很多新手可能跟作者一樣,首次引入Spring Security框架之后,突然發(fā)現(xiàn)很多頁面無法訪問,感到無所適從。

          為此,本文將基于Spring Boot 3.1.x依賴的Spring Security 6.1.x版本,深入探討Spring Security的架構(gòu)和實現(xiàn)原理。本文將著重解釋Spring Security的設計思想,而不會過多涉及具體的實現(xiàn)細節(jié)。

          文章的目標是讓讀者在閱讀完本文之后,能夠?qū)φ麄€Spring Security框架有個清晰的理解,并在面對問題時知道如何著手排查。另外,本文重點關(guān)注Spring Security的總體架構(gòu),以及身份認證(Authentication)和鑒權(quán)控制(Authorization)的實現(xiàn)。

          ?

          【版本兼容性】Spring Security 6引入了很多破壞性的更新,包括廢棄代碼的刪除,方法重命名,全新的配置DSL等,但是架構(gòu)和基本原理還是保持不變的。本文在講解過程中會盡量指出當前版本跟老版本的差異,尤其是涉及到兼容性問題的時候。
          【閱讀提示】本文的篇幅較長,并且包含了部分源碼分析,時間有限的情況下,可以重點閱讀架構(gòu)圖部分。

          Java Web應用的Security實現(xiàn)基本思路

          大家可以嘗試思考下,安全相關(guān)的校驗和處理,應該處于應用的哪個部分呢?答案是,應該放在所有請求的入口,因為它是跟具體的業(yè)務邏輯無關(guān)的,在Spring MVC世界里就是@Controller之前。

          在JakartaEE(JavaEE的新版)規(guī)范中,F(xiàn)ilter和Servlet都符合這個前置要求。然而,Spring的Web應用基本上只包含一個DispatcherServelt,主要用于請求分發(fā),缺乏安全相關(guān)的支持和合適的擴展機制。而Filter運行在Servlet之前,而規(guī)范本身就支持配置多個Filter。因此,在請求到達Servlet之前,先通過Filter進行安全驗證就是一個非常合理的實現(xiàn)方式。這樣可以在請求進入業(yè)務邏輯之前,對請求進行攔擊,然后進行必要的安全性檢查和處理。

          這也是Spring Security的實現(xiàn)方式。本質(zhì)上,Spring Security的實現(xiàn)原理很簡單,就是提供了一個用于安全驗證的Filter。假如我們自己實現(xiàn)一個簡化版的Filter,它的大概邏輯應該是這樣的:

          public class SimpleSecurityFilter extends HttpFilter {
              @Override
              protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
                  UsernamePasswordToken token = extractUsernameAndPasswordFrom(request);  // (1)
                  if (notAuthenticated(token)) {  // (2)
                      // 用戶名密碼錯誤
                      response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // HTTP 401.
                      return;
                  }
                  if (notAuthorized(token, request)) { // (3)
                      // 當前登錄用戶的權(quán)限不足
                      response.setStatus(HttpServletResponse.SC_FORBIDDEN); // HTTP 403
                      return;
                  }
                  // 通過了身份驗證和權(quán)限校驗,繼續(xù)執(zhí)行其它Filter,最終到達Servlet
                  chain.doFilter(request, response); // (4)
              }
          }
          1. 從HTTP請求中獲取用戶名和密碼,來源包括標準的Basic Auth HTTP Header,表單字段或者cookie等等。

          2. 身份認證,也就是校驗用戶名和密碼。

          3. 認證通過后,需要檢查當前登錄的用戶有沒有訪問當前HTTP請求的權(quán)限,也就是鑒權(quán)邏輯。

          4. 權(quán)限校驗也通過后,就繼續(xù)執(zhí)行其它Filter,所有Filter都通過后,進入Servlet,最終到達具體的Controller。

          FilterChain

          在安全領(lǐng)域,由于攻防手段的多樣性和認證鑒權(quán)方式的復雜性,將所有功能都放在一個Filter中會導致該Filter迅速演變?yōu)橐粋€龐大而復雜的類。

          因此,在實際應用場景中,我們常常將這個龐大的Filter拆分成多個小Filter,并將它們鏈接在一起。每個Filter都只負責特定領(lǐng)域的功能,比如CsrfFilter,AuthenticationFilter,AuthorizationFilter等。

          這種概念被稱為FilterChain,實際上JarkataEE規(guī)范也有相識的概念。通過使用FilterChain,你就可以以插拔的方式添加或移除特定功能的Filter,而無需改動現(xiàn)有的代碼。

          Spring Security框架的基本架構(gòu)和原理

          上一節(jié)其實已經(jīng)說明了Spring Security框架的基本思路,下面我們深入分析其實現(xiàn)原理和架構(gòu)。

          實現(xiàn)原理

          一個應用引入了Spring Security Starter包后,再啟動應用,你會發(fā)現(xiàn)控制臺多了下面這條日志,說明已經(jīng)開啟了Security特性。

          2023-07-12T10:05:23.168+08:00  INFO 680540 --- [
          main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@46e3559f, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@3b83459e, 
          org.springframework.security.web.context.SecurityContextHolderFilter@26837057, org.springframework.security.web.header.HeaderWriterFilter@2d74c81b, org.springframework.security.web.csrf.CsrfFilter@3a17b2e3, 
          org.springframework.security.web.authentication.logout.LogoutFilter@5f5827d0, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4ed5a1b0, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@3b332962, 
          org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@32118208, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@67b355c8, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@991cbde, 
          org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@dd4aec3, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@414f87a9, org.springframework.security.web.access.ExceptionTranslationFilter@59939293, org.springframework.security.web.access.intercept.AuthorizationFilter@f438904]

          從這條日志可以觀察到,Spring Security通過DefaultSecurityFilterChain類來完成安全相關(guān)的功能,而該類本身又由其它Filter組成。默認情況下,Spring Security Starter引入了15個Filter,下面我們簡要介紹下其中幾個重要的Filter:

          1. CsrfFilter:這個Filter用于防止跨站點請求偽造攻擊,這也是導致所有POST請求都失敗的原因?;赥oken驗證的API服務可以選擇關(guān)閉CsrfFilter,而一般Web頁面需要開啟。

          2. BasicAuthenticationFilter:支持HTTP的標準Basic Auth的身份驗證模塊。

          3. UsernamePasswordAuthenticationFilter:支持Form表單形式的身份驗證模塊。

          4. DefaultLoginPageGeneratingFilter和DefaultLogoutPageGeneratingFilter:用于自動生成登錄頁面和注銷頁面。

          5. AuthorizationFilter: 這個Filter負責授權(quán)模塊。值得注意的是,在老版本中鑒權(quán)模塊是FilterSecurityInterceptor.

          這些Filter構(gòu)成了Spring Security的核心功能,通過它們,我們可以實現(xiàn)身份驗證、授權(quán)、防護等安全特性。根據(jù)應用的需求,我們可以選擇啟用或禁用特定的Filter,以定制和優(yōu)化安全策略。

          SecurityFilterChain

          DefaultSecurityFilterChain類實現(xiàn)了SecurityFilterChain接口,我們打開這個接口的源碼,會發(fā)現(xiàn)它只有兩個方法,matches用于匹配特定的Http請求(比如特定規(guī)則的URL),getFilters 用于獲取可用的所有Security Filter。

          public interface SecurityFilterChain {
              boolean matches(HttpServletRequest request)// 規(guī)則匹配
              List<Filter> getFilters()// 該FilterChain下的所有Security Filter
          }

          從這段代碼可以得出兩個結(jié)論:

          1. 不同的Http請求可以對應不同的SecurityFilterChain(通過matches方法)。

          2. SecurityFilterChain不是我們以為的JakartaEE的Servlet Filter實現(xiàn),它僅僅是一個包含多個Filter的容器,本身不負責調(diào)度和執(zhí)行。它只是一個配置項,用于指定一組Filter,以實現(xiàn)特定的安全需求。

          DelegatingFilterProxy

          實際上,JakartaEE層面上的Filter實現(xiàn)是DelegatingFilterProxy類,它在Spring Security中起到了一個重要的橋梁作用,連接了Servlet容器和Spring容器。Servlet容器不了解Spring定義的Beans,而Spring Security的大部分組件及其依賴都是注冊到Spring容器中的Bean。

          DelegatingFilterProxy核心代碼的主要工作就是從WebApplicationContext獲取指定名稱的Filter Bean,然后委托給這個Bean的doFilter方法。以下是簡化后的偽代碼:

          public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
                  throws ServletException, IOException {
              Filter delegateToUse = this.delegate;
              if (delegateToUse == null) {
                  synchronized (this.delegateMonitor) {
                      delegateToUse = this.delegate;
                      if (delegateToUse == null) {
                          WebApplicationContext wac = findWebApplicationContext();
                          // 獲取Filter Bean并初始化
                          delegateToUse = initDelegate(wac);
                      }
                      this.delegate = delegateToUse;
                  }
              }
              // 委托給的delegate對象完成實際的doFilter
              invokeDelegate(delegateToUse, request, response, filterChain);
          }

          protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
              // Bean名稱配置在SecurityFilterAutoConfiguration.DEFAULT_FILTER_NAME = "springSecurityFilterChain"
              String targetBeanName = getTargetBeanName();
              // 從容器中獲取指定名稱的Filter類型Bean
              Filter delegate = wac.getBean(targetBeanName, Filter.class);
              if (isTargetFilterLifecycle()) {
                  delegate.init(getFilterConfig());
              }
              return delegate;
          }

          通過這種方式,DelegatingFilterProxy實現(xiàn)了將Servlet容器中的Filter請求委托給Spring容器中的具體Filter Bean處理,從而實現(xiàn)了Servlet容器和Spring容器之間的無縫連接。

          FilterChainProxy

          而這個被委托的Filter Bean的類型就是FilterChainProxy,是在WebSecurityConfiguration中配置的:

          // name = "springSecurityFilterChain"
          @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
          public Filter springSecurityFilterChain() throws Exception {
              // 配置SecurityFilterChain
              boolean hasFilterChain = !this.securityFilterChains.isEmpty();
              if (!hasFilterChain) {
                  this.webSecurity.addSecurityFilterChainBuilder(() -> {
                      this.httpSecurity.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated());
                      this.httpSecurity.formLogin(Customizer.withDefaults());
                      this.httpSecurity.httpBasic(Customizer.withDefaults());
                      return this.httpSecurity.build();
                  });
              }
              for (SecurityFilterChain securityFilterChain : this.securityFilterChains) {
                  this.webSecurity.addSecurityFilterChainBuilder(() -> securityFilterChain);
              }
              // WebSecurity自定義配置
              for (WebSecurityCustomizer customizer : this.webSecurityCustomizers) {
                  customizer.customize(this.webSecurity);
              }
              // FilterChainProxy最終是由WebSecurity構(gòu)建出來的
              return this.webSecurity.build();
          }

          從上面代碼可以發(fā)現(xiàn),FilterChainProxy對象最終是由WebSecurity根據(jù)SecurityFilterChain和其它一些配置構(gòu)建出來的。

          FilterChainProxy主要作用就是查找匹配當前Http請求規(guī)則的SecurityFilterChain,然后將工作委派給SecurityFilterChain的所有Filter。簡化后的偽代碼如下所示:

          public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
              // 獲取匹配的所有Filter
              List<Filter> filters = getFilters(request); 
              // 按順序執(zhí)行Filter
              Filter nextFilter = this.filters.get(this.currentPosition - 1);
              nextFilter.doFilter(request, response, this);
          }

          private List<Filter> getFilters(HttpServletRequest request) {
              for (SecurityFilterChain chain : this.filterChains) {
                  // 返回匹配規(guī)則的SecurityFilterChain的Filter列表
                  if (chain.matches(request)) { 
                      return chain.getFilters();
                  }
              }
              return null;
          }
          ?

          【Tips】FilterChainProxy可以認為是整個Spring Security處理請求的一個起點,如果你遇到Security相關(guān)問題,又不清楚是具體哪個Filter導致的,就可以從這里開始Debug。

          基本架構(gòu)

          從上一節(jié)的內(nèi)容,我們可以得出下面這一副架構(gòu)圖(圖中藍色和橘紅色的部分代表Security Security)。從圖中可以看出,Spring Security框架通過DelegatingFilterProxy建立起了Servlet容器和Spring容器的鏈接,FilterChainProxy基于匹配規(guī)則(比如URL匹配),決定使用哪個SecurityFilterChain。而SecurityFilterChain又由零到多個Filter組成,這些Filter完成實際的功能。

          Security Filter和配置DSL

          Spring Security是基于Jakarta EE的Filter實現(xiàn)的,而在此基礎上,它提供了一套自身的Filter機制,相當于兩層的Filter嵌套。為了不混淆這兩種Filter,我們把Spring Security框架提供的Filter稱為Security Filter。在下文中,我們所提及的配置,擴展和自定義的Filter都指的是Security Filter,如果沒有特別說明,都默認指的是Security Filter。

          通過一系列Security Filter,Spring Security提供了豐富的開箱即用的安全功能,包括身份認證,鑒權(quán),Csrf等等。每個功能都是通過一個或者多個Security Filter實現(xiàn)的。有些復雜的Filter,例如身份認證和鑒權(quán),擁有自己的特定架構(gòu),并且會依賴Filter的順序和執(zhí)行過程中的上下文信息,這也是導致Spring Security在使用上相對復雜的原因之一。

          Spring Securiy的基本配置都是通過自定義SecurityFilterChain的Bean來實現(xiàn)的。下面是一個示例配置,它提供了自定義的登錄頁面,并且針對不同的URL配置了不同的角色權(quán)限,這些配置方法實際上就是配置不同的Security Filter,更詳細的解釋會在后面講解具體特性的時候時展開說明。

          @Bean
          static SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {
              // 鑒權(quán)相關(guān)配置
              http.authorizeHttpRequests((requests) ->
                      request.requestMatchers("/admin").hasAuthority("ROLE_ADMIN"// "/admin"要求有“ROLE_ADMIN"角色權(quán)限
                              .requestMatchers("/hello").hasRole("USER"// "/hello"要求有"ROLE_USER"角色權(quán)限
                              .anyRequest().authenticated()); // 其它只需要身份認證通過即可,不需要其它特殊權(quán)限
              // 登錄相關(guān)配置
              http.formLogin(formLogin -> formLogin
                      .loginPage("/authentication"// 自定義登錄頁面,不再使用內(nèi)置的自動生成頁面
                      .permitAll() // 允許自定義頁面的匿名訪問,不需要認證和鑒權(quán)
              );
              return http.build(); // 返回構(gòu)建的SecurityFilterChain實例
          }
          ?

          【版本兼容性】Spring Security 6.0在配置方面引入了許多改變。在之前的老版本中,可以選擇廢棄的WebSecurityConfigurerAdapter進行配置,但從6.0版本開始,這個廢棄類已經(jīng)被刪除了。而目前很多老項目以及網(wǎng)上的教程仍在使用WebSecurityConfigurerAdapter。 另外,配置DLS也發(fā)生了變化。Spring Security 6.0采用了基于Lambda表達式的DSL配置方式,取代了之前的純鏈式調(diào)用方式,使得配置更加靈活和直觀。一些方法名稱也進行了修改,例如antMatchers替換為requestMatchers。

          除了Spring Boot的專有配置,Spring Security自身也提供了默認配置,這些默認配置在HttpSecurityConfiguration#httpSecurity方法中,它默認添加了很多Security Filter,核心代碼如下:

          @Bean(HTTPSECURITY_BEAN_NAME)
          @Scope("prototype")
          HttpSecurity httpSecurity() throws Exception {
              // ... //
              http
                  .csrf(withDefaults())
                  .addFilter(webAsyncManagerIntegrationFilter)
                  .exceptionHandling(withDefaults())
                  .headers(withDefaults())
                  .sessionManagement(withDefaults())
                  .securityContext(withDefaults())
                  .requestCache(withDefaults())
                  .anonymous(withDefaults())
                  .servletApi(withDefaults())
                  .apply(new DefaultLoginPageConfigurer<>());
              http.logout(withDefaults());
              // ... //
              return http;
          }

          以上解釋了Spring Security的實現(xiàn)原理和基本架構(gòu),而具體到特定的Security Filter,又有各種的框架,下面將展開說明認證和鑒權(quán)兩個核心模塊。

          Authentication身份認證

          身份認證有很多種方式,大致可以分為以下4類:

          1. 標準的賬號密碼認證:這是很多網(wǎng)站都支持的方式,也是大家最熟悉的認證模式;

          2. 調(diào)用第三方服務或內(nèi)部其它API進行認證:當服務自身無法直接獲取用戶的密碼時,需要借助第三方服務或者內(nèi)部API進行認證;

          3. 基于Token的認證:這是API服務一般使用的認知方式,通過令牌來進行身份驗證;

          4. OAuth2或其它OpenID認證:這種方式廣泛用于允許用戶使用其它平臺的身份信息進行登錄,例如微信登錄,Google登錄等。

          Spring Security支持大部分的認證方式,但不同的認證方式需要配置不同的Bean及其依賴Bean,否則很容易遇到各種異常和空指針。

          本文重點討論標準的賬號密碼認證方式。

          實現(xiàn)原理

          如果你使用的是Spring Boot,那么Spring Boot Starter Security默認就配置了Form表單和Basic認證方式,其配置代碼如下所示:

          @Configuration(proxyBeanMethods = false)
          @ConditionalOnWebApplication(type = Type.SERVLET)
          class SpringBootWebSecurityConfiguration {
              @Configuration(proxyBeanMethods = false)
              @ConditionalOnDefaultWebSecurity
              static class SecurityFilterChainConfiguration {
                  @Bean
                  @Order(SecurityProperties.BASIC_AUTH_ORDER)
                  SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
                      http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()); // 所有URL都需要認證用戶
                      http.formLogin(withDefaults()); // 支持form表單認證,默認配置提供了自動生成的登錄和注銷頁面
                      http.httpBasic(withDefaults()); // 支持HTTP Basic Authentication
                      return http.build();
                  }

              }
              // ...其它配置...
          }

          為了討論方便,我們用下面的配置覆蓋Spring Boot默認的配置,只支持Form表單認證方式,討論它具體是如何實現(xiàn)的。

          @Configuration()  
          public class MySecurityConfig {
              @Bean
              SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {
                  http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()); // (1)
                  http.formLogin(withDefaults()); // (2)
                  return http.build();
              }
          }
          1. authorizeHttpRequests方法用于配置每個請求的權(quán)限控制,這里要求所有請求都要通過認證后才能訪問。實際上,這個方法配置的更多是鑒權(quán)相關(guān)的內(nèi)容,跟身份認證的關(guān)聯(lián)較小,它本質(zhì)上是增加了一個AuthorizationFilter用于鑒權(quán),具體細節(jié)在鑒權(quán)部分會詳細說明。

          2. http.formLogin方法提供了Form表單認證的方式,withDefaults方法是Form表單認證的默認配置。這段配置的作用就是增加了用于賬號密碼認證的UsernamePasswordAuthenticationFilter,以及自動生成登錄頁面和注銷頁面的DefaultLogoutPageGeneratingFilterDefaultLogoutPageGeneratingFilter共3個Security Filter。值得注意的是,登錄頁面和注銷頁面這兩個Filter是配合DefaultLoginPageConfigurer配置一起注冊的。如果你通過formLogin.loginPage提供了自定義的登錄頁面,那么這兩個Filter就不會被注冊。

          在本節(jié)中,我們主要討論身份認證的實現(xiàn),因此,接下來將詳細探究Form表單認證方式中UsernamePasswordAuthenticationFilter的實現(xiàn)。

          AbstractAuthenticationProcessingFilter

          對于Filter,我們重點分析它的doFilter方法的源碼。實際上,它繼承了抽象類AbstractAuthenticationProcessingFilter,而這個抽象類的doFilter是一個模板方法,定義了整個認證流程。其核心流程非常簡單,偽代碼如下:

          private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
                  throws IOException, ServletException 
          {
              // 首先判斷該請求是否是認證請求或者登錄請求
              if (!requiresAuthentication(request, response)) { // (1)
                  chain.doFilter(request, response);
                  return;
              }
              try {
                  Authentication authenticationResult = attemptAuthentication(request, response); // (2) 實際認證邏輯
                  // 認證成功
                  successfulAuthentication(request, response, chain, authenticationResult); // (3)
              }
              catch (AuthenticationException ex) {
                  // 認證失敗
                  unsuccessfulAuthentication(request, response, ex); // (4)
              }
          }
          1. 首先requiresAuthentication方法用于判斷當前請求是否為認證請求或者登錄請求,例如通常是POST /login。只有在登錄認證的情況下,才需要通過這個Filter;

          2. attempAuthentication方法是實際的認證邏輯,這是一個抽象方法,具體的邏輯由子類重寫實現(xiàn)。它的規(guī)范行為是,如果認證成功,應該返回認證結(jié)果Authentication,否則以拋出異常AuthenticationException的方式表示認證失??;

          3. successfulAuthentication認證成功后,該方法會將Authentication對象放到Security Context中,這是非常關(guān)鍵的一步,后續(xù)需要認證結(jié)果的時候都是從Security Context獲取的,比如鑒權(quán)Filter。此外,該方法還會處理其它一些相關(guān)功能,比如RememberMe,事件發(fā)布,最后再調(diào)用AuthenticationSuccessHandler

          4. unsuccessfulAuthentication :在認證失敗后,它會清空Security Context,調(diào)用RememberMe相關(guān)服務和AuthenticationFailureHandler來處理認證失敗后的回調(diào)邏輯,比如跳轉(zhuǎn)到錯誤頁面。

          Authentication模型

          在這里,我們涉及到了一個非常重要的數(shù)據(jù)模型——Authentication,它是一個接口類型,它既是對認證結(jié)果的一個抽象表示,同時也是對認證請求的一個抽象,通常也被稱為認證Token。它的方法都比較抽象,定義如下:

          public interface Authentication extends PrincipalSerializable {
              // 當前認證用戶擁有的權(quán)限列表
              Collection<? extends GrantedAuthority> getAuthorities();
              // 用戶的一個身份標識,通常就是用戶名
              Object getPrincipal();
              // 可用于證明用戶身份的一個憑證,通常就是用戶密碼
              Object getCredentials();
              // 當前用戶是否認證通過
              boolean isAuthenticated();
              // 更新用戶的認證狀態(tài)
              void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
              // 獲取附加的詳情信息,比如原始的Http請求體等。
              Object getDetails();
          }

          具體的Authentication實現(xiàn)一般都命名為XXXToken,大部分都繼承自抽象類AbstractAuthenticationToken,比如表示標準的用戶名密碼認證結(jié)果的UsernamePasswordAuthenticationToken,表示匿名登錄用戶認證結(jié)果的AnonymousAuthenticationToken等等,你也可以完全實現(xiàn)自己的Authentication。

          attempAuthentication方法

          接下來,我們看下UsernamePasswordAuthenticationFilter的認證具體實現(xiàn)方法attempAuthentication,它的源碼如下:

          public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                  throws AuthenticationException 
          {
              // 默認只支持POST請求
              if (this.postOnly && !request.getMethod().equals("POST")) {
                  throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
              }
              // 從form表單獲取用戶名和密碼
              String username = obtainUsername(request);
              username = (username != null) ? username.trim() : "";
              String password = obtainPassword(request);
              password = (password != null) ? password : "";
              // 構(gòu)建一個用于認證的請求
              UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
                      password);
              // 附加詳細信息,比如請求體,有些認證方式需要除了用戶名密碼外更多的信息
              setDetails(request, authRequest);
              // 委托給AuthenticationManager做具體的認證
              return this.getAuthenticationManager().authenticate(authRequest);
          }

          這個方法非常簡單,它主要進行一些前置校驗工作,從請求體中獲取用戶名和密碼,并構(gòu)建認證請求對象。然后,剩余的認證工作都是委托給AuthenticationManager接口來完成的,該接口的定義如下:

          public interface AuthenticationManager {
              Authentication authenticate(Authentication authentication) throws AuthenticationException;
          }

          AuthenticationManager和AuthenticationProvider

          AuthenticationManager接口只有一個方法,它的入?yún)⒑统鰠⒍际?code style="color: rgb(53, 148, 247);line-height: 1.8em;letter-spacing: 0em;background: none 0% 0% / auto no-repeat scroll padding-box border-box rgba(27, 31, 35, 0.05);width: auto;height: auto;margin-left: 2px;margin-right: 2px;padding: 2px 4px;border-style: none;border-width: 3px;border-color: rgb(0, 0, 0) rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.4);border-radius: 4px;font-family: 'Operator Mono', Consolas, Monaco, Menlo, monospace;word-break: break-all;">Authentication對象。通常情況下,入?yún)⑻峁┝吮匾恼J證信息,例如用戶名和密碼。而在認證成功后,該方法會返回認證結(jié)果,并附加認證狀態(tài),用戶擁有的權(quán)限列表等信息。如果認證失敗,它會拋出AuthenticationException異常類的子類,其中包括DisabledException,LockedExceptionBadCredentialsException等賬號相關(guān)的異常。

          AuthenticationManager接口定義了Spring Security的認證行為。你可以提供自定義的實現(xiàn),Spring Security也提供了一個通用的實現(xiàn)類ProviderManagerProviderManager將具體的認證工作委托給一系列的AuthenticationProvider。

          每個AuthenticationProvider對應不同的認證方式。比如最常見的用戶名密碼的認證實現(xiàn)是DaoAuthenticationProvider,而JwtAuthenticationProvider提供了JWT Token的認證。你可以通過添加不同的AuthenticationProvider的方式,在同一個服務內(nèi)支持多種類型的認證方式,比如需要調(diào)用其它API檢驗密碼的情況,就需要自定義AuthenticationProvider。

          此外,ProviderManager還可以配置父級AuthenticationManager,當這個ProviderManager的所有AuthenticationProvider都不支持所需的認證方式時,它會繼續(xù)委托給父級的AuthenticationManager,而該父級通常也是一個ProviderManager類型。

          UserDetailsService和PasswordEncoder

          DaoAuthenticationProvider是最常用的認證實現(xiàn)之一,它通過UserDetailsServicePasswordEncoder來驗證用戶名和密碼。

          UserDetailsService的作用是查找用戶信息UserDetails,這些信息包括用戶密碼,狀態(tài),權(quán)限列表等。用戶信息可以存儲在內(nèi)存,數(shù)據(jù)庫或者其它任何地方。Spring Security默認的配置是內(nèi)存存儲,對應的UserDetailsService實現(xiàn)是InMemoryUserDetailsManager,而數(shù)據(jù)庫存儲則對應JdbcUserDetailsManager。

          UserDetailsService獲取到用戶密碼后,需要通過PasswordEncoder來驗證密碼的正確性。因為密碼一般都不應該以明文形式存儲,實際存儲的是按一定規(guī)則編碼后的文本,Spring Security支持多種編碼方式,例如bcryptargon2,scrypt,pbkdf2等。你可以配置PasswordEncoder Bean來選擇不同的編碼方式。都是請注意,內(nèi)置的編碼方式默認對編碼后的文本有一個格式要求,就是必須有類似{bcrypt}的前綴來表示編碼方式。

          基本架構(gòu)

          架構(gòu)圖

          上一節(jié)中,我們講述了用戶名密碼認證的實現(xiàn)細節(jié),現(xiàn)在,讓我們以用戶名密碼認證方式為例,從整體上來看下身份認證的架構(gòu)和流程。它的整體架構(gòu)如下:

          1. 當一個HTTP請求進來后,UsernamePasswordAuthenticationFilter會從HTTP請求體中獲取用戶名和密碼,然后使用這些信息創(chuàng)建一個UsernamePasswordAuthenticationToken對象作為認證請求的參數(shù)。

          2. 接下來,AuthenticationManager(其實現(xiàn)類是ProviderManager)負責對接受到的UsernamePasswordAuthenticationToken進行認證。

          3. ProviderManager會遍歷配置的所有AuthenticationProvider,查找支持UsernamePasswordAuthenticationToken類型的AuthenticationProvider,然后委托其進行實際的認證工作,而在這里,匹配的就是DaoAuthenticationProvider。

          4. DaoAuthenticationProvider首先調(diào)用UserDetailService獲取用戶信息,然后將獲取到的密碼(通常是編碼后的密碼)委托給PasswordEncoder進行驗證。如果認證失敗,DaoAuthenticationProvider會拋出AuthenticationException的子類表示認證失敗。

          5. 當認證成功時,AuthenticationManager會返回一個UsernamePasswordAuthenticationToken對象作為認證結(jié)果,這個對象除了包含用戶的基本信息外,最重要的是認證通過狀態(tài)以及該用戶擁有的權(quán)限列表,這些信息在后續(xù)的鑒權(quán)模塊會用到。

          6. 認證結(jié)果會被放入SecurityContext,這樣后續(xù)的模塊(包括鑒權(quán)和用戶業(yè)務模塊等)如果需要這個結(jié)果(包括用戶信息和權(quán)限列表),就可以通過以下方法獲?。?code style="color: rgb(53, 148, 247);line-height: 1.8em;letter-spacing: 0em;height: auto;background: none 0% 0% / auto no-repeat scroll padding-box border-box rgba(27, 31, 35, 0.05);width: auto;margin-left: 2px;margin-right: 2px;padding: 2px 4px;border-style: none;border-width: 3px;border-color: rgb(0, 0, 0) rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.4);border-radius: 4px;font-family: 'Operator Mono', Consolas, Monaco, Menlo, monospace;word-break: break-all;">SecurityContextHolder.getContext().getAuthentication()。

          組件替換

          這個架構(gòu)非常靈活,大部分組件都是可配置和可替換的,自底向上,我們分別可以替換以下組件來滿足特定需求:

          1. UserDetailsService:根據(jù)用戶名查找用戶信息的組件,默認配置的是內(nèi)存存儲InMemoryUserDetailsManager,你也可以配置為內(nèi)置的數(shù)據(jù)庫存儲JdbcUserDetailsManager,但是它有很多默認的約定要遵守,對未來的擴展也不夠靈活。通常會根據(jù)公司的規(guī)范要求或數(shù)據(jù)庫存儲的方式提供自定義的實現(xiàn)。

          2. PasswordEncoder:對密碼進行編碼的組件,建議根據(jù)公司的編碼要求或當前數(shù)據(jù)庫中已使用的編碼來配置。如果沒有特殊要求,建議采用默認的BCryptPasswordEncoder

          3. AuthenticationProvider: 為了安全需要,公司內(nèi)部很多應用是不允許直接訪問用戶的密碼的,而通常會提供一個認證的API。此時,就需要自定義AuthenticationProvider,它的核心邏輯就是調(diào)用API做認證,然后把結(jié)果再包裝成Authentication返回給AuthenticationManager。

          4. AuthenticationManager:它的默認實現(xiàn)ProviderManager適用于大部分場景,通常不需要替換,除非你不想引入太多的概念。

          5. UsernamePasswordAuthenticationFilter:如果你不想引入過多的概念和復雜度,可以提供自己的Security Filter,從而完全脫離該框架。但是需要確保認證結(jié)果模型Authentication仍然被正確處理,并且將結(jié)果通過方法SecurityContextHolder.getContext().setAuthentication放入Security Context中。

          ?

          【Tips】從整個Security框架的角度來看,認證模塊的核心概念只有兩個,分別是認證結(jié)果AuthenticationSecurity Context。其它概念都可以認為是認證模塊的內(nèi)部實現(xiàn)細節(jié)。

          鑒權(quán)模塊Authorization

          認證模塊證明了用戶的身份,但顯然普通用戶不應該可以隨意訪問管理頁面或敏感資源,因此還需要有個模塊來確保只有授權(quán)的用戶才能執(zhí)行特定的操作,這個模塊稱之為鑒權(quán)或者授權(quán)(Authorization)。

          當你通過HttpSecurity.authorizeHttpRequests方法來配置請求的訪問權(quán)限控制時,就會自動添加鑒權(quán)的Security Filter:AuthorizationFilter,它是整個SecurityFilterChain的最后一個Filter。

          ?

          【版本兼容性】在Spring Security 6.0版本中,鑒權(quán)模塊發(fā)生了很大變化。以前的版本中,鑒權(quán)模塊使用FilterSecurityInterceptor,而6.0版本之后,這個被廢棄了,取而代之的是AuthorizationFilter。同時,還有一些相關(guān)的依賴組件,如AccessDecisionManagerAccessDecisionVoter也被AuthorizationManger替換了。因此,本節(jié)的內(nèi)容只限于6.0以及之后的版本。

          實現(xiàn)原理

          我們先看下鑒權(quán)模塊的入口,也就是AuthorizationFilterdoFilter方法:

          @Override
          public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
                  throws ServletException, IOException 
          {
              // ...其它非核心邏輯... //
              try {
                  AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request); // (1)
                  this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);
                  if (decision != null && !decision.isGranted()) { // (2)
                      throw new AccessDeniedException("Access Denied");
                  }
                  chain.doFilter(request, response);
              }
              finally {
                  request.removeAttribute(alreadyFilteredAttributeName);
              }
          }

          private Authentication getAuthentication() {
              Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication(); // (3)
              if (authentication == null) {
                  throw new AuthenticationCredentialsNotFoundException(
                          "An Authentication object was not found in the SecurityContext");
              }
              return authentication;
          }
          1. 這個方法本身很簡單,核心邏輯都委托給了AuthorizationManager。AuthorizationManager會校驗Authentication的權(quán)限,并返回鑒權(quán)的結(jié)果AuthorizationDecision。

          2. 如果當前認證用戶沒有訪問權(quán)限,就會拋出AccessDeniedException異常,表示拒絕訪問。

          3. 待校驗的Authentication是從Security Context獲取的,通常是在前面的認證階段設置的。在這里,實際上傳給AuthorizationManager的是一個獲取Authentication的方法,而不是Authentication本身,這樣就把實際的獲取操作延后到了真正進行授權(quán)的時候,這在某些場景下可以提高性能,比如permitAll,實際上它根本用不到Authentication。

          AuthorizationManager

          AuthorizationManager才是真正執(zhí)行鑒權(quán)邏輯的類,最常用的實現(xiàn)類是AuthorityAuthorizationManager,它的實現(xiàn)邏輯很簡單,它會調(diào)用AuthenticationgetAuthorities方法,獲取當前登錄用戶的權(quán)限列表,然后將這些權(quán)限與請求需要的權(quán)限進行匹配。

          實際上,選擇使用哪個AuthorizationManager是開發(fā)手動設置的。我們來分析一個常用的權(quán)限配置代碼片段:

          @Bean
          static SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {
              http.authorizeHttpRequests((requests) -> // (1)
                      requests
                              .requestMatchers("/admin").hasAuthority("ROLE_ADMIN"// (2)
                              .requestMatchers("/hello").hasRole("USER"// (3)
                              .anyRequest().authenticated()); // (4)
              // ... 其它配置 ... //
              return http.build();
          }
          1. 調(diào)用authorizeHttpRequests方法就相當于打開了鑒權(quán)模塊,它會注冊AuthorizationFilterSecurityFilterChain的最后。

          2. 對于匹配/admin的請求,要求有ROLE_ADMIN權(quán)限。hasAuthority的底層就是配置了一個要求ROLE_ADMIN權(quán)限的AuthorityAuthorizationManager對象。

          3. 對于匹配/hello的請求,要求有USER角色,等價于ROLE_USER權(quán)限。hasRole會自動在角色名稱前面加上前綴ROLE_。hasRole的底層就是配置了一個要求ROLE_USER權(quán)限的AuthorityAuthorizationManager對象。

          4. 對于其它的請求,只要通過身份認證就可以訪問,不需要特定的權(quán)限。類似的,authenticated方法的底層配置了一個AuthenticatedAuthorizationManager對象。

          在Spring Security中,很多初學者都容易混淆RoleAuthority的區(qū)別,實際上在技術(shù)實現(xiàn)層面上,這兩者沒有本質(zhì)區(qū)別,底層都僅僅是一個表示權(quán)限的字符串標識符。更多的區(qū)別在于權(quán)限管理的概念上,一般情況下,Authority表示細粒度的操作權(quán)限,比如ADD_USERDELETE_USER等,通常是動詞;而Role則會與實際業(yè)務角色想對應,比如管理員ADMIN,普通員工STAFF等,通常是名稱。此外,一般一個Role會對應多個Authority,同時角色之間可以存在繼承關(guān)系,比如ADMIN可以繼承STAFF的所有權(quán)限。

          我們來看下hasAuthority的源碼,以分析它是如何配置AuthorizeManager的:

          public AuthorizationManagerRequestMatcherRegistry hasAuthority(String authority) {
              return access( // (3)
                withRoleHierarchy( //(2)
                    AuthorityAuthorizationManager.hasAuthority(authority) // (1)
                )
              );
          }

          public static <T> AuthorityAuthorizationManager<T> hasAuthority(String authority) {
              Assert.notNull(authority, "authority cannot be null");
              return new AuthorityAuthorizationManager<>(authority);
          }

          public AuthorizationManagerRequestMatcherRegistry access(
                  AuthorizationManager<RequestAuthorizationContext> manager)
           
          {
              Assert.notNull(manager, "manager cannot be null");
              return AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager);
          }

          private AuthorityAuthorizationManager<RequestAuthorizationContext> withRoleHierarchy(           
          AuthorityAuthorizationManager<RequestAuthorizationContext> manager)
           
          {
              manager.setRoleHierarchy(AuthorizeHttpRequestsConfigurer.this.roleHierarchy.get());
              return manager;
          }
          1. AuthorityAuthorizationManager.hasAuthority方法簡單地創(chuàng)建了一個要求特定authority權(quán)限的AuthorityAuthorizationManager實例。

          2. withRoleHierarchy是一個裝飾器方法,它打開了角色繼承的功能。角色繼承允許一個角色繼承另一個角色的所有權(quán)限,從而簡化權(quán)限配置。

          3. 最后,access方法將這個AuthorityAuthorizationManager實例注冊到權(quán)限控制中。

          access方法是公開的,你可以自己實現(xiàn)一個AuthorizationManager,然后通過這個方法進行注冊。例如,我們可以提供一個拒絕所有請求的實現(xiàn):

          http.authorizeHttpRequests((requests) ->
              requests.anyRequest().access((authentication, object) -> null));
          ?

          【Tips】通過自定義AuthorizationManager,我們可以完全接管鑒權(quán)的邏輯,實現(xiàn)更加靈活和復雜的權(quán)限控制。

          基本架構(gòu)

          相比認證模塊,鑒權(quán)模塊不需要太多的靈活性和擴展性需求,因此它的架構(gòu)相對簡單。

          同樣,我們以一個標準的鑒權(quán)流程為例,來看整體的架構(gòu)和流程圖。

          1. 一個HTTP請求進來,經(jīng)過了一系列Security Filter后,最終來到AuthorizationFilter,進而調(diào)用AuthorizationManager#check方法進行權(quán)限校驗。

          2. 實際的校驗工作繼續(xù)委托給AuthoritiesAuthorizationManager

          3. AuthoritiesAuthorizationManager先從Security Context中獲取到Authentication對象(這個對象一般是前面的某個認證Filter設置的),然后基于其Authorites權(quán)限列表構(gòu)建GrantedAuthority列表,用于權(quán)限項的匹配。

          4. 最終會返回一個AuthorizationDecision表示權(quán)限校驗結(jié)果。

          總結(jié)

          本文重點分析了Spring Security的源碼和架構(gòu),幫助讀者理解其實現(xiàn)原理。由于篇幅有限,本文只覆蓋了身份認證和鑒權(quán)模塊的核心邏輯,很多特性沒有涉及,包括Session管理,Remember Me服務,異常分支和錯誤處理等等,不過有了上述的基礎知識,讀者完全可以自己分析源碼并深入理解這些特性。

          FAQ

          認證和鑒權(quán)失敗拋出的異常是如何處理的?

          當發(fā)生認證或鑒權(quán)失敗時,Spring Security有專門的Security Filter ExceptionTranslationFilter來捕獲并處理這些異常。如果是認證異常錯誤AuthenticationException及其子類,會觸發(fā)AuthenticationEntryPoint#commence方法,而如果是鑒權(quán)錯誤AccessDeniedException及其子類,則會觸發(fā)AccessDeniedHandler#handle方法。

          一個請求被Security拒絕了,應該如何Debug排查?

          如果遇到身份認證錯誤,建議直接Debug相關(guān)Filter的doFilter方法,比如Form表單登錄的Filter就是UsernamePasswordAuthenticationFilter;而如果是鑒權(quán)錯誤,可以從AuthorizationFilter開始Debug。

          但需要注意的是,出于安全考慮,Security相關(guān)的錯誤通常不會提供明確的錯誤信息,甚至不會顯示錯誤信息,而是直接跳轉(zhuǎn)到登錄頁面,比如CsrfFilter可能會導致這種情況。在這種情況下,可以從第一個Filter開始Debug,啟動日志搜索Will secure any request with,就可以找到所有Security Filter列表?;蛘咧苯訌娜肟?code style="color: rgb(53, 148, 247);line-height: 1.8em;letter-spacing: 0em;background: none 0% 0% / auto no-repeat scroll padding-box border-box rgba(27, 31, 35, 0.05);width: auto;height: auto;margin-left: 2px;margin-right: 2px;padding: 2px 4px;border-style: none;border-width: 3px;border-color: rgb(0, 0, 0) rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.4);border-radius: 4px;font-family: 'Operator Mono', Consolas, Monaco, Menlo, monospace;word-break: break-all;">FilterChainProxy#doFilter開始Debug。

          SecurityFilterChain的配置方法底層是如何實現(xiàn)的?

          SecurityFilterChain是通過HttpSecurity提供的一套DSL進行配置的。諸如formLogincsrf,authorizeHttpRequests等方法的邏輯都類似,參數(shù)都是一個lambda表達式,用于做各種自定義配置。而每個方法都會對應一個特定的配置類,比如FormLoginConfigurer,CsrfConfigurer等,在執(zhí)行HttpSecurity#build方法的時候,會調(diào)用這些配置類的configure方法,該方法的作用就是根據(jù)用戶的自定義配置,創(chuàng)建一個或者多個Security Filter,并將其注冊到SecurityFilterChain。

          此外,開發(fā)者還可以通過HttpSecurity.addFilter方法直接添加自定義的Security Filter。而對于復雜且有許多配置選項的Filter,也可以自定義SecurityConfigurerAdapter類,并通過HttpSecurity#apply方法來配置和注冊Filter。

          Spring Security Starter有哪些默認配置?

          Spring Security Starter默認配置在spring-boot-autoconfigure-x.x.x包下的文件META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports中可以找到。而具體的核心配置類有HttpSecurityConfigurationSpringBootWebSecurityConfiguration。

          Spring Security版本跟本文的不一樣,遇到問題如何排查?

          每次Spring Security升級,尤其是大版本升級,都可能引入破壞性或者不兼容的更新。不過,基于Filter和SecuiryFilterChain的框架和架構(gòu)通常是不會改變的。但是,通常會廢棄掉老的配置方法,引入新的配置,某些特定模塊的實現(xiàn)也有可能完全替換,比如6.0的鑒權(quán)模塊AuthorizationFilter就完全替換了老的鑒權(quán)模塊。

          你可以先從Security Filter列表開始排查,也可以通過入口FilterChainProxy#doFilter來Debug。

          Spring Security整體太復雜了,能不能不使用它,而完全自己實現(xiàn)?

          Security是個一個非常復雜的領(lǐng)域,很多開發(fā)者對其了解不深。使用Spring Security不僅提供了大部分的安全特性,還包含了很多安全領(lǐng)域的最佳實踐。自己從頭實現(xiàn)安全功能成本很高,并可能缺乏一些重要的安全特性。不過Spring Security的復雜設計以及頻繁的破壞性更新,的確給開發(fā)帶來了很大的學習成本和維護成本。

          Spring Security的架構(gòu)非常靈活,因此作者的建議是,不需要完全照搬整體框架,對于不同的應用類型和場景,可以選擇性地引入部分功能。比如Admin應用可以提供自定義的AuthenticationProvider,而API服務完全可以自定義Securiy Filter,只要維護好Security Context的Authentication,就可以很好的集成到Spring Security框架里,同時開發(fā)的學習和維護成本也能降到最低。

          瀏覽 141
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久久三级视频 | 大奶模特惜萍 | 国产精品国产三级国产在线观看 | 亚洲成色A片77777在线小说 | 黄色逼视频 |