Spring Security
學(xué)習(xí) Spring Security 用法,架構(gòu)、設(shè)計(jì)模式等。
核心組件
主要介紹 Spring Security 中常見(jiàn)核心 Java 類(lèi)以及他們之間的依賴(lài)關(guān)系,以及整個(gè)架構(gòu)的設(shè)計(jì)原理。
SecurityContextHolder
SecurityContextHolder 用于存儲(chǔ)安全上下文(security context)的信息。當(dāng)前操作的用戶(hù)是誰(shuí),該用戶(hù)是否已經(jīng)被認(rèn)證,他擁有哪些角色權(quán)限…這些都被保存在 SecurityContextHolder中。SecurityContextHolder 默認(rèn)使用 ThreadLocal 策略來(lái)存儲(chǔ)認(rèn)證信息??吹?ThreadLocal 也就意味著,這是一種與線(xiàn)程綁定的策略。Spring Security 在用戶(hù)登錄時(shí)自動(dòng)綁定認(rèn)證信息到當(dāng)前線(xiàn)程,在用戶(hù)退出時(shí),自動(dòng)清除當(dāng)前線(xiàn)程的認(rèn)證信息。但這一切的前提,是你在 web 場(chǎng)景下使用 Spring Security。
獲取當(dāng)前用戶(hù)的信息
因?yàn)樯矸菪畔⑹桥c線(xiàn)程綁定的,所以可以在程序的任何地方使用靜態(tài)方法獲取用戶(hù)信息。一個(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 類(lèi),而 Principal 是位于 java.security 包中的??梢砸?jiàn)得,Authentication 在 spring security 中是最高級(jí)別的身份/認(rèn)證的抽象。
<2> 由這個(gè)頂級(jí)接口,我們可以得到用戶(hù)擁有的權(quán)限信息列表,密碼,用戶(hù)細(xì)節(jié)信息,用戶(hù)身份信息,認(rèn)證信息。
還記得1.1節(jié)中,authentication.getPrincipal()返回了一個(gè) Object,我們將 Principal 強(qiáng)轉(zhuǎn)成了 Spring Security 中最常用的UserDetails,這在 Spring Security 中非常常見(jiàn),接口返回 Object,使用 instanceof 判斷類(lèi)型,強(qiáng)轉(zhuǎn)成對(duì)應(yīng)的具體實(shí)現(xiàn)類(lèi)。接口詳細(xì)解讀如下:
- getAuthorities(),權(quán)限信息列表,默認(rèn)是 GrantedAuthority 接口的一些實(shí)現(xiàn)類(lèi),通常是代表權(quán)限信息的一系列字符串。
- getCredentials(),密碼信息,用戶(hù)輸入的密碼字符串,在認(rèn)證過(guò)后通常會(huì)被移除,用于保障安全。
- getDetails(),細(xì)節(jié)信息,web 應(yīng)用中的實(shí)現(xiàn)接口通常為 WebAuthenticationDetails,它記錄了訪問(wèn)者的 ip 地址和 sessionId 的值。
- getPrincipal(),敲黑板!??!最重要的身份信息,大部分情況下返回的是 UserDetails 接口的實(shí)現(xiàn)類(lèi),也是框架中的常用接口之一。UserDetails 接口將會(huì)在下面的小節(jié)重點(diǎn)介紹。
Spring Security是如何完成身份認(rèn)證的?
- 用戶(hù)名和密碼被過(guò)濾器獲取到,封裝成
Authentication,通常情況下是UsernamePasswordAuthenticationToken這個(gè)實(shí)現(xiàn)類(lèi)。 -
AuthenticationManager身份管理器負(fù)責(zé)驗(yàn)證這個(gè)Authentication - 認(rèn)證成功后,
AuthenticationManager身份管理器返回一個(gè)被填充滿(mǎn)了信息的(包括上面提到的權(quán)限信息,身份信息,細(xì)節(jié)信息,但密碼通常會(huì)被移除)Authentication實(shí)例。 -
SecurityContextHolder安全上下文容器將第3步填充了信息的Authentication,通過(guò)SecurityContextHolder.getContext().setAuthentication(…)方法,設(shè)置到其中。
這是一個(gè)抽象的認(rèn)證流程,而整個(gè)過(guò)程中,如果不糾結(jié)于細(xì)節(jié),其實(shí)只剩下一個(gè) AuthenticationManager 是我們沒(méi)有接觸過(guò)的了,這個(gè)身份管理器我們?cè)诤竺娴男」?jié)介紹。
AuthenticationManager
初次接觸 Spring Security 的朋友相信會(huì)被 AuthenticationManager,ProviderManager ,AuthenticationProvider …這么多相似的 Spring 認(rèn)證類(lèi)搞得暈頭轉(zhuǎn)向,但只要稍微梳理一下就可以理解清楚它們的聯(lián)系和設(shè)計(jì)者的用意。AuthenticationManager(接口)是認(rèn)證相關(guān)的核心接口,也是發(fā)起認(rèn)證的出發(fā)點(diǎn),因?yàn)樵趯?shí)際需求中,我們可能會(huì)允許用戶(hù)使用用戶(hù)名+密碼登錄,同時(shí)允許用戶(hù)使用郵箱+密碼,手機(jī)號(hào)碼+密碼登錄,甚至,可能允許用戶(hù)使用指紋登錄(還有這樣的操作?沒(méi)想到吧),所以說(shuō) AuthenticationManager 一般不直接認(rèn)證,AuthenticationManager 接口的常用實(shí)現(xiàn)類(lèi) ProviderManager 內(nèi)部會(huì)維護(hù)一個(gè) List<AuthenticationProvider> 列表,存放多種認(rèn)證方式,實(shí)際上這是委托者模式的應(yīng)用(Delegate)。也就是說(shuō),核心的認(rèn)證入口始終只有一個(gè):AuthenticationManager,不同的認(rèn)證方式:用戶(hù)名+密碼(UsernamePasswordAuthenticationToken),郵箱+密碼,手機(jī)號(hào)碼+密碼登錄則對(duì)應(yīng)了三個(gè) AuthenticationProvider。這樣一來(lái)四不四就好理解多了?熟悉 shiro 的朋友可以把AuthenticationProvider 理解成 Realm。在默認(rèn)策略下,只需要通過(guò)一個(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í)行到此,說(shuō)明沒(méi)有認(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)證器都無(wú)法認(rèn)證成功,則 ProviderManager 會(huì)拋出一個(gè)ProviderNotFoundException 異常。
以上已經(jīng)把 Spring Security 的整個(gè)認(rèn)證流程都講述了一遍,簡(jiǎn)單小結(jié)如下:身份信息的存放容器 SecurityContextHolder ,身份信息的抽象 Authentication ,身份認(rèn)證器 AuthenticationManager 及其認(rèn)證流程。姑且在這里做一個(gè)分隔線(xiàn)。下面來(lái)介紹下 AuthenticationProvider 接口的具體實(shí)現(xiàn)。
DaoAuthenticationProvider
AuthenticationProvider 最最最常用的一個(gè)實(shí)現(xiàn)便是 DaoAuthenticationProvider 。顧名思義,Dao 正是數(shù)據(jù)訪問(wèn)層的縮寫(xiě),也暗示了這個(gè)身份認(rèn)證器的實(shí)現(xiàn)思路。由于本文是一個(gè) Overview ,姑且只給出其 UML 類(lèi)圖:

按照我們最直觀的思路,怎么去認(rèn)證一個(gè)用戶(hù)呢?用戶(hù)前臺(tái)提交了用戶(hù)名和密碼,而數(shù)據(jù)庫(kù)中保存了用戶(hù)名和密碼,認(rèn)證便是負(fù)責(zé)比對(duì)同一個(gè)用戶(hù)名,提交的密碼和保存的密碼是否相同便是了。在Spring Security 中。提交的用戶(hù)名和密碼,被封裝成了UsernamePasswordAuthenticationToken ,而根據(jù)用戶(hù)名加載用戶(hù)的任務(wù)則是交給了 UserDetailsService ,在DaoAuthenticationProvider 中,對(duì)應(yīng)的方法便是 retrieveUser ,雖然有兩個(gè)參數(shù),但是 retrieveUser 只有第一個(gè)參數(shù)起主要作,返回一個(gè) UserDetails。還需要完成 UsernamePasswordAuthenticationToken 和 UserDetails 密碼的比對(duì),這便是交給 additionalAuthenticationChecks 方法完成的,如果這個(gè) void 方法沒(méi)有拋異常,則認(rèn)為比對(duì)成功。比對(duì)密碼的過(guò)程,用到了PasswordEncoder 和 SaltSource ,密碼加密和鹽的概念相信不用我贅述了,它們?yōu)楸U习踩O(shè)計(jì),都是比較基礎(chǔ)的概念。
如果你已經(jīng)被這些概念搞得暈頭轉(zhuǎn)向了,不妨這么理解 DaoAuthenticationProvider :它獲取用戶(hù)提交的用戶(hù)名和密碼,比對(duì)其正確性,如果正確,返回一個(gè)數(shù)據(jù)庫(kù)中的用戶(hù)信息(假設(shè)用戶(hù)信息被保存在數(shù)據(jù)庫(kù)中)。
UserDetails 與 UserDetailsService
上面不斷提到了 UserDetails 這個(gè)接口,它代表了最詳細(xì)的用戶(hù)信息,這個(gè)接口涵蓋了一些必要的用戶(hù)信息字段,具體的實(shí)現(xiàn)類(lèi)對(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 接口很類(lèi)似,比如它們都擁有 username ,authorities ,區(qū)分他們也是本文的重點(diǎn)內(nèi)容之一。Authentication 的 getCredentials() 與 UserDetails 中的 getPassword() 需要被區(qū)分對(duì)待,前者是用戶(hù)提交的密碼憑證,后者是用戶(hù)正確的密碼,認(rèn)證器其實(shí)就是對(duì)這兩者的比對(duì)。Authentication 中的 getAuthorities() 實(shí)際是由 UserDetails 的 getAuthorities() 傳遞而形成的。還記得 Authentication 接口中的 getUserDetails() 方法嗎?其中的 UserDetails 用戶(hù)詳細(xì)信息便是經(jīng)過(guò)了 AuthenticationProvider 之后被填充的。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService 和 AuthenticationProvider 兩者的職責(zé)常常被人們搞混,關(guān)于他們的問(wèn)題在文檔的 FAQ 和 issues 中屢見(jiàn)不鮮。記住一點(diǎn)即可,敲黑板?。。?code>UserDetailsService 只負(fù)責(zé)從特定的地方(通常是數(shù)據(jù)庫(kù))加載用戶(hù)信息,僅此而已,記住這一點(diǎn),可以避免走很多彎路。UserDetailsService 常見(jiàn)的實(shí)現(xiàn)類(lèi)有 JdbcDaoImpl,InMemoryUserDetailsManager,前者從數(shù)據(jù)庫(kù)加載用戶(hù),后者從內(nèi)存中加載用戶(hù),也可以自己實(shí)現(xiàn) UserDetailsService,通常這更加靈活。
架構(gòu)概覽圖
為了更加形象的理解上述我介紹的這些核心類(lèi),附上一張按照我的理解,所畫(huà)出 Spring Security 的一張非典型的 UML 圖

整個(gè)Spring Security 都是圍繞以上這張架構(gòu)圖展開(kāi)的,最頂層為最核心最抽象的 AuthenticationManager 接口, ProviderManager 為 AuthenticationManager 的一個(gè)具體實(shí)現(xiàn),功能如其名字他的作用是管理 Provider 的, AuthenticationProvider 才是真正認(rèn)證的接口,因此我們?cè)趯?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 來(lái)做驗(yàn)證,可以使用 Filter 返回的 Authentication 實(shí)現(xiàn)類(lèi)來(lái)限定,部分代碼如下:
//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é)果,先過(guò)個(gè)眼熟。后續(xù)的文章會(huì)秉持 Code First 的理念,陸續(xù)詳細(xì)地講解這些實(shí)現(xiàn)類(lèi)的使用場(chǎng)景,源碼分析,以及最基本的:如何配置 Spring Security ,在后面可以時(shí)不時(shí)往回看一看,找到具體的類(lèi)在整個(gè)架構(gòu)中所處的位置。另外,一些 Spring Security的過(guò)濾器還未囊括在架構(gòu)概覽中,如將表單信息包裝成 UsernamePasswordAuthenticationToken 的過(guò)濾器,考慮到這些雖然也是架構(gòu)的一部分,但是真正重寫(xiě)他們的可能性較小,所以打算放到后面講解。
配置介紹
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”(首頁(yè)),”/login”(登錄),”/logout”(注銷(xiāo)),之外,其他路徑都需要認(rèn)證。
- 指定“/login”該路徑為登錄頁(yè)面,當(dāng)未認(rèn)證的用戶(hù)嘗試訪問(wèn)任何受保護(hù)的資源時(shí),都會(huì)跳轉(zhuǎn)到“/login”。
- 默認(rèn)指定“/logout”為注銷(xiāo)頁(yè)面
- 配置一個(gè)內(nèi)存中的用戶(hù)認(rèn)證器,使用admin/admin作為用戶(hù)名和密碼,具有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ù)庫(kù)加載權(quán)限,這邊因?yàn)?Spring Security 的機(jī)制是在模塊啟動(dòng)的時(shí)候進(jìn)行加載的,如果想要?jiǎng)討B(tài) reload 權(quán)限,調(diào)查來(lái)看Spring 并沒(méi)有提供相關(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和方法名組合來(lái)區(qū)分,注意一種特殊情況,
//因?yàn)槭鞘褂肁nt語(yǔ)法來(lái)匹配URL,如果出現(xiàn)請(qǐng)求方法相同且URL是匹配子集關(guān)系時(shí),要把最具體的URL放在
//前面,比如同是 GET 方法,Api1:/api/user/paged 和 Api2:/api/user/{id},在Ant語(yǔ)法里面
//Api2是能匹配Api1的,如果用戶(hù)有Api2的權(quán)限而沒(méi)有Api1的權(quán)限,在如下初始化權(quán)限時(shí)Api2初始化的
//順序在Api1的前面,就會(huì)導(dǎo)致用戶(hù)即使沒(méi)有Api1的權(quán)限也能訪問(wèn),因此要確保數(shù)據(jù)庫(kù)加載的時(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)
//自定義沒(méi)有權(quán)限時(shí)的處理,一般根據(jù)業(yè)務(wù)封裝自定義的返回結(jié)果
.accessDeniedHandler(ajaxAccessDeniedHandler)
.and()
//Restful API 完全無(wú)狀態(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()
//處理跨域過(guò)濾器
.addFilterBefore(new CustomCorsFilter(), UsernamePasswordAuthenticationFilter.class)
//處理登錄獲取token的過(guò)濾器
.addFilterBefore(buildAjaxLoginProcessingFilter(AUTHENTICATION_URL), UsernamePasswordAuthenticationFilter.class)
//驗(yàn)證用戶(hù)token,以及用戶(hù)是否有接口訪問(wèn)權(quán)限的過(guò)濾器
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(permitAllEndpointList, Arrays.asList(API_ROOT_URL,API_ATTACHMENT_URL)), UsernamePasswordAuthenticationFilter.class);
}
具體解釋見(jiàn)代碼上的注釋
@EnableWebSecurity
我們自己定義的配置類(lèi) WebSecurityConfig 加上了 @EnableWebSecurity 注解,同時(shí)繼承了 WebSecurityConfigurerAdapter。你可能會(huì)在想誰(shuí)的作用大一點(diǎn),毫無(wú)疑問(wè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注解中包含的配置類(lèi)。 - <2>
WebSecurityConfiguration顧名思義,是用來(lái)配置web安全的,下面的小節(jié)會(huì)詳細(xì)介紹。 - <3>
@EnableGlobalAuthentication注解的源碼如下:
@Import(AuthenticationConfiguration.class)
@Configuration
public @interface EnableGlobalAuthentication {
}
注意點(diǎn)同樣在 @Import 之中,它實(shí)際上激活了 AuthenticationConfiguration 這樣的一個(gè)配置類(lèi),用來(lái)配置認(rèn)證相關(guān)的核心類(lèi)。
也就是說(shuō):@EnableWebSecurity完成的工作便是加載了WebSecurityConfiguration,AuthenticationConfiguration 這兩個(gè)核心配置類(lèi),也就此將 spring security 的職責(zé)劃分為了配置安全信息,配置認(rèn)證信息兩部分。
WebSecurityConfiguration
在這個(gè)配置類(lèi)中,有一個(gè)非常重要的Bean被注冊(cè)了。
@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的核心過(guò)濾器,是整個(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è)代理類(lèi),負(fù)責(zé)攔截請(qǐng)求(注意DelegatingFilterProxy這個(gè)類(lèi)不是spring security包中的,而是存在于web包中,spring使用了代理模式來(lái)實(shí)現(xiàn)安全過(guò)濾的解耦)。
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)容,來(lái)進(jìn)行重寫(xiě),而其提供了三個(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 作為根開(kāi)始配置,每一個(gè) and() 對(duì)應(yīng)了一個(gè)模塊的配置(等同于xml配置中的結(jié)束標(biāo)簽),并且 and() 返回了 HttpSecurity 本身,于是可以連續(xù)進(jìn)行配置。他們配置的含義也非常容易通過(guò)變量本身來(lái)推測(cè)
- authorizeRequests()配置路徑攔截,表明路徑訪問(wèn)所對(duì)應(yīng)的權(quán)限,角色,認(rèn)證信息。
- formLogin()對(duì)應(yīng)表單認(rèn)證相關(guān)的配置
- logout()對(duì)應(yīng)了注銷(xiāo)相關(guān)的配置
- httpBasic()可以配置basic登錄
他們分別代表了 http 請(qǐng)求相關(guān)的安全配置,這些配置項(xiàng)無(wú)一例外的返回了 Configurer 類(lèi),而所有的 http 相關(guān)配置可以通過(guò)查看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é)議有一定的了解才能完全掌握所有的配置,不過(guò),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)存中用戶(hù)的配置。
細(xì)心的朋友會(huì)發(fā)現(xiàn),在前面的文章中我們配置內(nèi)存中的用戶(hù)時(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()的方式則類(lèi)似于一個(gè)匿名內(nèi)部類(lèi),它的作用域局限于一個(gè)WebSecurityConfigurerAdapter內(nèi)部。關(guān)于這一點(diǎn)的區(qū)別,可以參考我曾經(jīng)提出的issuespring-security#issues4571。官方文檔中,也給出了配置多個(gè)WebSecurityConfigurerAdapter的場(chǎng)景以及demo,將在該系列的后續(xù)文章中解讀