一文帶你瞭解強大的 Spring Security 架構原理!

做者:before31
https://my.oschina.net/xuezi/...

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

本指南並非解決最基本問題的手冊(對於基本問題,有不少其餘可參考的資料),但對於初學者和專家均可能有用。 Spring Boot也被說起了不少次,由於它爲安全的應用程序提供了一些默認行爲,而且理解它與整個體系結構之間的關係會對你頗有幫助。全部這些原則一樣適用於不使用Spring Boot的應用程序。web

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

應用程序安全性差很少能夠歸結爲兩個獨立的問題:身份認證(你是誰)和受權(authorization)(你能夠作什麼?)。 有時人們會說「訪問控制」而不是「受權」,這可能會形成困惑,可是以這種方式思考可能會有所幫助,由於「受權」在其餘地方又有其餘含義。 Spring Security的體系結構旨在將身份認證與受權分開,而且具備許多策略和擴展點。spring

身份認證

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

public interface AuthenticationManager {

  Authentication authenticate(Authentication authentication)
    throws AuthenticationException;

}

在AuthenticationManager接口的authenticate()方法中能夠有3種處理狀況:後端

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

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

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沒法識別特定的身份驗證明例類型,則將跳過該類型。cookie

ProviderManager具備可選的父級,若是全部提供程序都返回null,則能夠諮詢該父級。 若是父級不可用,則空的Authentication將致使AuthenticationException。架構

有時,應用程序具備邏輯組的受保護資源(例如,與路徑模式/api/**匹配的全部Web資源),而且每一個組能夠具備本身的專用AuthenticationManager。 一般,每個都是ProviderManager,它們共享一個父級。 所以,父級是一種「全局」資源,充當全部providers的後備。app

使用ProviderManager的AuthenticationManager層次結構

定製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的配置,則一般能夠在「本地」對要保護的資源進行配置,而不要去關心全局的默認值。

受權或訪問控制(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。

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中顯式禁用了容器註冊的。

建立並自定義過濾器鏈

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以及惟一的順序和本身的請求匹配器。 若是匹配規則重疊,則最先的有序過濾器鏈將獲勝。

調度和受權之請求匹配(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時最容易犯的一個錯誤是忘記這些匹配器適用於不一樣的流程,一個是整個過濾器鏈的請求匹配器,另外一個是僅選擇要應用的訪問規則。

將應用程序安全規則與執行器規則相結合(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應用程序。

方法安全(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安全性和方法安全性並很多見。 過濾器鏈提供了用戶體驗功能,例如身份認證和重定向到登陸頁面等,而方法安全性在更精細的級別上提供了保護。

使用線程(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類時更具防護性)。

異步處理安全方法(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));
  }
}
關注公衆號《架構文摘》,天天一篇架構領域重磅好文,涉及一線互聯網公司應用架構(高可用、高性能、高穩定)、大數據、機器學習、Java架構等各個熱門領域。

相關文章
相關標籤/搜索