1. Introduction(簡介)
本篇是關(guān)于Spring安全框架的入門指導(dǎo),主要講解Spring 安全框架的體系結(jié)構(gòu),設(shè)計(jì)思路和組成模塊。雖然本文只涵蓋了最為基本的應(yīng)用安全知識,但這些足以幫助開發(fā)者消除在使用Spring 安全框架進(jìn)行開發(fā)時遇到的一些困惑。為了完成這些工作,我們來瞧一瞧如何通過Filters(Servlet規(guī)范中一種組件)以及更為常用的方法注解來在Web應(yīng)用中使用安全組件。
如果你需要在更高的層次上理解一個安全的應(yīng)用是如何工作的,或者你想知道如何定制化應(yīng)用的安全組件,或者你僅僅只是想要了解一下應(yīng)用安全方面的知識,那么,都可以通過閱讀本篇指導(dǎo)獲取你想要的。但是本篇指導(dǎo)并沒有打算去說明或者解決超出基本安全范圍的問題或者需求(這些工作由其他的指導(dǎo)來完成),但對于一個關(guān)于應(yīng)用安全的初學(xué)者來說,這篇指導(dǎo)是非常有用的。這篇指導(dǎo)有很大的篇幅涉及到了Spring Boot,這是因?yàn)镾pring Boot默認(rèn)為應(yīng)用的安全提供了一些支持,這對于我們理解Spring安全框架是如何適配整個Spring體系結(jié)構(gòu)是有幫助的。所有這些適用于Spring Boot應(yīng)用的方式或者方法,同樣適用于那些使用了Spring框架的其他形式的Web應(yīng)用程序。
2. Authentication(認(rèn)證) & Access Control(訪問控制)
應(yīng)用程序的安全問題或多或少可以歸納為兩個相互獨(dú)立的基本問題:認(rèn)證(Authentication,解決身份識別問題,即識別用戶身份是否合法)和授權(quán)(Authorization ,解決訪問權(quán)限問題,即允許用戶做什么)。有些人使用"access control(訪問控制)"來代替"authorization(授權(quán))",雖然這兩種說法會給用戶帶來一些困惑,但由于"authorization(授權(quán))"這個詞在有些地方被過度的解釋了,這導(dǎo)致"access control(訪問控制)"這種說法更有助于我們理解這種控制用戶的訪問權(quán)限的方式。Spring安全框架的體系結(jié)構(gòu)在設(shè)計(jì)的時候就將認(rèn)證(authentication)從授權(quán)(authorization)中分離出來,并且設(shè)計(jì)了一些策略能夠?qū)@兩者進(jìn)行擴(kuò)展。
2.1 Authentication(認(rèn)證)
Spring安全框架中,為認(rèn)證(Authentication)設(shè)計(jì)的主要策略接口是org.springframework.security.authentication.AuthenticationManager
,而這個接口只有一個方法:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
在AuthenticationManager
的authenticate()
方法中,用戶可以做三件事情:
- 如果可以確認(rèn)輸入的參數(shù)
authentication
(是org.springframework.security.core.Authentication
的實(shí)例對象)代表一個合法的用戶身份,那么返回另一個org.springframework.security.core.Authentication
實(shí)例對象,這個返回的對象通常會帶有一個authenticated=true
的標(biāo)記。 - 如果可以確認(rèn)輸入的參數(shù)
authentication
代表一個非法的用戶身份,那么將拋出一個org.springframework.security.core.AuthenticationException
異常。 - 如果無法判斷輸入的參數(shù)
authentication
是否是一個合法的用戶身份,可以返回null值。
org.springframework.security.core.AuthenticationException
是一個運(yùn)行時異常。一般情況下,該異常會被應(yīng)用程序使用專門的處理器進(jìn)行處理,而如何處理取決于應(yīng)用程序的形式和用途。換句話說,一般情況下,應(yīng)用程序并不指望開發(fā)者編寫代碼去捕獲和處理這些異常,而是提供默認(rèn)的策略,由應(yīng)用程序自身來處理這個異常,比如,應(yīng)用程序會提供一個界面同時渲染一個頁面來告訴用戶認(rèn)證失敗,同時后臺的HTTP服務(wù)也將會發(fā)送401狀態(tài)碼,也會根據(jù)應(yīng)用的上下文環(huán)境來決定是否攜帶WWW-Authenticate
頭部。
最常用的org.springframework.security.authentication.AuthenticationManager
實(shí)現(xiàn)類是org.springframework.security.authentication.ProviderManager
,該類維護(hù)了一個由接口org.springframework.security.authentication.AuthenticationProvider
的實(shí)現(xiàn)類的實(shí)例所組成的列表。而org.springframework.security.authentication.AuthenticationProvider
同org.springframework.security.authentication.AuthenticationManager
相似,區(qū)別在于org.springframework.security.authentication.AuthenticationProvider
內(nèi)部包含另一個方法允許調(diào)用者測試是否支持傳入的org.springframework.security.core.Authentication
實(shí)現(xiàn)類的類型:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
supports()
方法中的Class<?>
參數(shù)的真正類型是Class<? extens Authentication>
,主要是用來測試是否支持傳入authenticate()
方法的authentication
參數(shù)的類型。由于ProviderManager
代理了一個AuthenticationProviders
鏈,所以可以在同一個應(yīng)用中支持多種不同的認(rèn)證機(jī)制。一般情況下,ProviderManager
會跳過那些自己不支持的Authentication
的實(shí)例類型。
一個ProviderManager
有一個可選的父級provider,如果所有的providers都返回的是null,也就是說所有的認(rèn)證機(jī)制都無法確定當(dāng)前的用戶身份是合法的,最終將由這個父級(或者全局)的provider來決定,如果不存在這個父級的provider也會返回null,最終會拋出AuthenticationException
。
有時候,應(yīng)用將被保護(hù)的資源按照一定的規(guī)則分成邏輯上的分組(比如,所有的web資源都通過路徑來分組,即將資源按照linux目錄樹的形式進(jìn)行分組),并且每一個分組都擁有專屬的AuthenticationManager
,而這些AuthenticationManager
通常都是一個ProviderManager
,這些AuthenticationManager
共享一個父級(或者全局)的AuthenticationManager
,這個父級的AuthenticationManager
作為所有的providers的替補(bǔ)而存在。
2.2 Customizing Authentication Managers(自定義認(rèn)證管理器)
Spring安全框架提供了一些配置的輔助類,能夠在應(yīng)用中快速的創(chuàng)建常用的認(rèn)證管理功能。最常用的輔助類就是org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
,該類主要用來設(shè)置獲取用戶信息的方式,有三種方式,分別是in-memory,JDBC
或者LDAP
,也可以添加實(shí)現(xiàn)了org.springframework.security.core.userdetails.UserDetailsService
接口的類對象來設(shè)置獲取用戶詳情的方式。
下面的例子展示了如何在一個應(yīng)用中配置全局的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應(yīng)用的例子,AuthenticationManagerBuilder
的使用方式有很多種,而在這個例子中AuthenticationManagerBuilder
的一個實(shí)例被應(yīng)用作為參數(shù)通過@Autowired
注解傳入initialize()
方法中,在這個方法中將會創(chuàng)建一個全局(父級)的AuthenticationManager
。我們可以和另外一種使用方式進(jìn)行對比:
@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
的實(shí)例只是被該方法的調(diào)用者使用來創(chuàng)建一個局部的AuthenticationManager
,這個局部的AuthenticationManager
是全局AuthenticationManager
的孩子。在一個Spring Boot應(yīng)用中,我們可以使用@Autowired
注解將全局的AuthenticationManager
注入到其他的bean
中,但是無法將一個局部的AuthenticationManager
注入到其他的bean
中,除非我們通過配置@Bean
的方式明確的將該局部的AuthenticationManager
的實(shí)例作為一個組件發(fā)布出去。
Spring Boot提供了一個默認(rèn)的全局AuthenticationManager
,我們可以通過提供自己的AuthenticationManager
組件來替換他。這個默認(rèn)的AuthenticationManager
組件是足夠安全的,我們無需對他有過多的擔(dān)心,除非你確實(shí)需要一個自定義的AuthenticationManager
。如果我們做了一些配置創(chuàng)建了一個AuthenticationManager
組件,我們可以將該組件應(yīng)用到局部的受保護(hù)資源上而無需擔(dān)心全局的AuthenticationManager
。
前文說的全局的provider,是
ProviderManager
組件,ProviderManager
是AuthenticationManager
最常用的一個實(shí)現(xiàn)類,所以全局的provider就是全局的AuthenticationManager
組件。
2.3 Authorization or Access Control(授權(quán)或訪問控制)
一旦用戶成功通過了認(rèn)證,我們就可以開始關(guān)注授權(quán)問題,核心的授權(quán)策略由接口org.springframework.security.access.AccessDecisionManager
來提供。Spring安全框架提供這個接口的三種實(shí)現(xiàn),每一種都可以委托給一個org.springframework.security.access.AccessDecisionVoter<S>
鏈,這與ProviderManager
委托給AuthenticationProviders
有一點(diǎn)類似。
一個AccessDecisionVoter
主要用來處理代表用戶身份的Authentication
對象和一個由ConfigAttributes
描述的安全對象:
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
這個安全對象指的是參數(shù)object
,其類型就是AccessDecisionVoter<S>
的泛型參數(shù)S,他代表任何用戶想要訪問的目標(biāo)(Web資源或Java類的方法是該對象的兩種最常見的形式)。ConfigAttributes
指的是參數(shù)attributes
,他是org.springframework.security.access.ConfigAttribute
的集合,保存了安全對象的元數(shù)據(jù),而這些元數(shù)據(jù)確定了訪問該安全對象所要求的權(quán)限。ConfigAttribute
是只有一個方法的接口,而這個方法返回一個String
類型的值,這個值通常情況下是一串編碼,表示資源所有者制定的資源訪問規(guī)則。典型的ConfigAttribute
應(yīng)用方式就是返回表示用戶角色的字符串,比如(ROLE_ADMIN
或者ROLE_AUDIT
),這些都有統(tǒng)一的格式(比如以ROLE_
為前綴),而另外一種應(yīng)用方式是返回能夠用來執(zhí)行的表達(dá)式字符串。
絕大多開發(fā)者只使用默認(rèn)的AccessDecisionManager
組件,默認(rèn)的AccessDecisionManager
的機(jī)制是如果得票數(shù)沒有下降,那么訪問就應(yīng)該被允許。因此所有的定制化開發(fā)都傾向于發(fā)生在投票者那里,要么是增加新的投票者,要么改變已有投票者的行為。
最常用的ConfigAttributes
是Spring的EL表達(dá)式,例如isFullyAuthenticated() && hasRole('FOO')
。一種AccessDecisionVoter
組件就支持Spring的EL表達(dá)式,他不但能夠執(zhí)行這些表達(dá)式,同時還能為他們創(chuàng)建上下文環(huán)境。如果想要擴(kuò)展表達(dá)式的語法,可以實(shí)現(xiàn)org.springframework.security.access.expression.SecurityExpressionRoot
抽象類或者實(shí)現(xiàn)org.springframework.security.access.expression.SecurityExpressionHandler<T>
接口。
3. Web Security(Web安全)
3.1 Web Security基本組件
Spring安全框架在Web層的組件都是基于Servelt規(guī)范中的Filters
,所以事先弄明白Filters
所扮演的角色對與我們理解Web安全是有幫助的。下面的圖片展示了Http請求處理器的層級結(jié)構(gòu)。
客戶端發(fā)送請求到應(yīng)用,容器決定哪些fitlers以及哪一個servlet可以用來處理這次請求。絕大數(shù)情況下,一個servlet只能處理一種請求,但是這些filters組成了一個鏈,他們按照一定的順序排列,如果某一個filter想要處理這個請求,那么他可以將這個請求攔截下來,并且進(jìn)行處理。一個filter也能夠改變request請求或者response回復(fù)。Filter鏈的順序是非常重要的,Spring Boot有兩種機(jī)制在管理filter的順序 ,一種是在使用@Bean
注解發(fā)布一個Filter
的時候,同時使用@Order
注解來指定這個filter的優(yōu)先級(優(yōu)先級是由一個int
類型的整數(shù)表示,數(shù)值越大,優(yōu)先級越高),或者讓這個Filter
直接實(shí)現(xiàn)org.springframework.core.Ordered
接口,通過getOrder()
方法返回優(yōu)先級數(shù)值,另一種方法是使用org.springframework.boot.web.servlet.FilterRegistrationBean
注冊Filter
的時候,使用他的相關(guān)API來指定要注冊的Filter
的優(yōu)先級。一些標(biāo)準(zhǔn)的filters
通過定義一些常量值來確定他們之間的順序(比如Spring Session框架中的SessionRepositoryFilter
組件,默認(rèn)的優(yōu)先級數(shù)值由其自身定義的常量DEFAULT_ORDER
來表示,其值為Integer.MIN_VALUE + 50
,這個值只比int
型整數(shù)的最小值大一點(diǎn),因此這個過濾器幾乎是排在過濾器鏈的最下面,要到達(dá)這里,必須先通過其他過濾器)。
Spring安全的核心組件就是一個安裝到這個過濾器鏈中的Filter
,他的具體類型是org.springframework.security.web.FilterChainProxy
,稍后我們將會詳細(xì)說明這個安全過濾器。在一個Spring Boot應(yīng)用中,安全過濾器(security filter)是ApplicationContext
的一個@Bean
,一旦開啟了Spring安全功能,就會默認(rèn)安裝這個安全過濾器,并且攔截所有的請求。安全過濾器在過濾器鏈中的位置由org.springframework.boot.autoconfigure.security.SecurityProperties.DEFAULT_FILTER_ORDER
表示的優(yōu)先級數(shù)值來決定,這個值位于錨點(diǎn)org.springframework.boot.web.servlet.FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER
的下方(這個值是Spring Boot應(yīng)用中最大的過濾器優(yōu)先級數(shù)值,因?yàn)镾pring Boot希望請求在通過整個處理流程之前,先被這個過濾器包裝一下,改變一下行為)。從下圖我們可以看到,Spring安全框架所提供的功能由單個Filter
來提供,但是在這個Filter
中,包含著多個內(nèi)部filters
,并且每一個都具有特定的功能。圖片如下:
在Spring應(yīng)用中,過濾器通常是安裝在類型為org.springframework.web.filter.DelegatingFilterProxy
的代理容器中,這種容器并不以Spring的@Bean
的形式存在,而是作為原生的Servlet規(guī)范中的Filter
組件安裝到Servlet容器中。Spring安全的過濾器組件就是安裝在這種代理容器中,是一個類型為org.springframework.security.web.FilterChainProxy
且具有固定名字springSecurityFilterChain
的過濾器,這個安全過濾器是以Spring@Bean
的形式存在的。而springSecurityFilterChain
過濾器又包含了一個封裝了安全邏輯的有序過濾器鏈,組成這個鏈的過濾器都有相同的API(通常是實(shí)現(xiàn)了Servlet規(guī)范中Filter
接口)并且每一個過濾器都可能將請求攔截到自己這一層并進(jìn)行處理。所以,事實(shí)上的安全層可不止一層。
當(dāng)然,springSecurityFilterChain
也可能會管理多個不同的過濾器鏈,也就是包含一個過濾器鏈的列表,并且,所有這些過濾器對容器都是透明的。并且springSecurityFilterChain
會將請求派發(fā)給第一個匹配的過濾器鏈。下圖展示了基于請求路徑的派發(fā)過程(這也是使用最多但并不唯一的方式)。這種派發(fā)過程的最重要的特點(diǎn)就是有且只有一個過濾器鏈來處理這個請求。
一個沒有任何定制化配置的Spring Boot應(yīng)用具有6個過濾器鏈,前5個過濾器鏈只會忽略那些指向靜態(tài)資源的路徑,例如/css/**
和/images/**
,以及用來展示錯誤信息視圖的路徑/error
(這些忽略的路徑可以在SecurityProperties
配置bean中,使用security.ignored
來控制)。最后一個過濾器鏈匹配所有的路徑/**
并且也是最活躍的,包含認(rèn)證和授權(quán)的邏輯,錯誤處理,會話處理,頭部信息處理等。在這些默認(rèn)的過濾器鏈中有總共11個過濾器,通常情況下,用戶無需去關(guān)心哪個過濾器被使用了以及是什么時候使用的。
注意
Spring安全的所有內(nèi)部過濾器對于容器來說都是透明的,這很重要,特別是Spring Boot應(yīng)用,因?yàn)樗?code>Filter類型的@Bean
都是由容器自動注冊的。所以如果你想要添加自定義的安全過濾器到Spring安全的過濾器鏈中,那么你最好不要通過配置Filter
類型的@Bean
的方式來添加,因?yàn)檫@樣Spring應(yīng)用會把過濾器注冊到容器中而不是添加到Spring安全的過濾器鏈中,你可以通過將自定義的安全過濾器封裝在FilterRegistrationBean
中來達(dá)成目的。
3.2 Creating and Customizing Filter Chains(創(chuàng)建和定制過濾器鏈)
在Spring Boot應(yīng)用程序中,有一個默認(rèn)的后備過濾器鏈(該過濾器鏈匹配所有的請求路徑/**
)有一個預(yù)定義的順序值SecurityProperties.BASIC_AUTH_ORDER
,你可以通過設(shè)置security.basic.enabled=false
來徹底關(guān)閉它,或者你可以只把這個過濾器當(dāng)作一個定義了一些其他規(guī)則且具有一個低優(yōu)先級的后備。如果想要這樣做的話,只需要添加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/**")
...;
}
}
上例中的@Configuration
的bean
將會使Spring安全框架添加一個優(yōu)先級排在后備過濾器鏈前面的新過濾器鏈。
許多的應(yīng)用程序擁有訪問規(guī)則互不相同的資源組。例如:一個應(yīng)用程序提供的資源包括用戶UI和后臺API接口兩個部分,對于用戶UI,支持基于Cookie的認(rèn)證,而未認(rèn)證的請求會重定向至登陸頁面;而對于后臺API接口,則支持基于令牌的認(rèn)證,未認(rèn)證的請求會收到攜帶401狀態(tài)碼的回復(fù)。每一個資源組都有他自己的WebSecurityConfigurerAdapter
,并且具有唯一的優(yōu)先級以及請求路徑的匹配規(guī)則。如果匹配規(guī)則發(fā)生重疊,那么優(yōu)先級更高的過濾器鏈將會勝出。
3.3 Request Matching for Dispatch and Authorization(針對派發(fā)和授權(quán)的請求匹配)
一個安全過濾器鏈(等同與一個WebSecurityConfigurerAdapter
)持有一個請求匹配器,這個匹配器被用來決定該過濾器鏈?zhǔn)欠襁m用于當(dāng)前的HTTP請求。一旦一個HTTP請求適用與一個特定的過濾器鏈,其他的過濾器鏈則不會被應(yīng)用于這個HTTP請求。但是在一個過濾器鏈內(nèi)部,你可以使用HttpSecurity
來配置額外的匹配器,這樣你就可以擁有更細(xì)粒度的授權(quán)控制。例如:
@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安全的時候最容易犯的一個錯誤就是忘記了這些匹配器將應(yīng)用于不同的程序,一個是請求匹配器,將應(yīng)用于整個過濾器鏈,而其他的匹配器僅僅是用來選擇訪問規(guī)則。
3.4 Combining Application Security Rules with Actuator Rules(應(yīng)用程序的安全規(guī)則與監(jiān)控規(guī)則的整合)
如果你在使用Spring Boot Actuator來監(jiān)控應(yīng)用程序的端點(diǎn)(即由path所指向的資源),你應(yīng)該希望他們是安全的并且默認(rèn)他們是安全的。實(shí)際上,當(dāng)你將Spring Boot監(jiān)控功能添加到一個安全的應(yīng)用程序中時,同時會添加一個過濾器鏈,而這個過濾器鏈只會攔截訪問Spring Boot監(jiān)控端點(diǎn)路徑的請求。這個過濾器鏈定義了一個請求匹配器,這個匹配器只匹配監(jiān)控端點(diǎn)路徑,并且具有一個值為ManagementServerProperties.BASIC_AUTH_ORDER
的優(yōu)先級,這個優(yōu)先級只比默認(rèn)的SecurityProperties
替補(bǔ)過濾器高一點(diǎn)(數(shù)值小5),所以匹配監(jiān)控端點(diǎn)路徑的請求會先到達(dá)這個過濾器鏈。
如果你想要你的應(yīng)用程序的安全規(guī)則應(yīng)用到監(jiān)控功能端點(diǎn)上,你可以添加一個優(yōu)先級高于監(jiān)控端點(diǎn)過濾器鏈的新的過濾器鏈,并且讓這個新過濾器攔截所有訪問監(jiān)控端點(diǎn)的請求。如果你更傾向于對監(jiān)控端點(diǎn)使用默認(rèn)的安全配置,那么最簡單的做法就是將你自己的過濾器鏈添加到監(jiān)控接口過濾器的后面和替補(bǔ)過濾器的前面。示例如下:
@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是綁定的,所以現(xiàn)階段,Spring安全框架只能應(yīng)用于基于Servlet規(guī)范,運(yùn)行在Servlet容器中的應(yīng)用程序,而無論容器是否是嵌入式的。當(dāng)然,Spring安全框架并沒有與Spring MVC框架或者Spring的Web技術(shù)棧綁定,所以他能夠應(yīng)用于所有基于Servlet規(guī)范的應(yīng)用程序。
4. Method Security(方法安全)
Spring安全框架不僅僅支持Web應(yīng)用程序,也為Java方法的執(zhí)行提供安全的訪問規(guī)則支持。對于Spring安全來說,Java方法只是一種其他形式的“受保護(hù)資源”。這就意味著方法的訪問規(guī)則是與ConfigAttribute
形式一樣的字符串(比如角色或者表達(dá)式),只是應(yīng)用在你的代碼不同的地方。如何引入方法安全功能呢?第一步就是開啟方法安全功能,例如:
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {
}
然后我們就可以直接在方法資源上加注解,例如:
@Service
public class MyService {
@Secured("ROLE_USER")
public String secure() {
return "Hello Security";
}
}
上面的示例是一個含有安全方法的業(yè)務(wù)類。如果Spring像上面的例子那樣創(chuàng)建這種類型的@Bean
,那么在這些方法真正被執(zhí)行之前,這個類會被代理,同時調(diào)用者將需要先通過一個安全的攔截器。如果訪問被拒絕,那么調(diào)用者將會得到一個AccessDeniedException
而非該方法的正確執(zhí)行結(jié)果。
當(dāng)然,還有其他的注解類型可以應(yīng)用在方法來執(zhí)行安全限制,比如@PreAuthorize
和@PostAuthorize
,這些注解都允許你編寫含有指向方法參數(shù)和方法返回值的引用的表達(dá)式。
提示
將Web安全和方法安全結(jié)合起來使用并非是不常用的。Web安全過濾器鏈提供用戶粒度的安全功能,例如認(rèn)證和重定向到登陸頁面,而方法安全能夠提供更細(xì)粒度的安全保證。
5. Working with Threads(工作線程)
Spring安全本身就是一個基本的線程邊界,因?yàn)楫?dāng)前身份被認(rèn)證后,仍然需要被各種下游消費(fèi)者所使用。最基本的構(gòu)造塊是org.springframework.security.core.context.SecurityContext
的實(shí)例對象,他包含了一個Authentication
對象(并且如果用戶已經(jīng)登陸,那么這個Authentication
是被明確標(biāo)記為authenticated
的)。你可以通過org.springframework.security.core.context.SecurityContextHolder
的靜態(tài)方法方便的訪問和使用保存在ThreadLocal
中的SecurityContext
實(shí)例。例如:
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
assert(authentication.isAuthenticated);
雖然對于用戶應(yīng)用程序來說,上面的代碼并不常用,但這并不代表他沒有用處,相反對于開發(fā)者來說,這段代碼非常有用,比如在開發(fā)者想要定制化編寫一個認(rèn)證過濾器的時候(盡管認(rèn)證過濾器在Spring安全中是最基本的類,開發(fā)者編寫該類的時候應(yīng)該盡量避免使用SecurityContextHolder
).
如果你想要在一個Web接口中訪問當(dāng)前的已認(rèn)證用戶,你可以在@RequestMapping
使用方法參數(shù)注解@AuthenticationPrincipal
來注入持有用戶信息的對象。例如:
@RequestMapping("/foo")
public String foo(@AuthenticationPrincipal User user) {
... // do stuff with user
}
@AuthenticationPrincipal
注解會SecurityContext
實(shí)例中抽取當(dāng)前Authentication
對象并且調(diào)用其getPrincipal()
方法來獲取用戶身份對象,然后將用戶身份對象注入到方法參數(shù)中。Authentication
持有的Principal
(用戶身份)的類型取決于AuthenticationManager
驗(yàn)證認(rèn)證時所使用的類型。
如果Spring安全從HttpServletRequest
中獲取的Principal
就是Authentication
類型的,那么開發(fā)者可以用這種方式來直接使用:
@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
成為線程邊界后,如果你想要調(diào)用安全的方法做一些異步的后臺操作,比如使用@Async
注解,那么你需要確保這個上下文是可傳播的。說白了就是將SecurityContext
封裝到task(Runnable
,Callable
等)中,然后交給后面的線程去執(zhí)行。Spring安全提供了一些輔助類來幫助開發(fā)者更方便地完成這個過程。當(dāng)然,為了傳播SecurityContext
到@Async
方法中,開發(fā)者需要提供一個AsyncConfigurer
同時要確保Executor
的正確類型:
@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
}
}