從本系列開始,部落客将帶來大家深入學習Spring Security。部落客對該架構的看法是不但要會使用,還有能夠了解其源碼,要知其然,還要知其是以然。
相信朋友們閱讀完部落客本系列全部文章之後,定會了解Spring Security,讓我們從入門、到了解、最終吊打面試官!
PS:部落客早在8月中旬開始寫本系列部落格,本來想一文搞定Spring Security,但由于Spring Security的細節特别多,已經寫了2w字卻感覺才将心中所想寫了近半不到,是以萌生了想寫Spring Security體系一系列文章的想法。還請多多關注部落客,不勝感激!
-
- 一、 Spring Security簡介
- 二、 Spring Security提供了哪些功能
- 三、 Spring Security是如何完成認證的
-
- 3.1 DelegatingFilterProxy
- 3.2 FilterChainProxy & SecurityFilterChain(二者關系密切,放一起講述)
- 3.3 多個SecurityFilterChain
- 四、 Spring Security的初步使用
-
- 4.1 內建Spring Security
- 4.2 通路測試
- 4.3 為什麼預設通路資源會傳回登入頁面?
- 五、 小結
在本篇内容,部落客給大家介紹一下Spring Security在市場上的使用情況,以及Spring Security是通過什麼原來完成認證操作的(梗概)。同時也涉及Spring Security的源碼結構,可能不太易懂,建議配合本系列文章食用。
一、 Spring Security簡介
在Java企業開發中,市面上常見的開源安全架構非常少,主要有以下幾種方案:
- Shiro
- Spring Security
- 企業自行開發的方案
幾年前,微服務還沒有大火的時候,Shiro以其輕量、簡單、易于內建的優點獨當一面。
而最近今年,随着微服務的大火,Spring Security作為Spring家族的首推的安全架構,在與Spring等其他元件的無縫整合的特點,導緻其市面占有率也是逐年提高。
二、 Spring Security提供了哪些功能
Spring Security是Spring全家桶裡面的一個項目,提供認證、授權以及應對漏洞攻擊的保護。
- 認證
: 可以簡單了解成”你是誰“,最簡單的例子就是使用者登入,這就是認證,下文中登入操作代表認證。
authentication
- 授權
:可以簡單了解成“你有哪些權限,你能做什麼”,比如登入進來的使用者是具有管理者或是普通使用者的權限。
authorization
- 保護
:應對遭受漏洞利用的保護。
protection
三、 Spring Security是如何完成認證的
Spring Security通過一系列過濾器完成認證與授權的工作。
對于SpringBoot工程,并沒有引入其他依賴。
用戶端發起請求時,tomcat容器會建立一個包括Filter和Servlet的FilterChain(過濾器鍊)。通過Filter可以控制請求與響應,以及是否調用下遊的過濾器或Servlet。
接來下部落客簡要說明下Spring Security中起到核心作用的幾個類,這是通過這幾個類Spring Security才能內建到SpringBoot當中,并發揮作用。
此處參考了Spring Security的官網文檔:連結: Spring Security官方文檔
3.1 DelegatingFilterProxy
Spring Seucrity實作認證與授權的功能提供了很多過濾器,通過這些過濾器來攔截請求,并做相應處理。那麼如何将這些過濾器嵌入到Spring的IOC容器呢,最好的做法就是将Spring Security這些過濾器注冊成Bean,這樣就可以統一的進行管理了,
DelegatingFilterProxy
就是為了實作這個目的。
Delegating
這個名字很繞口
ˈdelɪɡeɪtɪŋ'
,是委托的意思。
DelegatingFilterProxy
合到一起就是
委托過濾器代理
。
整體意思就是
DelegatingFilterProxy
是一個代理,他委托了某個類(
FilterChainProxy
下文會提到),并讓那個類完成後續攔截操作。
可以把他了解成一個膠水,由他連接配接了web應用的原生過濾器和Spring Security的過濾器。
DelegatingFilterProxy
是一個過濾器,裡面有個成員變量
他就是委托對象。
在用戶端請求來臨的時候會執行
doFilter()
方法。
首先會判斷delegate是否為空,若為空的話從IOC容器中通過
getBean()
的方法拿到這個代理對象
FilterChainProxy
。
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
String targetBeanName = getTargetBeanName();
Assert.state(targetBeanName != null, "No target bean name set");
Filter delegate = wac.getBean(targetBeanName, Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
并執行代理對象的
doFilter()
方法。
protected void invokeDelegate(
Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
這樣後續的操作就交由這個代理對象去做了。
3.2 FilterChainProxy & SecurityFilterChain(二者關系密切,放一起講述)
FilterChainProxy
這個類可以了解成過濾器鍊代理。
DelegatingFilterProxy
正是委托給
FilterChainProxy
,就是上文提到的
delegate
來完成攔截等操作。
FilterChainProxy
是Spring Security發揮作用的入口,一切Spring Security的過濾器都是從這之後開始調用的。
另外值得注意的是,
DelegatingFilterProxy
是注冊到Tomcat容器的一個過濾器,他的生命周期由Tomcat來控制。而
FilterChainProxy
則是Spring的IOC容器中的一個Bean。
這幅圖展示了用戶端client請求到系統中時,經過Tomcat的某些原生過濾器後,到達
DelegatingFilterProxy
。并委托給
FilterChainProxy
,而
FilterChainProxy
通過
SecurityFilterChain
來代理各種Filter執行個體。之後再到Tomcat的原生過濾器,最終到達Servet。
簡而言之,
FilterChainProxy
使用
SecurityFilterChain
确定應對此請求調用哪些Spring Security過濾器。
可以看到delegate對象中包括一個過濾器鍊的清單(
SecurityFilterChain
)。其中
DefaultSecurityFilterChain
對象就是Spring Security的一個過濾器鍊,如前一個圖檔所示的
SecurityFilterChain
。
FilterChainProxy
作為一個代理類,他的
doFilter()
方法最終會調到下面的
doFilterInternal()
。
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(firewallRequest);
if (filters == null || filters.size() == 0) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
}
firewallRequest.reset();
chain.doFilter(firewallRequest, firewallResponse);
return;
}
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
}
// 看這裡
VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
virtualFilterChain.doFilter(firewallRequest, firewallResponse);
}
将後續的執行操作交由他的一個内部靜态類去實作。執行
VirtualFilterChain#doFilter()
方法。
@Override
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.currentPosition == this.size) {
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Secured " + requestLine(this.firewalledRequest)));
}
// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
this.originalChain.doFilter(request, response);
//退出循環
return;
}
this.currentPosition++;
// 執行Spring Security的過濾器
Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1);
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Invoking %s (%d/%d)", nextFilter.getClass().getSimpleName(),
this.currentPosition, this.size));
}
nextFilter.doFilter(request, response, this);
}
在這裡面會循環的調用每個Spring Security提供的過濾器進行各種攔截處理操作,并在最後退出循環,進入Tomcat的其他過濾器中…
3.3 多個SecurityFilterChain
在Spring Security中,可以配置多個
SecurityFilterChain
,由
FilterChainProxy
決定應使用哪個
SecurityFilterChain
。
FilterChainProxy
會根據請求的路由比對第一個符合條件的
SecurityFilterChain
,并執行其過濾器。
四、 Spring Security的初步使用
很多人對Spring Security的感覺都是太繁瑣,其實到了微服務的天下,Spring Security的使用非常簡單。
接下來部落客以一個簡單的例子給大家示範一下。
4.1 內建Spring Security
引入pom依賴。
<!--辨別一個springboot的web工程-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入Spring Security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Spring Security是通過一系列過濾器來完成認證與授權的功能的。用戶端請求之後逐個通過Spring Security的各種過濾器。當引入Spring Security依賴時,其實已經加載了Spring Security提供的許多個預設過濾器。
添加請求URL,當做用來測試的資源URL。
@RequestMapping("hello")
public class HelloController {
@GetMapping()
public String hello() {
return "hello";
}
}
啟動項目,可以看到控制台輸出的日志中,包括了如下的内容。
按照Spring Security官網的描述,其實生成了名為
user
的使用者,密碼為如下
71c36beb-7af5-4116-b807-ab84e484e6fa
。
并且可以看到控制台列印了Spring Security預設加載的15個過濾器,正是他們支撐着Spring Security做到了認證相關的操作。
稍後部落客會挑常見的過濾器給大家說明一下,值得注意的是,這15個過濾器的先後執行順序就是控制台列印的順序。
Using generated security password: 71c36beb-7af5-4116-b807-ab84e484e6fa
2022-08-22 20:22:07.179 INFO 10672 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: any request, [
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@4784013e,
org.springframework.security.web.context.SecurityContextPersistenceFilter@2ca6546f,
org.springframework.security.web.header.HeaderWriterFilter@aa10649,
[email protected],
org.springframework.security.web.authentication.logout.LogoutFilter@3af356f,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@267517e4,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@231baf51,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@6f952d6c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@56ba8773,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7923f5b3,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@6050462a,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@5965844d,
org.springframework.security.web.session.SessionManagementFilter@37095ded,
org.springframework.security.web.access.ExceptionTranslationFilter@368d5c00,
org.springframework.se[email protected]
]
4.2 通路測試
接下來使用浏覽器通路該資源。請求位址
http://localhost:8080/hello
(SpringBoot預設啟動端口為8080)
可以觀察到頁面直接跳轉到了
http://localhost:8080/login
并打開了一個登入頁面。
F12可以看到頁面請求
http://localhost:8080/hello
之後,傳回響應302,并重定向到
http://localhost:8080/login
接口進行請求,該接口響應為一個頁面。讓我們完成登入操作。
輸入賬号密碼後,點選登入(使用者:
user
,密碼:
71c36beb-7af5-4116-b807-ab84e484e6fa
),此時可以看到頁面傳回了接口
hello
,這也意味着隻有認證成功才會允許通路資源。
這就是Spring Security的魅力。部落客隻是引入了一個Spring Security依賴就做到了所有資源的保護,那他是怎麼做到的呢,且聽我慢慢道來。
4.3 為什麼預設通路資源會傳回登入頁面?
該圖檔來自《深入淺出Spring Security》
當用戶端發起一個資源的請求時(
http://localhost:8080/hello
),會經過上文所述的15個Spring Security的過濾器依次執行。
直到走到
FilterSecurityInterceptor
這個過濾器的時候,抛出一個通路被拒絕的異常。
此處代碼走到
AbstractSecurityInterceptor
類的原因是FilterSecurityInterceptor的
doFilter()
調用到了父類的代碼,在父類的方法中抛出了
AccessDeniedException
,該異常會繼續往上抛出。
直到ExceptionTranslationFilter的catch子產品捕獲到了這個異常。
并最終調用
最終将請求重定向到
http://localhost:8080/login
頁面。
緊接着,用戶端再次向服務請求
http://localhost:8080/login
。
老規矩又開始按順序執行這15個過濾器,直到到達
DefaultLoginPageGeneratingFilter
過濾器的時候,會判斷若是通路登入請求URL或是登入失敗或是退出成功中的一個,會執行下面的邏輯。
很明顯
isLoginUrlRequest(request) == true
然後代碼來到了
generateLoginPageHtml()
可以看到通過StringBuilder拼接了一個HTML的登入頁面。
後續操作就是往response寫入了這個html的登入頁面,并傳回。是以就有了當初請求
http://localhost:8080/hello
時,出現了一個登入頁面。
這便是內建Spring Security後,Spring Security的預設安全政策。
部落客簡單梳理一下這塊邏輯。
- 用戶端請求一個資源URL。
- 請求會按照順序經過Spring Security預設提供的15個過濾器,在FilterSecurityInterceptor過濾器中發現使用者沒有認證會抛出AccessDeniedException異常。
- 異常會被ExceptionTranslationFilter過濾器被捕獲到,并調用authenticationEntryPoint#commence方法将請求重定向到/login接口。
- 用戶端再次請求/login接口。
- 請求被DefaultLoginPageGeneratingFilter過濾器攔截,并生成了一個登入頁面并傳回給用戶端。
五、 小結
本章部落客主要給大家介紹了Spring Security在市場上的使用情況,以及Spring Security的整體架構。并舉了一個簡單的例子說明為什麼僅僅引入了Spring Security的maven依賴就對資源做了保護。
接下來部落客會帶來大家進一步了解Spring Security的認證細節,盡情期待!