Security 簡單介紹
本篇是Spring安全的初級指南,主要介紹Spring安全框架的設計和基本模組。此處僅僅涉及應用安全方面非常基礎的知識,但是通過本篇可以掃清使用Spring安全框架是遇到的一些困惑。為了達到此目的,我們會關注安全是如何通過過濾器和註解而被應用到Web應用中的。當你想在更高的層次理解Spring安全框架都是如何工作的,並且想自定義一些特性時可以考慮這份指南,或者你只是想了解一下應用安全的知識也是可以的。
本指南不打算作為一個手冊或解決多個最基本的問題的配方(可以檢視別的地方),但是對於初學者或專家都會有幫助。SpringBoot也被引用了很多次,這是因為它為一個安全的應用提供了很多預設的行為,這對於理解他們如何同整個架構整合在一起很有幫助。對於所有沒有使用SpringBoot的應用這些原則也能很好的應用。
身份驗證和訪問控制
應用安全可以大概分為兩個獨立的問題:身份驗證(你是誰?)和授權(你有權做什麼?)。有時候把 “授權” 稱作 “訪問控制” ,這可能會引起困惑,但是在某些地方這麼想是有好處的而“授權”卻顯得太重了。Spring安全框架被設計成分開身份驗證和授權模組,但是對於二者都提供了各自的策略和擴充套件點。
身份驗證
身份驗證的主要策略設定介面是 AuthenticationManager
,它只有一個方法:
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
AuthenticationManager
會在它的 authenticate()
方法裡面做下面三件事之一:
- 如果它驗證當前使用者通過,會返回一個
Authentication
(正常情況下authenticated=true
) - 如果驗證當前使用者不合法,會丟擲一個
AuthenticationException
異常 - 如果它無法決定,返回
null
AuthenticationException
是一個執行時異常,應用通常會根據自己的型別和目的採用比較通用的方法來處理。換句話說,使用者的程式碼通常不會直接捕獲並處理這個異常。比如:Web伺服器會返回一個使用者身份驗證失敗的頁面,同時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>
. 通過把工作代理給一組AuthenticationProviders
例項,在同一個應用中,一個ProviderManager
能夠同時支援多種不同的身份驗證機制。如果一個 ProviderManager
不能識別特殊的 Authentication
型別,那麼它會被直接跳過。
每個ProviderManager
會有一個可選的父母(parent),如果所有的 providers
都返回 null
時,會呼叫父母的實現。如果父母不可用(null),會丟擲 AuthenticationException
異常。
有時,應用對於一些受保護的資源會有一個邏輯分組(比如,所有的Web資源訪問路徑類似/api/**
),每個分組都可以設定自己的AuthenticationManager
。通常情況下,每一個這樣的分組都會對應一個 ProviderManager
,他們都會共享同一個父母。父母在某種程度上是全域性的資源,給所有的其它ProviderManager
提供保底方案。
Figure 1. An AuthenticationManager hierarchy using ProviderManager
自定義 AuthenticationManager
Spring安全提供了一些配置幫助類,以便快速的在應用中建立通用的身份驗證功能。最常用的幫助類是 AuthenticationManagerBuilder
,它對於建立基於記憶體的(in-memory)、JDBC或者LDAP使用者詳情(user details) 或者新增自定義的 UserDetailsService
支援的都非常好。下面是一個配置全域性型別(parent)的 AuthenticationManager
的示例:
@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
... // web stuff here
@Autowired
public initialize(AuthenticationManagerBuilder builder, DataSource dataSource) {
auth.jdbcAuthentication().dataSource(dataSource).withUser("dave")
.password("secret").roles("USER");
}
}
這個例子涉及到一個Web應用,但是對於 AuthenticationManagerBuilder
的使用有更廣泛的適用場景(見下面Spring安全的實現細節)。注意到 AuthenticationManagerBuilder
通過 @Autowired
註解被注入到一個 @Bean
中的方法 - 這會導致它構造的是全域性的(parent)AuthenticationManager
。與之相反,如果通過下面的方式:
@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
... // web stuff here
@Override
public configure(AuthenticationManagerBuilder builder) {
auth.jdbcAuthentication().dataSource(dataSource).withUser("dave")
.password("secret").roles("USER");
}
}
(使用 @Override
註解方法)會導致 AuthenticationManagerBuilder
構建的是一個"區域性的(local)“的AuthenticationManager
,他會是全域性 AuthenticationManager
的一個孩子。在SpringBoot應用中,你可以通過 @Autowired
把全域性的 AuthenticationManager
注入到另外一個Bean中,但是除非你主動暴露否則是不能把本地的(local)的AuthenticationManager
通過類似的方式注入到另一個Bean中的。
如果你沒有提供自己的AuthenticationManager
, SpringBoot會提供預設的全域性 AuthenticationManager
(僅包含一個使用者)。預設的實現已經足夠的安全,除了急需一個自定義的AuthenticationManager
實現,大多時候你不需要擔心它的安全性。一般情況下,只要根據自己要保護的資源定義一個相關的本地AuthenticationManager
實現就足夠了,沒有必要去修改全域性的那個。
授權/訪問控制
一旦身份驗證成功,接下來會進入授權訪問環節,它的核心策略是通過 AccessDecisionManager
介面實現的。框架提供了3種實現,它們內部都會把工作委託給一組 DecisionVoter
集合(鏈),這和 ProviderManager
類似,後者委託給一組 AuthenticationProviders
集合。
//譯註:這個程式碼片段是譯者加上去的,方便對本節的理解
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException;
DecisionVoter
會考慮 Authentication
(當前使用者) 和一個通過 ConfigAttributes
描述的普通物件。在AccessDecisionManager
和 DecisionVoter
的方法簽名上,這個物件是一個泛化型別 - 它代表了使用者想要訪問的任何資源(Web資源或者某個Java類的方法)。ConfigAttributes
的設計也是泛化的,它描述了一個需要安全訪問的物件,並提供一些元資訊來決定訪問物件的許可權等級。
ConfigAttribute
是一個只有一個方法的介面,它的實現相當通用,它會返回一個 String
型別,可以把資源所有者的意圖編碼在字串裡面以控制資源訪問的規則。典型的 ConfigAttribute
是一些使用者角色字串(比如:ROLE_ADMIN
或 ROLE_AUDIT
),他們通常會有特殊的格式(比如ROLE_
開頭的字串)或者是可以推導的表示式。
大部分人會使用預設的AccessDecisionManager
,它是AffirmativeBased
(如果沒人投票拒絕,那麼會允許訪問)。對於自定義AccessDecisionManager
一般會通過新增一個新的Voter或者修改一個現成的來實現。
在ConfigAttributes
中,使用Spring的表示式語言(Spring Expression Language)是很常見的做法,比如 isFullyAuthenticated() && hasRole('FOO')
。它是通過一個支援解析這種語法的DecisionVoter
實現的。為了擴充套件這種語法的處理範圍,需要實現定義的 SecurityExpressionRoot
有時候還需要 SecurityExpressionHandler
。
Web 安全
Spring安全在Web層(UI和HTTP後臺)是基於Servlet過濾器實現的,所以關注一下 Filters
在整個環節中扮演的角色很有幫助。下圖展示了對一個HTTP請求處理的典型佈局:
客戶端傳送一個請求給應用,然後容器根據請求的URI的路徑來決定使用哪個過濾器或Servlet來處理請求。一個Servlet最多處理一個請求(?),但是過濾器會形成一個鏈,所以它們是有序的,並且事實上一個過濾器如果想自己處理請求那麼可以否決鏈上之後的過濾器。一個過濾器也可以中途修改下游過濾器或Servlet中要處理的請求(request)或者響應(response)。過濾器鏈的順序是非常重要的,SpringBoot通過兩種機制來管理:一種是Filter
型別的Bean可以使用@Order
註解或者實現Ordered
介面,另一種是把Filter新增到FilterRegistrationBean
,它有提供了設定Filter順序的方法。有一些現成的Filter定義了一些常量來標記自己的順序(比如:SpringSession中的SessionRepositoryFilter定義了DEFAULT_ORDER = Integer.MIN_VALUE + 50
),告訴我們它想待在鏈的前端但是又不想完全霸佔最前排。
Spring安全是使用一個Filter
實現的。在SpringBoot應用中,Spring安全模組只是 ApplicationContext
中的一個Bean,它預設已經被安裝到系統中,所以每個請求都會經過安全模組。它的位置是通過 SecurityProperties.DEFAULT_FILTER_ORDER
定義的,而它是進一步通過 FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER
(SpringBoot應用期望那些封裝請求/更改行為的Filter的最大順序)來錨點。關於它還有更多需要說明的:從容器的角度來說,Spring安全就是一個Filter而已,但是在它內部包含其它的Filter,每一個都扮演了不同的角色。如下圖示:
Figure 2. Spring Security is a single physical Filter but delegates processing to a chain of internal filters
事實上,在SpringSecurity的Filter實現包含更多的中間層。它通常是通過 DelegatingFilterProxy
安裝到容器中的。這個代理會把工作委託給FilterChainProxy
- 它本身是一個Bean通常有一個固定的名字 - springSecurityFilterChain
。FilterChainProxy
包含了所有的安全邏輯,它內部組織成一個或多個鏈的形式。所有的過濾器都包含相同的API(它們都實現了來自Servlet標準的Filter介面API)而且它們都有給下游的Filter投票的機會。
SpringSecurity內部可以包含多個過濾器鏈,這對於容器來說都是透明的。SpringSecurity內部包含多個Filter鏈,它們會根據匹配的請求被應用在不同的請求的處理上。下圖展示了根據匹配路徑(/foo/**
會在/**
之前匹配)匹配過濾器的場景。這是比較常見的方式,但不是唯一的方式。在整個分發過程中,最重要的事情是每次只有一個鏈用來處理請求。
Figure 3. The Spring Security FilterChainProxy dispatches requests to the first chain that matches.
一個普通的沒有自定義過任何安全配置的SpringBoot應用會包含多個(=n)Filter鏈,通常 n=6.第一個(n-1)鏈是用來忽略靜態資源的,比如 /css/**
和 /images/**
還有錯誤頁面 /error
(這些路徑可以通過SecurityProperties配置Bean中的security.ignored屬性定義)。最後一個Filter鏈會匹配所有的請求路徑 /**
而且更活躍,包含了身份驗證邏輯,授權邏輯,異常處理邏輯,Session處理邏輯,HEAD寫入邏輯等。預設情況下,這個鏈中包含11個過濾器,但是對於開發者來說,通常沒有必要知道哪個Filter在什麼時候被使用。
注:SpringSecurity的內部包含哪些Filter對於容器來說是無知的,這一點很重要,特別是在SpringBoot程式中,所有的Filter型別的Bean都會被預設注入到容器中。所以如果你想在安全鏈中新增一個自定義的Filter,那麼最好不要把它註解為一個Bean而是通過
FilterRegistrationBean
封裝進去,這樣可以避免容器自動把這個Filter註冊到自身。
建立並自定義過濾器鏈
在SpringBoot應用中,保底的過濾器鏈(匹配/**
路徑的那個)有一個提前定義好的順序 - SecurityProperties.BASIC_AUTH_ORDER
。你可以通過設定 security.basic.enabled=false
關閉它,或者你可以把它用作一個保底方案同時把其它的規則定義成更低的順序 - 通過實現一個WebSecurityConfigurerAdapter
型別的Bean(或者WebSecurityConfigurer
也可以)並通過 @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會導致SpringSecurity在過濾鏈中新增一條新的鏈,並且處於兜底方案之前。
對於不同的資源可能有完全不同的訪問控制規則。比如,一個包含UI服務和API服務的應用,對於UI部分可能會採取基於Cookie的身份驗證方案並會重定向到登陸頁面,但是對API部分,可能會採用基於Token的方案,並且返回一個401訪問受限的狀態碼。它們分別會配置自己的 WebSecurityConfigurerAdapter
並設定順序和匹配規則。如果規則重疊,那麼處於前排的過濾器鏈會勝出。
匹配請求
SpringSecurity的過濾器鏈(或者等價的說是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/**") // 負責匹配整個FilterChain
.authorizeRequests() // 以下匹配訪問控制的規則
.antMatchers("/foo/bar").hasRole("BAR")
.antMatchers("/foo/spam").hasRole("SPAM")
.anyRequest().isAuthenticated();
}
}
在配置Filter的時候最容易犯的錯誤是,這裡不同的匹配器應用於不同的處理流程,一個是負責匹配整個FilterChain,其它的用來選擇訪問控制的規則。
方法安全
SpringSecurity不僅能夠保護Web的安全,同樣能夠保證java方法的安全。對於SpringSecurity來說,這僅僅是另外一種”資源“。對於使用者來說,訪問控制的規則使用和 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安全和方法安全是常見的做法。FilterChain提供了使用者體驗方面的功能,比如身份驗證並重定向到登陸頁面,而方法安全提供了更細粒度的保護。
處理執行緒問題
Spring安全是以執行緒為邊界的,因為它需要讓當前驗證的使用者對接下來的一大批下游消費者可用。基礎模組是 SecurityContext
,它包含一個 Authentication
物件(當一個使用者登陸的時候,它會返回一個驗證通過的Authentication物件
)。你可以隨時通過 SecurityContextHolder
提供的靜態便捷方法操作 SecurityContext
物件,它內部其實是在操作一個 TheadLocal
,比如:
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
assert(authentication.isAuthenticated);
對於使用者程式碼來說很少去直接操作它,但是如果你是在自定義自己的身份驗證Filter,那麼這會非常有用(也可以通過父類提供的方法來獲取SecurityContext
所以可能也不會用到 SecurityContextHolder
)。
如果你想在Web伺服器的API上訪問當前身份驗證通過的使用者,你可以通過@RequestMapping
註解的方法引數獲得:
@RequestMapping("/foo")
public String foo(@AuthenticationPrincipal User user) {
... // do stuff with user
}
這注解(@AuthenticationPrincipal
)會從SecurityContext
拿到當前的Authentication
,並呼叫它的getPrincipal()
方法來生成方法引數。Authentication
中的 Principal
的具體型別取決於 AuthenticationManager
中用來驗證身份的物件型別,所以這是一種有用的小技巧以便來獲得一個型別安全的指向使用者資料的引用。
如果使用了SpringSecurity,那麼從HttpServletRequest
中提取的Principal
物件將會是Authentication
型別,你也可以直接使用:
@RequestMapping("/foo")
public String foo(Principal principal) {
Authentication authentication = (Authentication) principal;
User = (User) authentication.getPrincipal();
... // do stuff with user
}
有時候如果你想在沒有使用SpringSecurity的情況下,也希望這片程式碼生效,那麼這種寫法是很有幫助的(你在獲取Authentication
的時候可能需要寫一點防衛性的程式碼)。
非同步的處理安全方法
因為 SecurityContext
是以執行緒為邊界的,如果你想非同步呼叫相關方法,比如使用@Async
註解,你需要保證Context有被傳遞。可以使用能夠後臺執行的任務(Runnable
,Callable
等)來封裝SecurityContext
.Spring安全提供了一些幫助方法來使這個過程更簡單,比如對Runnable
和Callable
的一些封裝。為了把SecurityContext
傳遞到@Async
註解的非同步方法,你需要提供一個 AsyncConfigurer
配置並且保證Executor
是正確的型別:
@Confiuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
return new DelegatingSecurityContextExecutorService(Executors.new FixedThreadPool(5);
}
}
作者:ntop
連結:https://www.jianshu.com/p/158b0c30c905
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。