Spring Security
學(xué)習(xí) Spring Security 用法题画,架構(gòu)婴程、設(shè)計(jì)模式等档叔。
核心組件
主要介紹 Spring Security
中常見核心 Java
類以及他們之間的依賴關(guān)系衙四,以及整個(gè)架構(gòu)的設(shè)計(jì)原理押逼。
SecurityContextHolder
SecurityContextHolder
用于存儲(chǔ)安全上下文(security context)
的信息挑格。當(dāng)前操作的用戶是誰漂彤,該用戶是否已經(jīng)被認(rèn)證,他擁有哪些角色權(quán)限…這些都被保存在 SecurityContextHolder
中媳板。SecurityContextHolder
默認(rèn)使用 ThreadLocal
策略來存儲(chǔ)認(rèn)證信息蛉幸【拊担看到 ThreadLocal
也就意味著,這是一種與線程綁定的策略又固。Spring Security
在用戶登錄時(shí)自動(dòng)綁定認(rèn)證信息到當(dāng)前線程仰冠,在用戶退出時(shí),自動(dòng)清除當(dāng)前線程的認(rèn)證信息昼捍。但這一切的前提担锤,是你在 web
場景下使用 Spring Security
肛循。
獲取當(dāng)前用戶的信息
因?yàn)樯矸菪畔⑹桥c線程綁定的累舷,所以可以在程序的任何地方使用靜態(tài)方法獲取用戶信息笋粟。一個(gè)例子如下:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
getAuthentication()
返回了認(rèn)證信息,再次 getPrincipal()
返回了身份信息闷畸,UserDetails
便是 Spring
對(duì)身份信息封裝的一個(gè)接口佑菩。Authentication
和 UserDetails
的介紹在下面的小節(jié)具體講解,本節(jié)重要的內(nèi)容是介紹 SecurityContextHolder
這個(gè)容器绞幌。
Authentication
直接上源碼:
package org.springframework.security.core;// <1>
public interface Authentication extends Principal, Serializable { // <1>
Collection<? extends GrantedAuthority> getAuthorities(); // <2>
Object getCredentials();// <2>
Object getDetails();// <2>
Object getPrincipal();// <2>
boolean isAuthenticated();// <2>
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
<1> Authentication 是 spring security 包中的接口莲蜘,直接繼承自 Principal 類,而 Principal 是位于 java.security 包中的问顷《耪可以見得羞芍,Authentication 在 spring security 中是最高級(jí)別的身份/認(rèn)證的抽象唯咬。
<2> 由這個(gè)頂級(jí)接口胆胰,我們可以得到用戶擁有的權(quán)限信息列表,密碼厚柳,用戶細(xì)節(jié)信息别垮,用戶身份信息碳想,認(rèn)證信息。
還記得1.1節(jié)中龙填,authentication.getPrincipal()返回了一個(gè) Object觅够,我們將 Principal 強(qiáng)轉(zhuǎn)成了 Spring Security 中最常用的UserDetails喘先,這在 Spring Security 中非常常見,接口返回 Object涤姊,使用 instanceof 判斷類型思喊,強(qiáng)轉(zhuǎn)成對(duì)應(yīng)的具體實(shí)現(xiàn)類恨课。接口詳細(xì)解讀如下:
- getAuthorities()剂公,權(quán)限信息列表纲辽,默認(rèn)是 GrantedAuthority 接口的一些實(shí)現(xiàn)類鳞上,通常是代表權(quán)限信息的一系列字符串因块。
- getCredentials(),密碼信息拒名,用戶輸入的密碼字符串,在認(rèn)證過后通常會(huì)被移除同云,用于保障安全炸站。
- getDetails()旱易,細(xì)節(jié)信息,web 應(yīng)用中的實(shí)現(xiàn)接口通常為 WebAuthenticationDetails忌堂,它記錄了訪問者的 ip 地址和 sessionId 的值士修。
- getPrincipal()登淘,敲黑板G荨A髌蕖!最重要的身份信息在辆,大部分情況下返回的是 UserDetails 接口的實(shí)現(xiàn)類浑度,也是框架中的常用接口之一箩张。UserDetails 接口將會(huì)在下面的小節(jié)重點(diǎn)介紹。
Spring Security是如何完成身份認(rèn)證的论熙?
- 用戶名和密碼被過濾器獲取到赴肚,封裝成
Authentication
,通常情況下是UsernamePasswordAuthenticationToken
這個(gè)實(shí)現(xiàn)類誉券。 -
AuthenticationManager
身份管理器負(fù)責(zé)驗(yàn)證這個(gè)Authentication
- 認(rèn)證成功后,
AuthenticationManager
身份管理器返回一個(gè)被填充滿了信息的(包括上面提到的權(quán)限信息商玫,身份信息袭异,細(xì)節(jié)信息御铃,但密碼通常會(huì)被移除)Authentication
實(shí)例。 -
SecurityContextHolder
安全上下文容器將第3步填充了信息的Authentication
睡互,通過SecurityContextHolder.getContext().setAuthentication(…)
方法,設(shè)置到其中嗓违。
這是一個(gè)抽象的認(rèn)證流程冕广,而整個(gè)過程中撒汉,如果不糾結(jié)于細(xì)節(jié)挠阁,其實(shí)只剩下一個(gè) AuthenticationManager
是我們沒有接觸過的了,這個(gè)身份管理器我們在后面的小節(jié)介紹丰刊。
AuthenticationManager
初次接觸 Spring Security
的朋友相信會(huì)被 AuthenticationManager
寻歧,ProviderManager
猾封,AuthenticationProvider
…這么多相似的 Spring
認(rèn)證類搞得暈頭轉(zhuǎn)向,但只要稍微梳理一下就可以理解清楚它們的聯(lián)系和設(shè)計(jì)者的用意枚钓。AuthenticationManager
(接口)是認(rèn)證相關(guān)的核心接口搀捷,也是發(fā)起認(rèn)證的出發(fā)點(diǎn),因?yàn)樵趯?shí)際需求中家厌,我們可能會(huì)允許用戶使用用戶名+密碼登錄,同時(shí)允許用戶使用郵箱+密碼掰吕,手機(jī)號(hào)碼+密碼登錄殖熟,甚至,可能允許用戶使用指紋登錄(還有這樣的操作斑响?沒想到吧)菱属,所以說 AuthenticationManager
一般不直接認(rèn)證,AuthenticationManager
接口的常用實(shí)現(xiàn)類 ProviderManager
內(nèi)部會(huì)維護(hù)一個(gè) List<AuthenticationProvider>
列表舰罚,存放多種認(rèn)證方式纽门,實(shí)際上這是委托者模式的應(yīng)用(Delegate
)。也就是說沸停,核心的認(rèn)證入口始終只有一個(gè):AuthenticationManager
,不同的認(rèn)證方式:用戶名+密碼(UsernamePasswordAuthenticationToken
)败玉,郵箱+密碼血淌,手機(jī)號(hào)碼+密碼登錄則對(duì)應(yīng)了三個(gè) AuthenticationProvider
乳蓄。這樣一來四不四就好理解多了?熟悉 shiro
的朋友可以把AuthenticationProvider
理解成 Realm
赠叼。在默認(rèn)策略下贯被,只需要通過一個(gè) AuthenticationProvider
的認(rèn)證诵姜,即可被認(rèn)為是登錄成功。
ProviderManager 關(guān)鍵部分源碼
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
// 維護(hù)一個(gè)AuthenticationProvider列表
private List<AuthenticationProvider> providers = Collections.emptyList();
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
// 依次認(rèn)證
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
...
catch (AuthenticationException e) {
lastException = e;
}
}
// 如果有Authentication信息微饥,則直接返回
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
//移除密碼
((CredentialsContainer) result).eraseCredentials();
}
//發(fā)布登錄成功事件
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
...
//執(zhí)行到此叉袍,說明沒有認(rèn)證成功姐呐,包裝異常信息
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
}
ProviderManager
中的 List
,會(huì)依照次序去認(rèn)證新锈,認(rèn)證成功則立即返回墩新,若認(rèn)證失敗則返回 null
,下一個(gè) AuthenticationProvider
會(huì)繼續(xù)嘗試認(rèn)證,如果所有認(rèn)證器都無法認(rèn)證成功挤茄,則 ProviderManager
會(huì)拋出一個(gè)ProviderNotFoundException
異常。
以上已經(jīng)把 Spring Security 的整個(gè)認(rèn)證流程都講述了一遍肥哎,簡單小結(jié)如下:身份信息的存放容器 SecurityContextHolder
达椰,身份信息的抽象 Authentication
栓辜,身份認(rèn)證器 AuthenticationManager
及其認(rèn)證流程贩毕。姑且在這里做一個(gè)分隔線集绰。下面來介紹下 AuthenticationProvider
接口的具體實(shí)現(xiàn)仰猖。
DaoAuthenticationProvider
AuthenticationProvider
最最最常用的一個(gè)實(shí)現(xiàn)便是 DaoAuthenticationProvider
。顧名思義,Dao
正是數(shù)據(jù)訪問層的縮寫,也暗示了這個(gè)身份認(rèn)證器的實(shí)現(xiàn)思路湃鹊。由于本文是一個(gè) Overview
蛤奢,姑且只給出其 UML
類圖:
按照我們最直觀的思路,怎么去認(rèn)證一個(gè)用戶呢当纱?用戶前臺(tái)提交了用戶名和密碼坪稽,而數(shù)據(jù)庫中保存了用戶名和密碼,認(rèn)證便是負(fù)責(zé)比對(duì)同一個(gè)用戶名顷帖,提交的密碼和保存的密碼是否相同便是了。在Spring Security
中。提交的用戶名和密碼,被封裝成了UsernamePasswordAuthenticationToken
,而根據(jù)用戶名加載用戶的任務(wù)則是交給了 UserDetailsService
皆刺,在DaoAuthenticationProvider
中,對(duì)應(yīng)的方法便是 retrieveUser
凌摄,雖然有兩個(gè)參數(shù)羡蛾,但是 retrieveUser
只有第一個(gè)參數(shù)起主要作,返回一個(gè) UserDetails
锨亏。還需要完成 UsernamePasswordAuthenticationToken
和 UserDetails
密碼的比對(duì)林说,這便是交給 additionalAuthenticationChecks
方法完成的,如果這個(gè) void
方法沒有拋異常屯伞,則認(rèn)為比對(duì)成功。比對(duì)密碼的過程豪直,用到了PasswordEncoder
和 SaltSource
劣摇,密碼加密和鹽的概念相信不用我贅述了,它們?yōu)楸U习踩O(shè)計(jì)弓乙,都是比較基礎(chǔ)的概念末融。
如果你已經(jīng)被這些概念搞得暈頭轉(zhuǎn)向了,不妨這么理解 DaoAuthenticationProvider
:它獲取用戶提交的用戶名和密碼暇韧,比對(duì)其正確性勾习,如果正確,返回一個(gè)數(shù)據(jù)庫中的用戶信息(假設(shè)用戶信息被保存在數(shù)據(jù)庫中)懈玻。
UserDetails 與 UserDetailsService
上面不斷提到了 UserDetails
這個(gè)接口巧婶,它代表了最詳細(xì)的用戶信息,這個(gè)接口涵蓋了一些必要的用戶信息字段涂乌,具體的實(shí)現(xiàn)類對(duì)它進(jìn)行了擴(kuò)展艺栈。
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
它和 Authentication
接口很類似,比如它們都擁有 username
湾盒,authorities
湿右,區(qū)分他們也是本文的重點(diǎn)內(nèi)容之一。Authentication
的 getCredentials()
與 UserDetails
中的 getPassword()
需要被區(qū)分對(duì)待罚勾,前者是用戶提交的密碼憑證毅人,后者是用戶正確的密碼,認(rèn)證器其實(shí)就是對(duì)這兩者的比對(duì)尖殃。Authentication
中的 getAuthorities()
實(shí)際是由 UserDetails
的 getAuthorities()
傳遞而形成的丈莺。還記得 Authentication
接口中的 getUserDetails()
方法嗎?其中的 UserDetails
用戶詳細(xì)信息便是經(jīng)過了 AuthenticationProvider
之后被填充的分衫。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService
和 AuthenticationProvider
兩者的職責(zé)常常被人們搞混场刑,關(guān)于他們的問題在文檔的 FAQ
和 issues
中屢見不鮮。記住一點(diǎn)即可,敲黑板GO帧n戆谩!UserDetailsService
只負(fù)責(zé)從特定的地方(通常是數(shù)據(jù)庫)加載用戶信息瞎疼,僅此而已科乎,記住這一點(diǎn),可以避免走很多彎路贼急。UserDetailsService
常見的實(shí)現(xiàn)類有 JdbcDaoImpl
茅茂,InMemoryUserDetailsManager
,前者從數(shù)據(jù)庫加載用戶太抓,后者從內(nèi)存中加載用戶空闲,也可以自己實(shí)現(xiàn) UserDetailsService
,通常這更加靈活走敌。
架構(gòu)概覽圖
為了更加形象的理解上述我介紹的這些核心類碴倾,附上一張按照我的理解,所畫出 Spring Security
的一張非典型的 UML
圖
整個(gè)Spring Security 都是圍繞以上這張架構(gòu)圖展開的掉丽,最頂層為最核心最抽象的 AuthenticationManager
接口跌榔, ProviderManager
為 AuthenticationManager
的一個(gè)具體實(shí)現(xiàn),功能如其名字他的作用是管理 Provider
的捶障, AuthenticationProvider
才是真正認(rèn)證的接口僧须,因此我們在實(shí)踐中要實(shí)現(xiàn)我們自己的認(rèn)證方式,也就是 AuthenticationProvider
的一個(gè)具體實(shí)現(xiàn)项炼。當(dāng)然可以實(shí)現(xiàn)多個(gè)担平,如果有多種認(rèn)證方式,現(xiàn)實(shí)中往往也是有多種認(rèn)證方式锭部。
注意 AuthenticationProvider
接口中的 supports
方法驱闷,ProviderManager
里面其實(shí)是維護(hù)了一個(gè) AuthenticationProvider
的 List
因此到底使用哪個(gè) Provider
來做驗(yàn)證,可以使用 Filter
返回的 Authentication
實(shí)現(xiàn)類來限定空免,部分代碼如下:
//Filter 代碼
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
......
return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token));
}
//Provider 代碼
@Override
public boolean supports(Class<?> authentication) {
return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
}
//在 ProviderManager 中驗(yàn)證時(shí)有如下代碼
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
如果對(duì)Spring Security
的這些概念感到理解不能空另,不用擔(dān)心,因?yàn)檫@是 Architecture First
導(dǎo)致的必然結(jié)果蹋砚,先過個(gè)眼熟扼菠。后續(xù)的文章會(huì)秉持 Code First
的理念,陸續(xù)詳細(xì)地講解這些實(shí)現(xiàn)類的使用場景坝咐,源碼分析循榆,以及最基本的:如何配置 Spring Security
,在后面可以時(shí)不時(shí)往回看一看墨坚,找到具體的類在整個(gè)架構(gòu)中所處的位置秧饮。另外,一些 Spring Security
的過濾器還未囊括在架構(gòu)概覽中,如將表單信息包裝成 UsernamePasswordAuthenticationToken
的過濾器盗尸,考慮到這些雖然也是架構(gòu)的一部分柑船,但是真正重寫他們的可能性較小,所以打算放到后面講解泼各。
配置介紹
Security 安全核心配置例子:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin").password("admin").roles("USER");
}
}
當(dāng)配置了上述的javaconfig之后鞍时,我們的應(yīng)用便具備了如下的功能:
- 除了“/”,”/home”(首頁),”/login”(登錄),”/logout”(注銷),之外,其他路徑都需要認(rèn)證扣蜻。
- 指定“/login”該路徑為登錄頁面逆巍,當(dāng)未認(rèn)證的用戶嘗試訪問任何受保護(hù)的資源時(shí),都會(huì)跳轉(zhuǎn)到“/login”莽使。
- 默認(rèn)指定“/logout”為注銷頁面
- 配置一個(gè)內(nèi)存中的用戶認(rèn)證器锐极,使用admin/admin作為用戶名和密碼,具有USER角色
- 防止CSRF攻擊
- Session Fixation protection
- Security Header(添加一系列和Header相關(guān)的控制)
- HTTP Strict Transport Security for secure requests
- 集成X-Content-Type-Options
- 緩存控制
- 集成X-XSS-Protection.aspx)
- X-Frame-Options integration to help prevent Clickjacking(iframe被默認(rèn)禁止使用)
- 為Servlet API集成了如下的幾個(gè)方法
- HttpServletRequest#getRemoteUser())
- HttpServletRequest.html#getUserPrincipal())
- HttpServletRequest.html#isUserInRole(java.lang.String))
- HttpServletRequest.html#login(java.lang.String, java.lang.String))
- HttpServletRequest.html#logout())
一個(gè) Restful 配置
動(dòng)態(tài)從數(shù)據(jù)庫加載權(quán)限芳肌,這邊因?yàn)?Spring Security
的機(jī)制是在模塊啟動(dòng)的時(shí)候進(jìn)行加載的溪烤,如果想要?jiǎng)討B(tài) reload
權(quán)限,調(diào)查來看Spring 并沒有提供相關(guān)的接口庇勃,需要?jiǎng)討B(tài) reload Spring
相關(guān)的 bean
是一種比較危險(xiǎn)暴力的做法,需要多加注意槽驶。
@Override
protected void configure(HttpSecurity http) throws Exception {
List<String> permitAllEndpointList = Arrays.asList(AUTHENTICATION_URL,"/api/*/webjars/**","/api/*/swagger**","/api/*/swagger-resources/**","/api/*/v2/api-docs");
//load Authorities from DB
ResponseData<List<Authority>> authorityRules = authorityService.getAuthority();
try {
for (AuthorityModel rule : authorityRules.getData()) {
//這里因?yàn)?Restful API url 有相同的情況责嚷,因此需要URL和方法名組合來區(qū)分,注意一種特殊情況掂铐,
//因?yàn)槭鞘褂肁nt語法來匹配URL罕拂,如果出現(xiàn)請求方法相同且URL是匹配子集關(guān)系時(shí),要把最具體的URL放在
//前面全陨,比如同是 GET 方法爆班,Api1:/api/user/paged 和 Api2:/api/user/{id},在Ant語法里面
//Api2是能匹配Api1的辱姨,如果用戶有Api2的權(quán)限而沒有Api1的權(quán)限柿菩,在如下初始化權(quán)限時(shí)Api2初始化的
//順序在Api1的前面,就會(huì)導(dǎo)致用戶即使沒有Api1的權(quán)限也能訪問雨涛,因此要確保數(shù)據(jù)庫加載的時(shí)候Api1在Api2
//前面
if (HttpMethod.POST.name().equals(rule.getMethod().name())) {
http
.authorizeRequests()
.antMatchers(HttpMethod.POST, rule.getPattern()).hasAuthority(rule.getSystemName());
} else if (HttpMethod.GET.name().equals(rule.getMethod().name())) {
http
.authorizeRequests()
.antMatchers(HttpMethod.GET, rule.getPattern()).hasAuthority(rule.getSystemName());
} else if (HttpMethod.PUT.name().equals(rule.getMethod().name())) {
http
.authorizeRequests()
.antMatchers(HttpMethod.PUT, rule.getPattern()).hasAuthority(rule.getSystemName());
} else if (HttpMethod.DELETE.name().equals(rule.getMethod().name())) {
http
.authorizeRequests()
.antMatchers(HttpMethod.DELETE, rule.getPattern()).hasAuthority(rule.getSystemName());
}
}
} catch (Exception e) {
log.error("", e);
}
http
.csrf().disable() // We don't need CSRF for JWT based authentication
.exceptionHandling().authenticationEntryPoint(this.authenticationEntryPoint)
//自定義沒有權(quán)限時(shí)的處理枢舶,一般根據(jù)業(yè)務(wù)封裝自定義的返回結(jié)果
.accessDeniedHandler(ajaxAccessDeniedHandler)
.and()
//Restful API 完全無狀態(tài),使用JWT token
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//定義哪些API不需要認(rèn)證替久,比如獲取 Token 的接口
.authorizeRequests()
.antMatchers(permitAllEndpointList.toArray(new String[permitAllEndpointList.size()])).permitAll()
.and()
// Protected API End-points
.authorizeRequests()
.antMatchers(API_ROOT_URL, API_ATTACHMENT_URL).authenticated()
.and()
//處理跨域過濾器
.addFilterBefore(new CustomCorsFilter(), UsernamePasswordAuthenticationFilter.class)
//處理登錄獲取token的過濾器
.addFilterBefore(buildAjaxLoginProcessingFilter(AUTHENTICATION_URL), UsernamePasswordAuthenticationFilter.class)
//驗(yàn)證用戶token凉泄,以及用戶是否有接口訪問權(quán)限的過濾器
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(permitAllEndpointList, Arrays.asList(API_ROOT_URL,API_ATTACHMENT_URL)), UsernamePasswordAuthenticationFilter.class);
}
具體解釋見代碼上的注釋
@EnableWebSecurity
我們自己定義的配置類 WebSecurityConfig
加上了 @EnableWebSecurity
注解,同時(shí)繼承了 WebSecurityConfigurerAdapter
蚯根。你可能會(huì)在想誰的作用大一點(diǎn)后众,毫無疑問 @EnableWebSecurity
起到?jīng)Q定性的配置作用,它其實(shí)是個(gè)組合注解。
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,// <2>
SpringWebMvcImportSelector.class })// <1>
@EnableGlobalAuthentication // <3>
@Configuration
public @interface EnableWebSecurity {
boolean debug() default false;
}
- <1>
@Import
是springboot
提供的用于引入外部的配置的注解蒂誉,可以理解為:@EnableWebSecurity
注解激活了@Import
注解中包含的配置類教藻。 - <2>
WebSecurityConfiguration
顧名思義,是用來配置web
安全的拗盒,下面的小節(jié)會(huì)詳細(xì)介紹怖竭。 - <3>
@EnableGlobalAuthentication
注解的源碼如下:
@Import(AuthenticationConfiguration.class)
@Configuration
public @interface EnableGlobalAuthentication {
}
注意點(diǎn)同樣在 @Import
之中,它實(shí)際上激活了 AuthenticationConfiguration
這樣的一個(gè)配置類陡蝇,用來配置認(rèn)證相關(guān)的核心類痊臭。
也就是說:@EnableWebSecurity
完成的工作便是加載了WebSecurityConfiguration
,AuthenticationConfiguration
這兩個(gè)核心配置類登夫,也就此將 spring security
的職責(zé)劃分為了配置安全信息广匙,配置認(rèn)證信息兩部分。
WebSecurityConfiguration
在這個(gè)配置類中恼策,有一個(gè)非常重要的Bean被注冊了鸦致。
@Configuration
public class WebSecurityConfiguration {
//DEFAULT_FILTER_NAME = "springSecurityFilterChain"
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
...
}
}
在未使用springboot
之前,大多數(shù)人都應(yīng)該對(duì)springSecurityFilterChain
這個(gè)名詞不會(huì)陌生涣楷,他是spring security
的核心過濾器分唾,是整個(gè)認(rèn)證的入口。在曾經(jīng)的XML
配置中狮斗,想要啟用spring security
绽乔,需要在web.xml
中進(jìn)行如下配置:
<!-- Spring Security -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
而在springboot
集成之后,這樣的XML
被java
配置取代碳褒。WebSecurityConfiguration
中完成了聲明springSecurityFilterChain
的作用折砸,并且最終交給DelegatingFilterProxy
這個(gè)代理類,負(fù)責(zé)攔截請求(注意DelegatingFilterProxy
這個(gè)類不是spring security
包中的沙峻,而是存在于web
包中睦授,spring
使用了代理模式來實(shí)現(xiàn)安全過濾的解耦)。
AuthenticationConfiguration
@Configuration
@Import(ObjectPostProcessorConfiguration.class)
public class AuthenticationConfiguration {
@Bean
public AuthenticationManagerBuilder authenticationManagerBuilder(
ObjectPostProcessor<Object> objectPostProcessor) {
return new AuthenticationManagerBuilder(objectPostProcessor);
}
public AuthenticationManager getAuthenticationManager() throws Exception {
...
}
}
AuthenticationConfiguration
的主要任務(wù)摔寨,便是負(fù)責(zé)生成全局的身份認(rèn)證管理者 AuthenticationManager
去枷。還記得在《初識(shí) Spring Security 篇一》中,介紹了 Spring Security
的認(rèn)證體系是复,AuthenticationManager
便是最核心的身份認(rèn)證管理器沉填。
WebSecurityConfigurerAdapter
適配器模式在 spring
中被廣泛的使用,在配置中使用 Adapter
的好處便是佑笋,我們可以選擇性的配置想要修改的那一部分配置翼闹,而不用覆蓋其他不相關(guān)的配置。WebSecurityConfigurerAdapter
中我們可以選擇自己想要修改的內(nèi)容蒋纬,來進(jìn)行重寫猎荠,而其提供了三個(gè) configure
重載方法坚弱,是我們主要關(guān)心的:
/**WebSecurityConfigurerAdapter**/
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
this.disableLocalConfigureAuthenticationBldr = true;
}
...
/**
* Override this method to configure {@link WebSecurity}. For example, if you wish to
* ignore certain requests.
*/
public void configure(WebSecurity web) throws Exception {
}
/**
* Override this method to configure the {@link HttpSecurity}. Typically subclasses
* should not invoke this method by calling super as it may override their
* configuration. The default configuration is:
*
* <pre>
* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
* </pre>
*
* @param http the {@link HttpSecurity} to modify
* @throws Exception if an error occurs
*/
// @formatter:off
protected void configure(HttpSecurity http) throws Exception {
logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
由參數(shù)就可以知道,分別是對(duì) AuthenticationManagerBuilder
关摇,WebSecurity
荒叶,HttpSecurity
進(jìn)行個(gè)性化的配置。
HttpSecurity常用配置
@Configuration
@EnableWebSecurity
public class CustomWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/resources/**", "/signup", "/about").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
.anyRequest().authenticated()
.and()
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.failureForwardUrl("/login?error")
.loginPage("/login")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/index")
.permitAll()
.and()
.httpBasic()
.disable();
}
}
上述是一個(gè)使用 Java Configuration
配置 HttpSecurity
的典型配置输虱,其中 http
作為根開始配置些楣,每一個(gè) and()
對(duì)應(yīng)了一個(gè)模塊的配置(等同于xml配置中的結(jié)束標(biāo)簽),并且 and()
返回了 HttpSecurity
本身宪睹,于是可以連續(xù)進(jìn)行配置愁茁。他們配置的含義也非常容易通過變量本身來推測
- authorizeRequests()配置路徑攔截,表明路徑訪問所對(duì)應(yīng)的權(quán)限亭病,角色鹅很,認(rèn)證信息。
- formLogin()對(duì)應(yīng)表單認(rèn)證相關(guān)的配置
- logout()對(duì)應(yīng)了注銷相關(guān)的配置
- httpBasic()可以配置basic登錄
他們分別代表了 http
請求相關(guān)的安全配置罪帖,這些配置項(xiàng)無一例外的返回了 Configurer
類促煮,而所有的 http
相關(guān)配置可以通過查看HttpSecurity
的主要方法得知:
public LogoutConfigurer<HttpSecurity> logout() throws Exception {
return getOrApply(new LogoutConfigurer<HttpSecurity>());
}
......
public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
ApplicationContext context = getContext();
return getOrApply(new CsrfConfigurer<HttpSecurity>(context));
}
需要對(duì) http
協(xié)議有一定的了解才能完全掌握所有的配置,不過整袁,springboot
和spring security
的自動(dòng)配置已經(jīng)足夠使用了菠齿。其中每一項(xiàng) Configurer
(e.g.FormLoginConfigurer,CsrfConfigurer)都是 HttpConfigurer
的細(xì)化配置項(xiàng)。
WebSecurityBuilder
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/resources/**");
}
}
以筆者的經(jīng)驗(yàn)坐昙,這個(gè)配置中并不會(huì)出現(xiàn)太多的配置信息绳匀。
AuthenticationManagerBuilder
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin").password("admin").roles("USER");
}
}
想要在 WebSecurityConfigurerAdapter
中進(jìn)行認(rèn)證相關(guān)的配置,可以使用 configure(AuthenticationManagerBuilder auth)
暴露一個(gè) AuthenticationManager
的建造器:AuthenticationManagerBuilder
民珍。如上所示,我們便完成了內(nèi)存中用戶的配置盗飒。
細(xì)心的朋友會(huì)發(fā)現(xiàn)嚷量,在前面的文章中我們配置內(nèi)存中的用戶時(shí),似乎不是這么配置的逆趣,而是:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin").password("admin").roles("USER");
}
}
如果你的應(yīng)用只有唯一一個(gè) WebSecurityConfigurerAdapter
蝶溶,那么他們之間的差距可以被忽略,從方法名可以看出兩者的區(qū)別:使用@Autowired
注入的 AuthenticationManagerBuilder
是全局的身份認(rèn)證器宣渗,作用域可以跨越多個(gè)WebSecurityConfigurerAdapter
抖所,以及影響到基于Method
的安全控制;而 protected configure()
的方式則類似于一個(gè)匿名內(nèi)部類痕囱,它的作用域局限于一個(gè)WebSecurityConfigurerAdapter
內(nèi)部田轧。關(guān)于這一點(diǎn)的區(qū)別,可以參考我曾經(jīng)提出的issuespring-security#issues4571鞍恢。官方文檔中傻粘,也給出了配置多個(gè)WebSecurityConfigurerAdapter
的場景以及demo
,將在該系列的后續(xù)文章中解讀