Spring Security Architecture(Spring安全框架的體系結構)

1. Introduction(簡介)

本篇是關於Spring安全框架的入門指導,主要講解Spring 安全框架的體系結構,設計思路和組成模塊。雖然本文只涵蓋了最爲基本的應用安全知識,但這些足以幫助開發者消除在使用Spring 安全框架進行開發時遇到的一些困惑。爲了完成這些工作,我們來瞧一瞧如何通過Filters(Servlet規範中一種組件)以及更爲常用的方法註解來在Web應用中使用安全組件。
如果你需要在更高的層次上理解一個安全的應用是如何工作的,或者你想知道如何定製化應用的安全組件,或者你僅僅只是想要了解一下應用安全方面的知識,那麼,都可以通過閱讀本篇指導獲取你想要的。但是本篇指導並沒有打算去說明或者解決超出基本安全範圍的問題或者需求(這些工作由其他的指導來完成),但對於一個關於應用安全的初學者來說,這篇指導是非常有用的。這篇指導有很大的篇幅涉及到了Spring Boot,這是因爲Spring Boot默認爲應用的安全提供了一些支持,這對於我們理解Spring安全框架是如何適配整個Spring體系結構是有幫助的。所有這些適用於Spring Boot應用的方式或者方法,同樣適用於那些使用了Spring框架的其他形式的Web應用程序。

2. Authentication(認證) & Access Control(訪問控制)

應用程序的安全問題或多或少可以歸納爲兩個相互獨立的基本問題:認證(Authentication,解決身份識別問題,即識別用戶身份是否合法)和授權(Authorization ,解決訪問權限問題,即允許用戶做什麼)。有些人使用"access control(訪問控制)"來代替"authorization(授權)",雖然這兩種說法會給用戶帶來一些困惑,但由於"authorization(授權)"這個詞在有些地方被過度的解釋了,這導致"access control(訪問控制)"這種說法更有助於我們理解這種控制用戶的訪問權限的方式。Spring安全框架的體系結構在設計的時候就將認證(authentication)從授權(authorization)中分離出來,並且設計了一些策略能夠對這兩者進行擴展。

2.1 Authentication(認證)

Spring安全框架中,爲認證(Authentication)設計的主要策略接口是org.springframework.security.authentication.AuthenticationManager ,而這個接口只有一個方法:

public interface AuthenticationManager {

  Authentication authenticate(Authentication authentication)
    throws AuthenticationException;
}

AuthenticationManagerauthenticate()方法中,用戶可以做三件事情:

  1. 如果可以確認輸入的參數authentication(是org.springframework.security.core.Authentication的實例對象)代表一個合法的用戶身份,那麼返回另一個org.springframework.security.core.Authentication實例對象,這個返回的對象通常會帶有一個authenticated=true的標記。
  2. 如果可以確認輸入的參數authentication代表一個非法的用戶身份,那麼將拋出一個org.springframework.security.core.AuthenticationException異常。
  3. 如果無法判斷輸入的參數authentication是否是一個合法的用戶身份,可以返回null值。

org.springframework.security.core.AuthenticationException是一個運行時異常。一般情況下,該異常會被應用程序使用專門的處理器進行處理,而如何處理取決於應用程序的形式和用途。換句話說,一般情況下,應用程序並不指望開發者編寫代碼去捕獲和處理這些異常,而是提供默認的策略,由應用程序自身來處理這個異常,比如,應用程序會提供一個界面同時渲染一個頁面來告訴用戶認證失敗,同時後臺的HTTP服務也將會發送401狀態碼,也會根據應用的上下文環境來決定是否攜帶WWW-Authenticate頭部。
最常用的org.springframework.security.authentication.AuthenticationManager實現類是org.springframework.security.authentication.ProviderManager,該類維護了一個由接口org.springframework.security.authentication.AuthenticationProvider的實現類的實例所組成的列表。而org.springframework.security.authentication.AuthenticationProviderorg.springframework.security.authentication.AuthenticationManager相似,區別在於org.springframework.security.authentication.AuthenticationProvider內部包含另一個方法允許調用者測試是否支持傳入的org.springframework.security.core.Authentication實現類的類型:

public interface AuthenticationProvider {

    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
    
    boolean supports(Class<?> authentication);
}

supports()方法中的Class<?>參數的真正類型是Class<? extens Authentication>,主要是用來測試是否支持傳入authenticate()方法的authentication參數的類型。由於ProviderManager代理了一個AuthenticationProviders鏈,所以可以在同一個應用中支持多種不同的認證機制。一般情況下,ProviderManager會跳過那些自己不支持的Authentication的實例類型。
一個ProviderManager有一個可選的父級provider,如果所有的providers都返回的是null,也就是說所有的認證機制都無法確定當前的用戶身份是合法的,最終將由這個父級(或者全局)的provider來決定,如果不存在這個父級的provider也會返回null,最終會拋出AuthenticationException
有時候,應用將被保護的資源按照一定的規則分成邏輯上的分組(比如,所有的web資源都通過路徑來分組,即將資源按照linux目錄樹的形式進行分組),並且每一個分組都擁有專屬的AuthenticationManager,而這些AuthenticationManager通常都是一個ProviderManager,這些AuthenticationManager共享一個父級(或者全局)的AuthenticationManager,這個父級的AuthenticationManager作爲所有的providers的替補而存在。

2.2 Customizing Authentication Managers(自定義認證管理器)

Spring安全框架提供了一些配置的輔助類,能夠在應用中快速的創建常用的認證管理功能。最常用的輔助類就是org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder,該類主要用來設置獲取用戶信息的方式,有三種方式,分別是in-memory,JDBC或者LDAP,也可以添加實現了org.springframework.security.core.userdetails.UserDetailsService接口的類對象來設置獲取用戶詳情的方式。
下面的例子展示瞭如何在一個應用中配置全局的AuthenticationManager

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

   ... // web stuff here

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

}

這是一個web應用的例子,AuthenticationManagerBuilder的使用方式有很多種,而在這個例子中AuthenticationManagerBuilder的一個實例被應用作爲參數通過@Autowired註解傳入initialize()方法中,在這個方法中將會創建一個全局(父級)的AuthenticationManager。我們可以和另外一種使用方式進行對比:

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

  @Autowired
  DataSource dataSource;

   ... // web stuff here

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

}

上例中使用@Override註解,覆蓋了父類的configure()方法,在這個方法中,AuthenticationManagerBuilder的實例只是被該方法的調用者使用來創建一個局部的AuthenticationManager,這個局部的AuthenticationManager是全局AuthenticationManager的孩子。在一個Spring Boot應用中,我們可以使用@Autowired註解將全局的AuthenticationManager注入到其他的bean中,但是無法將一個局部的AuthenticationManager注入到其他的bean中,除非我們通過配置@Bean的方式明確的將該局部的AuthenticationManager的實例作爲一個組件發佈出去。
Spring Boot提供了一個默認的全局AuthenticationManager,我們可以通過提供自己的AuthenticationManager組件來替換他。這個默認的AuthenticationManager組件是足夠安全的,我們無需對他有過多的擔心,除非你確實需要一個自定義的AuthenticationManager。如果我們做了一些配置創建了一個AuthenticationManager組件,我們可以將該組件應用到局部的受保護資源上而無需擔心全局的AuthenticationManager

前文說的全局的provider,是ProviderManager組件,ProviderManagerAuthenticationManager最常用的一個實現類,所以全局的provider就是全局的AuthenticationManager組件。

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

一旦用戶成功通過了認證,我們就可以開始關注授權問題,核心的授權策略由接口org.springframework.security.access.AccessDecisionManager來提供。Spring安全框架提供這個接口的三種實現,每一種都可以委託給一個org.springframework.security.access.AccessDecisionVoter<S>鏈,這與ProviderManager委託給AuthenticationProviders有一點類似。
一個AccessDecisionVoter主要用來處理代表用戶身份的Authentication對象和一個由ConfigAttributes描述的安全對象:

boolean supports(ConfigAttribute attribute);

boolean supports(Class<?> clazz);

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

這個安全對象指的是參數object,其類型就是AccessDecisionVoter<S>的泛型參數S,他代表任何用戶想要訪問的目標(Web資源或Java類的方法是該對象的兩種最常見的形式)。ConfigAttributes指的是參數attributes,他是org.springframework.security.access.ConfigAttribute的集合,保存了安全對象的元數據,而這些元數據確定了訪問該安全對象所要求的權限。ConfigAttribute是隻有一個方法的接口,而這個方法返回一個String類型的值,這個值通常情況下是一串編碼,表示資源所有者制定的資源訪問規則。典型的ConfigAttribute應用方式就是返回表示用戶角色的字符串,比如(ROLE_ADMIN或者ROLE_AUDIT),這些都有統一的格式(比如以ROLE_爲前綴),而另外一種應用方式是返回能夠用來執行的表達式字符串。
絕大多開發者只使用默認的AccessDecisionManager組件,默認的AccessDecisionManager的機制是如果得票數沒有下降,那麼訪問就應該被允許。因此所有的定製化開發都傾向於發生在投票者那裏,要麼是增加新的投票者,要麼改變已有投票者的行爲。
最常用的ConfigAttributes是Spring的EL表達式,例如isFullyAuthenticated() && hasRole('FOO')。一種AccessDecisionVoter組件就支持Spring的EL表達式,他不但能夠執行這些表達式,同時還能爲他們創建上下文環境。如果想要擴展表達式的語法,可以實現org.springframework.security.access.expression.SecurityExpressionRoot抽象類或者實現org.springframework.security.access.expression.SecurityExpressionHandler<T>接口。

3. Web Security(Web安全)

3.1 Web Security基本組件

Spring安全框架在Web層的組件都是基於Servelt規範中的Filters,所以事先弄明白Filters所扮演的角色對與我們理解Web安全是有幫助的。下面的圖片展示了Http請求處理器的層級結構。

客戶端發送請求到應用,容器決定哪些fitlers以及哪一個servlet可以用來處理這次請求。絕大數情況下,一個servlet只能處理一種請求,但是這些filters組成了一個鏈,他們按照一定的順序排列,如果某一個filter想要處理這個請求,那麼他可以將這個請求攔截下來,並且進行處理。一個filter也能夠改變request請求或者response回覆。Filter鏈的順序是非常重要的,Spring Boot有兩種機制在管理filter的順序 ,一種是在使用@Bean註解發佈一個Filter的時候,同時使用@Order註解來指定這個filter的優先級(優先級是由一個int類型的整數表示,數值越大,優先級越高),或者讓這個Filter直接實現org.springframework.core.Ordered接口,通過getOrder()方法返回優先級數值,另一種方法是使用org.springframework.boot.web.servlet.FilterRegistrationBean註冊Filter的時候,使用他的相關API來指定要註冊的Filter的優先級。一些標準的filters通過定義一些常量值來確定他們之間的順序(比如Spring Session框架中的SessionRepositoryFilter組件,默認的優先級數值由其自身定義的常量DEFAULT_ORDER來表示,其值爲Integer.MIN_VALUE + 50,這個值只比int型整數的最小值大一點,因此這個過濾器幾乎是排在過濾器鏈的最下面,要到達這裏,必須先通過其他過濾器)。
Spring安全的核心組件就是一個安裝到這個過濾器鏈中的Filter,他的具體類型是org.springframework.security.web.FilterChainProxy,稍後我們將會詳細說明這個安全過濾器。在一個Spring Boot應用中,安全過濾器(security filter)是ApplicationContext的一個@Bean,一旦開啓了Spring安全功能,就會默認安裝這個安全過濾器,並且攔截所有的請求。安全過濾器在過濾器鏈中的位置由org.springframework.boot.autoconfigure.security.SecurityProperties.DEFAULT_FILTER_ORDER表示的優先級數值來決定,這個值位於錨點org.springframework.boot.web.servlet.FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER的下方(這個值是Spring Boot應用中最大的過濾器優先級數值,因爲Spring Boot希望請求在通過整個處理流程之前,先被這個過濾器包裝一下,改變一下行爲)。從下圖我們可以看到,Spring安全框架所提供的功能由單個Filter來提供,但是在這個Filter中,包含着多個內部filters,並且每一個都具有特定的功能。圖片如下:

在Spring應用中,過濾器通常是安裝在類型爲org.springframework.web.filter.DelegatingFilterProxy的代理容器中,這種容器並不以Spring的@Bean的形式存在,而是作爲原生的Servlet規範中的Filter組件安裝到Servlet容器中。Spring安全的過濾器組件就是安裝在這種代理容器中,是一個類型爲org.springframework.security.web.FilterChainProxy且具有固定名字springSecurityFilterChain的過濾器,這個安全過濾器是以Spring@Bean的形式存在的。而springSecurityFilterChain過濾器又包含了一個封裝了安全邏輯的有序過濾器鏈,組成這個鏈的過濾器都有相同的API(通常是實現了Servlet規範中Filter接口)並且每一個過濾器都可能將請求攔截到自己這一層並進行處理。所以,事實上的安全層可不止一層。
當然,springSecurityFilterChain也可能會管理多個不同的過濾器鏈,也就是包含一個過濾器鏈的列表,並且,所有這些過濾器對容器都是透明的。並且springSecurityFilterChain會將請求派發給第一個匹配的過濾器鏈。下圖展示了基於請求路徑的派發過程(這也是使用最多但並不唯一的方式)。這種派發過程的最重要的特點就是有且只有一個過濾器鏈來處理這個請求。

一個沒有任何定製化配置的Spring Boot應用具有6個過濾器鏈,前5個過濾器鏈只會忽略那些指向靜態資源的路徑,例如/css/**/images/**,以及用來展示錯誤信息視圖的路徑/error(這些忽略的路徑可以在SecurityProperties配置bean中,使用security.ignored來控制)。最後一個過濾器鏈匹配所有的路徑/**並且也是最活躍的,包含認證和授權的邏輯,錯誤處理,會話處理,頭部信息處理等。在這些默認的過濾器鏈中有總共11個過濾器,通常情況下,用戶無需去關心哪個過濾器被使用了以及是什麼時候使用的。

注意
Spring安全的所有內部過濾器對於容器來說都是透明的,這很重要,特別是Spring Boot應用,因爲所有Filter類型的@Bean都是由容器自動註冊的。所以如果你想要添加自定義的安全過濾器到Spring安全的過濾器鏈中,那麼你最好不要通過配置Filter類型的@Bean的方式來添加,因爲這樣Spring應用會把過濾器註冊到容器中而不是添加到Spring安全的過濾器鏈中,你可以通過將自定義的安全過濾器封裝在FilterRegistrationBean中來達成目的。

3.2 Creating and Customizing Filter Chains(創建和定製過濾器鏈)

在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/**")
     ...;
  }
}

上例中的@Configurationbean將會使Spring安全框架添加一個優先級排在後備過濾器鏈前面的新過濾器鏈。

許多的應用程序擁有訪問規則互不相同的資源組。例如:一個應用程序提供的資源包括用戶UI和後臺API接口兩個部分,對於用戶UI,支持基於Cookie的認證,而未認證的請求會重定向至登陸頁面;而對於後臺API接口,則支持基於令牌的認證,未認證的請求會收到攜帶401狀態碼的回覆。每一個資源組都有他自己的WebSecurityConfigurerAdapter,並且具有唯一的優先級以及請求路徑的匹配規則。如果匹配規則發生重疊,那麼優先級更高的過濾器鏈將會勝出。

3.3 Request Matching for Dispatch and Authorization(針對派發和授權的請求匹配)

一個安全過濾器鏈(等同與一個WebSecurityConfigurerAdapter)持有一個請求匹配器,這個匹配器被用來決定該過濾器鏈是否適用於當前的HTTP請求。一旦一個HTTP請求適用與一個特定的過濾器鏈,其他的過濾器鏈則不會被應用於這個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安全的時候最容易犯的一個錯誤就是忘記了這些匹配器將應用於不同的程序,一個是請求匹配器,將應用於整個過濾器鏈,而其他的匹配器僅僅是用來選擇訪問規則。

3.4 Combining Application Security Rules with Actuator Rules(應用程序的安全規則與監控規則的整合)

如果你在使用Spring Boot Actuator來監控應用程序的端點(即由path所指向的資源),你應該希望他們是安全的並且默認他們是安全的。實際上,當你將Spring Boot監控功能添加到一個安全的應用程序中時,同時會添加一個過濾器鏈,而這個過濾器鏈只會攔截訪問Spring Boot監控端點路徑的請求。這個過濾器鏈定義了一個請求匹配器,這個匹配器只匹配監控端點路徑,並且具有一個值爲ManagementServerProperties.BASIC_AUTH_ORDER的優先級,這個優先級只比默認的SecurityProperties替補過濾器高一點(數值小5),所以匹配監控端點路徑的請求會先到達這個過濾器鏈。
如果你想要你的應用程序的安全規則應用到監控功能端點上,你可以添加一個優先級高於監控端點過濾器鏈的新的過濾器鏈,並且讓這個新過濾器攔截所有訪問監控端點的請求。如果你更傾向於對監控端點使用默認的安全配置,那麼最簡單的做法就是將你自己的過濾器鏈添加到監控接口過濾器的後面和替補過濾器的前面。示例如下:

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

注意
Spring安全框架在Web層與Servlet的API是綁定的,所以現階段,Spring安全框架只能應用於基於Servlet規範,運行在Servlet容器中的應用程序,而無論容器是否是嵌入式的。當然,Spring安全框架並沒有與Spring MVC框架或者Spring的Web技術棧綁定,所以他能夠應用於所有基於Servlet規範的應用程序。

4. Method Security(方法安全)

Spring安全框架不僅僅支持Web應用程序,也爲Java方法的執行提供安全的訪問規則支持。對於Spring安全來說,Java方法只是一種其他形式的“受保護資源”。這就意味着方法的訪問規則是與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安全和方法安全結合起來使用並非是不常用的。Web安全過濾器鏈提供用戶粒度的安全功能,例如認證和重定向到登陸頁面,而方法安全能夠提供更細粒度的安全保證。

5. Working with Threads(工作線程)

Spring安全本身就是一個基本的線程邊界,因爲當前身份被認證後,仍然需要被各種下游消費者所使用。最基本的構造塊是org.springframework.security.core.context.SecurityContext的實例對象,他包含了一個Authentication對象(並且如果用戶已經登陸,那麼這個Authentication是被明確標記爲authenticated的)。你可以通過org.springframework.security.core.context.SecurityContextHolder的靜態方法方便的訪問和使用保存在ThreadLocal中的SecurityContext實例。例如:

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

雖然對於用戶應用程序來說,上面的代碼並不常用,但這並不代表他沒有用處,相反對於開發者來說,這段代碼非常有用,比如在開發者想要定製化編寫一個認證過濾器的時候(儘管認證過濾器在Spring安全中是最基本的類,開發者編寫該類的時候應該儘量避免使用SecurityContextHolder).
如果你想要在一個Web接口中訪問當前的已認證用戶,你可以在@RequestMapping使用方法參數註解@AuthenticationPrincipal來注入持有用戶信息的對象。例如:

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

@AuthenticationPrincipal註解會SecurityContext實例中抽取當前Authentication對象並且調用其getPrincipal()方法來獲取用戶身份對象,然後將用戶身份對象注入到方法參數中。Authentication持有的Principal(用戶身份)的類型取決於AuthenticationManager驗證認證時所使用的類型。
如果Spring安全從HttpServletRequest中獲取的Principal就是Authentication類型的,那麼開發者可以用這種方式來直接使用:

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

5.1 Processing Secure Methods Asynchronously(以異步的方式處理安全的方法)

自從SecurityContext成爲線程邊界後,如果你想要調用安全的方法做一些異步的後臺操作,比如使用@Async註解,那麼你需要確保這個上下文是可傳播的。說白了就是將SecurityContext封裝到task(RunnableCallable等)中,然後交給後面的線程去執行。Spring安全提供了一些輔助類來幫助開發者更方便地完成這個過程。當然,爲了傳播SecurityContext@Async方法中,開發者需要提供一個AsyncConfigurer同時要確保Executor的正確類型:

@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {
  @Override
  public Executor getAsyncExecutor() {
    return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章