[譯]Spring Security Architecture

原文css

也能夠在個人博客上查看前端

雖然這篇文章也有其餘的翻譯,可是看起來像是機翻的,因此我本身翻譯了一下,而且加上本身的理解。若是有理解錯誤的地方或者錯別字,還望指出。java

本文是 Spring Security 的入門指南,深刻解析了 Spring Security 框架的設計和基礎模塊。咱們僅涉及程序安全性的基礎知識,可是這能夠幫助使用 Spring Security 的開發者解開一些疑惑。爲此,咱們如何使用 filter 和方法註解來實踐 web 應用的安全。若是你想在更高層次上理解如何保障應用安全性,或者想要定製應用安全,又或者你只是想了解設計應用安全的思路,那麼本指南就很適合你。web

本指南不是解決最基本問題以外的用戶手冊(這樣文章已經有不少了),可是本文對初學者和高手都有必定的幫助。Spring Boot 在本文中常常被說起,由於它爲 Spring Security 提供了一些默認配置而且這有助於理解 Spring Security 是如何適應整個架構的。而全部這些原則對不使用 Spring Boot 的應用一樣適用。spring

認證和訪問控制

應用安全總結起來就是兩大問題:認證(authentication,你是誰?)和受權(authorization,容許你作什麼?)有時候也會用訪問控制(access control)這個名詞來代替受權,這會讓咱們一些困惑,但以這種方式思考可能會有幫助:「受權」在其餘地方已經實現了。Spring Security 的架構旨在將認證從受權中分離出來,並也有適用於二者的策略和可擴展的設計。後端

認證(Authentication)

用於認證的主要接口是AuthenticationManager,它只有一個方法:api

public interface AuthenticationManager {
  Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
複製代碼

一個 AuthenticationManagerauthenticate()方法中有三種狀況:安全

  1. 返回 Authentication (authenticated=true),若是驗證輸入是合法的Principal)。
  2. 拋出AuthenticationException異常,若是輸入不合法。
  3. 若是沒法判斷,則返回null

AuthenticationException是一個運行時異常,一般被應用程序以常規的方式的處理,這取決於應用的母的和代碼風格。換句話說,代碼中通常不會捕捉和處理這個異常。好比,可使得網頁顯示認證失敗,後端返回 401 HTTP 狀態碼,響應頭中的WWW-Authenticate 有無視狀況而定。cookie

AuthenticationManager最廣泛的實現是ProviderManagerProviderManager將認證委託給一系列的AuthenticationProvider實例 。AuthenticationProviderAuthenticationManager 很相似,可是它有一個額外的方法容許查詢它支持的Authentication方式:session

public interface AuthenticationProvider {
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
  
	boolean supports(Class<?> authentication);
}
複製代碼

supports方法的Class<?> authentication參數實際上是Class<? extends Authentication>類型的。一個ProviderManager在一個應用中能支持多種不一樣的認證機制,經過將認證委託給一系列的AuthenticationProviderProviderManager沒有識別出的認證類型,將會被忽略。

每一個ProviderManager能夠有一個父類,若是全部AuthenticationProvider都返回null,那麼就交給父類去認證。若是父類也不可用,則拋出AuthenticationException異常。

有時應用的資源會有邏輯分組(好比全部網站資源都匹配URL/api/**),而且每一個組都有本身的AuthenticationManager,一般是一個ProviderManager,它們之間有共同的父類認證器。那麼父類就是一種全局資源,充當全部認證器的 fallback。

authentication

圖1 ProviderManager 的繼承關係

自定義AuthenticationManager

Spring Security 提供了一些配置方式幫助你快速的配置通用的AuthenticationManager。最多見的是AuthenticationManagerBuilder,它可使用內存方式(in-memory)、JDBC 或 LDAP、或自定義的UserDetailService來認證用戶。下面是設置全局認證器的例子:

@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的用處大爲廣闊(詳細狀況請看[Web 安全](#Web 安全)是如何實現的)。請注意AuthenticationManagerBuilder是經過@AutoWired注入到被@Bean註解的一個方法中的,這使得它成爲一個全局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");
  }

}
複製代碼

重寫configure(AuthenticationManagerBuilder builder)方法,那麼AuthenticationManagerBuilder僅會構造一個「本地」的AuthenticationManager,只是全局認證器的一個子實現。在 Spring Boot 應用中你可使用@Autowired注入全局的AuthenticationManager,可是你不能注入「本地」的,除非你本身公開暴露它。

Spring Boot 提供默認的全局AuthenticationManager,除非你提供本身的全局AuthenticationManager。不用擔憂,默認的已經足夠安全了,除非你真的須要一個自定義的全局AuthenticationManager。通常的,你只需只用「本地」的AuthenticationManagerBuilder來配置,而不須要擔憂全局的。

受權(Authorization)

一旦認證成功,咱們就能夠進行受權了,它核心的策略就是AccessDecisionManager。它提供三個方法而且所有委託給AccessDecisionVoter,這有點像ProviderManager將認證委託給AuthenticationProvider

一個AccessDecisionVoter考慮一個Authentication(表明一個Principal)和一個被ConfigAttributes裝飾的安全對象:

boolean supports(ConfigAttribute attribute);

boolean supports(Class<?> clazz);

int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
複製代碼

AccessDecisionVoterAccessDecisionManager方法中的object參數是徹底泛型化的,它表明任何用戶想要訪問(web 資源或 Java 方法是最多見的兩種狀況)。ConfigAttributes也是至關泛型化的,它表示一個被裝飾的安全對象並帶有訪問權限級別的元數據。ConfigAttributes是一個接口,僅有一個返回String的方法,返回的字符串中包含資源全部者,解釋了訪問資源的規則。常見的ConfigAttributes是用戶的角色(好比ROLE_ADMINROLE_AUDIT),它們一般有必定的格式(好比以ROLE_做爲前綴)或者是可計算的表達式。

大部分人使用默認的AccessDecisionManager,即AffirmativeBased(若是沒有 voters 返回那麼該訪問將被受權)。任何自定義的行爲最好放在 voter 中,不亂世添加一個新的 voter 仍是修改已有的 voter。

使用 Spring Expression Language(SpEL)表達式的ConfigAttributes是很常見的,好比isFullyAuthenticated() && hasRole('FOO')。解析表達式和加載表達式由AccessDecisionVoter實現。要擴展可處理的表達式的範圍,須要自定義SecurityExpressionRoot,有時候也須要SecurityExpressionHandler

Web 安全

Web 層中的 Spring Security 基於 Servlet 的Filter。因此先來看下Filter在 web 安全中所扮演的角色。下圖展現了處理單個 HTTP 請求的經典分層結構。

filters

客戶端嚮應用發送請求,而後容器根據 URI 來決定哪一個 filter(過濾器) 和哪一個 Servlet 適用於它。一個 servlet 最多處理一個請求,過濾器是鏈式的,它們是有順序的。事實上一個過濾器能夠否決接下來的過濾器,若是它想獨自處理這個請求的話。一個過濾器也能夠對下流的過濾器和 servlet 修改響應和請求。因此過濾器的順序十分重要,Spring Boot 提供管理過濾器的兩種機制:一個是被@Bean註解的Filter能夠用@Order註解或實現Ordered接口;另外一個是過濾器是FilterRegistrationBean的一部分,它自己就有一個順序。一些現有的過濾器定義了本身的常量來表示順序,以幫助代表他們相對於彼此的順序(好比 Spring Session 中的SessionRepositoryFilterDEFAULT_ORDER的值爲Integer.MIN_VALUE + 50,它表示這個過濾器相對的在過濾鏈的前端,可是也不排斥在它以前的過濾器,前面還剩下50個位置)。

Spring Security 在過濾鏈中表現爲一個Filter,其類型是FilterChainProxy,緣由你很快就會知道。在一個 Spring Boot 應用中安全過濾器是ApplicationContext中的一個Bean,而且它是默認配置的,因此在每次請求中都會存在。而它在過濾鏈中的位置由SecurityProperties.DEFAULT_FILTER_ORDER決定,而該位置又由FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER(值爲0,是 Spring Boot 中改變請求行爲的過濾器的順序的最大值)錨定。(譯者注:Order的值越小,越在過濾鏈的前端)。除此以外,從容器的角度來看 Spring Security 是一個單獨的過濾器,可是其中包含了額外過濾器,每一個過濾器都發揮特殊的做用,以下圖所示:

security-filters

圖2 Spring Security 是一個單獨的物理過濾器,可是它將請求委託一系列的內部過濾器

事實上內部的安全過濾器不止一個層次結構:它們一般是DelegatingFilterProxy,不須要是一個 Spring Bean。代理委託給FilterChainProxy,而它是一個Bean,bean 的名字一般是springSecurityFilterChainFilterChainProxy包含了全部內部安全過濾器,而且以必定順序排列成過濾鏈。其中全部的過濾器都有相同的 API(它們都實現了Servlet規範的Filter接口),它們都有機會否決過濾鏈的下流部分。

Spring Security 能夠在同一頂層FilterChainProxy中管理多個過濾器鏈,而且對容器來講都是未知的。Spring Security Filter 包含了一系列的過濾鏈,而且向這些鏈分發匹配它們的請求。下圖展現了根據請求路徑來分發(/foo/**/**以前匹配)。這是一種常見但不是惟一的分發方式。最重要的特徵是,分發過程當中,只有一條過濾鏈只處理該請求。

security-filters-dispatch

圖3 Spring Security FilterChainProxy 分發請求給首先匹配的過濾鏈。

一個純淨的(沒有自定義安全配置的) Spring Boot 應用一般有 n 條過濾鏈,n = 6。第一條鏈(n-1)是忽略靜態資源的,好比/css/**/images/**,和錯誤頁面/error(這些路徑能夠在SecurityProperties中的security.ignored裏配置)。最後一條鏈匹配全部路徑/**,而且包含認證邏輯、受權、異常處理、session 處理,響應頭處理等。這條過濾鏈中默認一共有 11 個過濾器,咱們通常不關心使用哪一個過濾器以及在什麼時候使用他們。

注意:意識到 Spring Security 的內部過濾器對容器是透明的這是很重要的,全部的Filter都以@Bean的方式自動註冊到容器中。因此若是你想在安全過濾鏈中添加過濾器,你不須要使用@Bean註解或將其包裹在顯示禁用容器註冊的FilterRegistrationBean中。

建立和自定義過濾鏈

Spring Boot 中默認的 fallback 過濾鏈(使用/**匹配的過濾鏈)有一個預約義的順序SecurityProperties.BASIC_AUTH_ORDER。你可使用security.base.enabled=false關閉它,或者你能夠定義一個更低的順序值(譯者注:越低的值表示順序更前,因此它的順序在默認的 fallback 以前)。只要添加一個WebSecurityConfigurerAdapterWebSecurityConfigurer的 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 在默認的 fallback 以前添加一個過濾鏈。

許多應用中對一組資源的和另外一組資源的訪問規則可能大不相同。好比一個有前端頁面和後端 API 的應用支持基於 cookie 的認證將用戶重定向到登陸界面,同時也支持基於 token 的認證,認證失敗將返回 401 響應碼。每組資源有它本身的WebSecurityConfigurerAdapter,而且有這惟一的順序和他本身的請求匹配規則。若是匹配規則重疊,則匹配順序最前的過濾鏈。

請求匹配分發和受權

一條安全過濾鏈(等價的 ,就是一個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 最容易犯的一個錯誤就是忘記匹配規則能夠應用在不一樣的範圍中,一個是整條過濾鏈,另外一個是應用於過濾鏈匹配規則中的規則。

將應用安全規則與 Actuator 規則結合

略,我沒用過 Actuator,因此就沒翻譯

Method 安全

Spring Security 在支持 web 安全的同時,也提供了對 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 建立了MyService Bean,那麼它將被代理,調用者必須在方法調用以前經過一個安全攔截器。若是訪問被拒絕,調用者會拋出一個AccessDeniedException而不是執行這個方法的結果。

還有其餘可用於強制執行安全約束的方法註解,特別是@PreAuthorize@PostAuthorize, 它們容許你在其中寫 SpEL 表達式並能夠引用方法的參數和返回值。

提示: 把 web 安全和方法安全放在一塊兒並不突兀。過濾鏈提供了用戶體驗特性,好比認證和重定向到登陸界面。而方法安全在更細粒度級別上提供了保護。

Spring Security 和線程

Spring Security是線程綁定的,由於它須要保證當前的已認證的用戶(authenticated principal)對下流的消費者可用。基本構建塊是SecurityContext,它可能包含Authentication(當一個用戶登錄後,authenticated確定是 true)。你老是能夠從SecurityContextHolder中的靜態方法獲得SecurityContext,它內部使用了ThreadLocal進行管理。

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
assert(authentication.isAuthenticated);
複製代碼

這種操做並不常見,可是它可能對你有幫助。好比,你須要寫一個自定義的認證過濾器(儘管如此,Spring Security 中還有一些基類可用於避免使用SecurityContextHolder的地方)。

若是須要訪問 web endpoint(譯者注:對應響應的 URL) 中通過身份驗證的用戶,則能夠在@RequestMapping中使用方法參數註解。例如:

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

這個註解至關於從SecurityContext中得到當前Authentication,並調用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類時更加謹慎)。

異步執行安全方法

由於SecurityContext是線程綁定的,因此若是你想在後臺執行安全方法,好比使用@Async,你須要確保上下文的傳遞。這總結起來就是將SecurityContextRunnableCallable等包裹起來在後臺執行。Spring Security 提供了一些幫助使之變得簡單,好比RunnableCallable的包裝器。 要將 SecurityContext 傳遞到@Async註解的方法,你須要編寫 AsyncConfigurer 並確保 Executor 的正確性:

@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {

  @Override
  public Executor getAsyncExecutor() {
    return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
  }
}
複製代碼
相關文章
相關標籤/搜索