[轉]Spring Security架構


作者:before31
原文:https://my.oschina.net/xuezi/blog/3126351

 

  本指南是Spring Security的入門,它提供了對該框架的設計和基本構建的見解。我們僅介紹了應用程序安全性的最基本知識,但是這樣做可以解除使用Spring Security的開發人員所遇到的一些困惑。為此,我們會看一下使用過濾器(更通常是使用方法注釋)在Web應用程序中應用安全性的方式。當你需要從高層次了解安全應用程序的工作方式,如何自定義它,或者僅需要學習如何考慮應用程序安全性時,請使用本指南。

  本指南並不是解決最基本問題的手冊(對於基本問題,有很多其他可參考的資料),但對於初學者和專家都可能有用。Spring Boot也被提及了很多次,因為它為安全的應用程序提供了一些默認行為,並且理解它與整個體系結構之間的關系會對你很有幫助。所有這些原則同樣適用於不使用Spring Boot的應用程序。

 

1. 身份認證和訪問控制(Authentication and Access Control)

  應用程序安全性差不多可以歸結為兩個獨立的問題:身份認證(你是誰)和授權(authorization)(你可以做什么?)。有時人們會說“訪問控制”而不是“授權”,這可能會造成困惑,但是以這種方式思考可能會有所幫助,因為“授權”在其他地方又有其他含義。Spring Security的體系結構旨在將身份認證與授權分開,並且具有許多策略和擴展點。

1.1 身份認證

  認證的主要策略接口是AuthenticationManager,該接口只有一個方法:

public interface AuthenticationManager {

  Authentication authenticate(Authentication authentication) throws AuthenticationException;

}

  在AuthenticationManager接口的authenticate()方法中可以有3種處理情況:

  • 如果認證成功,則返回一個Authentication對象(通常將其authenticated屬性設置為true)。
  • 如果認證失敗,則拋出AuthenticationException異常。
  • 果無法判斷成功或失敗,則返回null。

  AuthenticationException是運行時異常。它通常由應用程序以通用方式處理,具體取決於應用程序的風格或目的。換句話說,通常不希望用戶代碼捕獲並處理它。例如,一個Web UI將呈現一個頁面,該頁面說明身份驗證失敗,而后端HTTP服務將發送401響應,是否攜帶WWW-Authenticate標頭則取決於上下文。

  AuthenticationManager最常用的實現是ProviderManager,它委派了AuthenticationProvider實例鏈。AuthenticationProvider有點像AuthenticationManager,但是它還有一個額外的方法,允許調用者查詢是否支持給定的Authentication類型:

public interface AuthenticationProvider {

    Authentication authenticate(Authentication authentication) throws AuthenticationException;

    boolean supports(Class<?> authentication);

}

  supports()方法中的Class<?>參數實際上是Class<? extends Authentication>(僅會詢問它是否支持將傳遞到authenticate()方法中的內容)。通過委派給AuthenticationProviders鏈,ProviderManager可以在同一應用程序中支持多種不同的身份驗證機制。如果ProviderManager無法識別特定的身份驗證實例類型,則將跳過該類型。

  ProviderManager具有可選的父級,如果所有提供程序都返回null,則可以咨詢該父級。如果父級不可用,則空的Authentication將導致AuthenticationException。

  有時,應用程序具有邏輯組的受保護資源(例如,與路徑模式/api/**匹配的所有Web資源),並且每個組可以具有自己的專用AuthenticationManager。通常,每一個都是ProviderManager,它們共享一個父級。因此,父級是一種“全局”資源,充當所有providers的后備。

  使用ProviderManager的AuthenticationManager層次結構

 

 

 

1.2 定制Authentication Managers

  Spring Security提供了一些配置助手,可以快速獲取在應用程序中設置的通用Authentication Managers功能。最常用的幫助程序是AuthenticationManagerBuilder,它非常適合設置內存中、JDBC或LDAP用戶詳細信息,或添加自定義UserDetailsService。這是配置全局(父)AuthenticationManager的應用程序的示例:

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

    ... // web stuff here
 
    @Autowired
    public void initialize(AuthenticationManagerBuilder builder, DataSource dataSource) {
        builder.jdbcAuthentication().dataSource(dataSource).withUser("dave").password("secret").roles("USER");
    }

}

  此示例與Web應用程序有關,但是AuthenticationManagerBuilder的用法更為廣泛(有關如何實現Web應用程序安全性的詳細信息,請參見下文)。注意,AuthenticationManagerBuilder是@Autowired到@Bean中的方法中的-這就是使它構建全局(父)AuthenticationManager的原因。相反,如果我們這樣做的話:

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

     @Autowired
     DataSource dataSource;

     ... // web stuff here

     @Override
     public void configure(AuthenticationManagerBuilder builder) {
         builder.jdbcAuthentication().dataSource(dataSource).withUser("dave").password("secret").roles("USER");
     }

}

  (在配置程序中使用方法的@Override),那么AuthenticationManagerBuilder僅用於構建“本地” AuthenticationManager,它是全局管理器的子級。在Spring Boot應用程序中,你可以使用@Autowired將全局的管理器注入到另一個bean,但是除非你自己顯式公開“本地”管理器,否則不能對本地管理器執行此操作。

  Spring Boot提供了一個默認的全局AuthenticationManager(只有一個用戶),除非你通過提供自己的AuthenticationManager類型的bean來搶占它。除非你主動需要自定義全局AuthenticationManager,否則默認值本身就足夠安全,你不必擔心太多。如果執行任何構建AuthenticationManager的配置,則通常可以在“本地”對要保護的資源進行配置,而不要去關心全局的默認值。

 

2. 授權或訪問控制(Authorization or Access Control)

  身份認證成功以后,我們接下來討論授權,這里的核心策略是AccessDecisionManager。該框架提供了三種實現,所有這三種實現都委托給AccessDecisionVoter鏈,有點像ProviderManager委托給AuthenticationProviders。

  AccessDecisionVoter會考慮Authentication(表示一個主體)和被ConfigAttributes修飾的安全Object:

boolean supports(ConfigAttribute attribute);

boolean supports(Class<?> clazz);

int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);

  Object在AccessDecisionManager和AccessDecisionVoter的簽名中是完全通用的-它表示用戶可能要訪問的任何內容(Web資源或Java類中的方法是兩種最常見的情況)。ConfigAttributes也相當通用,用一些元數據來表示安全Object的修飾,這些元數據確定訪問它所需的權限級別。ConfigAttribute是一個接口,但是它只有一個非常通用的方法並返回String,因此這些字符串以某種方式編碼資源所有者的意圖,表達有關允許誰訪問它的規則。典型的ConfigAttribute是用戶角色的名稱(如ROLE_ADMIN或ROLE_AUDIT),並且它們通常具有特殊的格式(如ROLE_前綴)或表示需要求值的表達式。

  大多數人只使用默認的AccessDecisionManager,它是AffirmativeBased的,即任何選民(voter)返回允許,則將授予訪問權限。任何定制都傾向於在選民中發生,要么增加新選民,要么修改現有選民的工作方式。

  使用SpEL(Spring表達式語言)表達式的ConfigAttribute非常常見,例如isFullyAuthenticated()&& hasRole('FOO')。AccessDecisionVoter支持此功能,可以處理表達式並為其創建上下文。為了擴展可以處理的表達式的范圍,需要SecurityExpressionRoot的自定義實現,有時還需要實現SecurityExpressionHandler。

 

3. Web安全

  Web層(用於UI和HTTP后端)中的Spring Security基於Servlet過濾器,因此通常首先了解過濾器的作用會很有幫助。下圖顯示了單個HTTP請求的處理程序的典型分層。

 

 

  客戶端向應用程序發送請求,然后容器根據請求URI的路徑確定對它應用哪些過濾器和哪個servlet。一個servlet最多只能處理一個請求,但是過濾器形成一個鏈,因此它們是有序的,實際上,如果某個過濾器要自己處理該請求,則其可以否決鏈的其余部分。過濾器還可以修改下游過濾器和Servlet中使用的請求和/或響應。過濾器鏈的順序非常重要,Spring Boot通過兩種機制對其進行管理:一種是Filter類型的@Bean可以具有@Order或實現Ordered,另一種是它們可以成為FilterRegistrationBean本身的一部分,該Bean本身就維持了順序。一些現成的過濾器定義了自己的常量,以幫助表明它們相對彼此的順序(例如,Spring Session中的SessionRepositoryFilter擁有的DEFAULT_ORDER值為Integer.MIN_VALUE + 50,這告訴我們此過濾器需要在過濾器鏈中比較靠前,但也並不阻止其他過濾器更靠前一些)。

  Spring Security是作為鏈中的單個Filter安裝的,其隱秘類型為FilterChainProxy,原因很快就會變得顯而易見。在Spring Boot應用程序中,安全過濾器是ApplicationContext中的@Bean,默認情況下會安裝它,以便將其應用於每個請求。它安裝在SecurityProperties.DEFAULT_FILTER_ORDER定義的位置,該位置又由FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER錨定(Spring Boot應用程序希望過濾器包裝請求並修改其行為時期望的最大順序)。但是,還有更多要說的:從容器的角度來看,Spring Security是一個過濾器,但是在內部有其他過濾器,每個過濾器都扮演着特殊的角色。這是一張圖:

  Spring Security是單一Filter,但將處理邏輯委托給一系列內部過濾器。

 

  實際上,安全過濾器中甚至還有一層間接層:它通常作為DelegatingFilterProxy安裝在容器中,而不必是Spring @Bean。代理委托給一個始終為@Bean的FilterChainProxy,通常使用固定名稱springSecurityFilterChain。它是FilterChainProxy,它包含所有內部安全邏輯,這些安全邏輯在內部排列為一個或多個過濾器鏈。所有過濾器都具有相同的API(它們都實現了Servlet規范中的Filter接口),並且它們都有機會否決該鏈的其余部分。

  在同一頂級FilterChainProxy中可以有多個由Spring Security管理的過濾器鏈,而對於容器來說都是未知的。Spring Security過濾器包含一個過濾器鏈列表,並向與其匹配的第一個鏈調度(dispatch)一個請求。下圖顯示了基於匹配請求路徑(/foo/*在/*之前匹配)進行的調度。這是很常見的,但不是匹配請求的唯一方法。此調度過程的最重要特征是,只有一個鏈可以處理請求。

 

  Spring SecurityFilterChainProxy將請求調度到匹配的第一個鏈。

  一個Spring Boot應用程序,如果沒有自定義安全配置,會擁有多個(稱為n)過濾器鏈,通常n = 6。前(n-1)個鏈僅用於忽略靜態資源,例如/css/和/images/,以及錯誤視圖/error(路徑可以由SecurityProperties配置bean中的security.ignored控制)。最后一個鏈與所有路徑/**匹配,並且更活躍,包含用於身份認證、授權、異常處理、會話處理、標頭寫入等邏輯。默認情況下,該鏈中共有11個過濾器,但通常情況下用戶不必關心使用了哪個過濾器以及何時使用。

  注意:容器不知道Spring Security內部的所有過濾器這一事實很重要,尤其是在Spring Boot應用程序中,默認情況下,所有Filter類型的@Beans都會自動向容器注冊。因此,如果要向安全鏈中添加自定義過濾器,請不要使其成為@Bean,或者干脆將其包裝在FilterRegistrationBean中,在該Bean中顯式禁用了容器注冊的。

 

4. 創建並自定義過濾器鏈

  Spring Boot應用程序中的默認后備過濾器鏈(匹配/**請求的那個鏈)具有SecurityProperties.BASIC_AUTH_ORDER的預定義順序。你可以通過設置security.basic.enabled = false完全關閉它,也可以將其用作后備方式,而只是以較低的順序定義其他規則。為此,只需添加類型為WebSecurityConfigurerAdapter(或WebSecurityConfigurer)的@Bean並使用@Order注解。例如:

@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
       http.antMatcher("/foo/**")
       ...;
    }
}

  這個bean將使Spring Security添加一個新的過濾器鏈並將其放置在后背過濾器鏈之前。

  許多應用程序對一組資源的訪問規則可能完全不同於另外一組。例如,對於一個承載了UI並且也支持API的應用程序,可能其UI部分需要支持基於cookie(cookid-based)的身份認證以及對登錄頁面的重定向,而其API部分則需要支持基於令牌(token-based)的身份認證以及對未經身份認證的請求的401響應。每組資源都有其自己的WebSecurityConfigurerAdapter以及唯一的順序和自己的請求匹配器。如果匹配規則重疊,則最早的有序過濾器鏈將獲勝。

 

5. 調度和授權之請求匹配(Request Matching for Dispatch and Authorization)

  安全過濾器鏈(或等效的WebSecurityConfigurerAdapter)具有請求匹配器,該請求匹配器用於確定是否將其應用於HTTP請求。一旦決定應用特定的過濾器鏈,就不再應用其他過濾器鏈。但是在過濾器鏈中,可以通過在HttpSecurity配置器中設置其他匹配器來對授權進行更精細的控制。例如:

@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/foo/**")
          .authorizeRequests()
          .antMatchers("/foo/bar").hasRole("BAR")
          .antMatchers("/foo/spam").hasRole("SPAM")
          .anyRequest().isAuthenticated();
    }
}

  配置Spring Security時最容易犯的一個錯誤是忘記這些匹配器適用於不同的流程,一個是整個過濾器鏈的請求匹配器,另一個是僅選擇要應用的訪問規則。

 

6. 將應用程序安全規則與執行器規則相結合(Combining Application Security Rules with Actuator Rules)

  如果你使用了Spring Boot執行器(Actuator),則可能希望它的端點(endpoint)是安全的,默認情況下確實將是安全的。實際上,將執行器添加到安全應用程序后,你會獲得一條僅適用於執行器端點的附加過濾器鏈。它由僅匹配執行器端點的請求匹配器來定義,並且其順序為ManagementServerProperties.BASIC_AUTH_ORDER,該順序比默認的SecurityProperties后備過濾器的順序小5,因此會在后備過濾器處理之前請執行。

  如果你希望將應用程序安全規則應用於執行器端點,則可以添加一個比執行器過濾器鏈順序更早的過濾器鏈,並且使之帶有包括所有執行器端點的請求匹配器。如果你更傾向於使用執行器端點的默認安全設置,那么最簡單的方法是在執行器端點之后但在后備過濾器鏈之前(例如ManagementServerProperties.BASIC_AUTH_ORDER +1)添加自己的過濾器。例如:

@Configuration
@Order(ManagementServerProperties.BASIC_AUTH_ORDER + 1)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/foo/**")
        ...;
    }
}

  注意:Web層中的Spring Security當前與Servlet API綁定,因此僅當在Servlet容器中運行應用程序時才真正適用,不管是嵌入式的容器還是獨立式的容器。但是,它不依賴於Spring MVC或Spring Web棧的其它部分,因此可以在任何servlet應用程序中使用,例如使用JAX-RS的servlet應用程序。

 

7. 方法安全(Method Security)

  除了支持Web應用程序安全外,Spring Security還支持將訪問規則應用於Java方法執行。對於Spring Security,這只是另一種類型的“受保護資源”。對於用戶來說,這意味着使用相同的ConfigAttribute字符串格式(例如角色或表達式)來聲明訪問規則,但在代碼中的不同位置。第一步是啟用方法安全性,例如在應用程序的頂級配置中:

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {
}

  然后我們就可以直接修飾方法資源,例如:

@Service
public class MyService {

   @Secured("ROLE_USER")
   public String secure() {
      return "Hello Security";
   }
}

  此示例是一種使用安全方法的服務。如果Spring創建了這種類型的@Bean,則它將被代理,並且在實際執行該方法之前,調用者必須經過安全攔截器。如果訪問被拒絕,則調用者將得到AccessDeniedException異常,而不是實際的方法結果。

  方法上還可以使用其他注解來強制執行安全性約束,特別是@PreAuthorize和@PostAuthorize,它們可以使你編寫分別包含對方法參數和返回值的引用的表達式。

  小貼士:結合使用Web安全性和方法安全性並不少見。過濾器鏈提供了用戶體驗功能,例如身份認證和重定向到登錄頁面等,而方法安全性在更精細的級別上提供了保護。

 

8. 使用線程(Working with Threads)

  Spring Security從根本上講是線程綁定的,因為它需要使當前經過身份驗證的主體可供各種下游使用者使用。基本構件是SecurityContext,它可以包含一個Authentication(當用戶登錄成功以后,它將被顯式注明為authenticated)。你始終可以通過SecurityContextHolder中的靜態方法訪問和操作SecurityContext,而該方法里面其實是簡單地操作TheadLocal,例如:

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
assert(authentication.isAuthenticated);

  用戶應用程序代碼執行此操作並不常見,但是如果在你需要編寫自定義身份認證過濾器時可能會很有用(盡管即使如此,Spring Security中也可以使用基類來避免使用SecurityContextHolder)。

  如果需要訪問Web端點中當前已認證的用戶,則可以在@RequestMapping中使用方法參數。例如。

@RequestMapping("/foo")
public String foo(@AuthenticationPrincipal User user) {
      ... // do stuff with user
}

  該注解將當前的Authentication從SecurityContext中取出,並調用其getPrincipal()方法以產生方法參數。Authentication中的主體(Principal)的類型取決於用於身份認證的AuthenticationManager,因此這也是一個獲得對用戶數據的類型安全引用的有用的小技巧。

  如果使用Spring Security,則HttpServletRequest中的Principal的類型將為Authentication,因此你也可以直接使用它:

@RequestMapping("/foo")
public String foo(Principal principal) {
     Authentication authentication = (Authentication) principal;
     User = (User) authentication.getPrincipal();
     ... // do stuff with user
}

  如果你需要編寫在不使用Spring Security的情況下仍可以正常工作的代碼,那么這就會很有用(你需要在加載Authentication類時更具防御性)。

 

9. 異步處理安全方法(Processing Secure Methods Asynchronously)

  由於SecurityContext是線程綁定的,因此,如果要執行任何調用安全方法的后台處理,例如使用@Async,你需要確保傳播該SecurityContext。簡單歸結起來就是,要將SecurityContext與在后台執行的任務(Runnable,Callable等)包裝在一起。Spring Security提供了一些幫助程序,例如Runnable和Callable的包裝器,可以使上述操作變得簡單。要將SecurityContext傳播到@Async方法,你需要提供AsyncConfigurer並確保Executor具有正確的類型:

@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {

    @Override
    public Executor getAsyncExecutor() {
        return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM