天天看點

【Spring Security詳解】第一章 | 概述

從本系列開始,部落客将帶來大家深入學習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中起到核心作用的幾個類,這是通過這幾個類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的過濾器。

【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。

【Spring Security詳解】第一章 | 概述

這幅圖展示了用戶端client請求到系統中時,經過Tomcat的某些原生過濾器後,到達

DelegatingFilterProxy

。并委托給

FilterChainProxy

,而

FilterChainProxy

通過

SecurityFilterChain

來代理各種Filter執行個體。之後再到Tomcat的原生過濾器,最終到達Servet。

簡而言之,

FilterChainProxy

使用

SecurityFilterChain

确定應對此請求調用哪些Spring Security過濾器。

【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的其他過濾器中…

【Spring Security詳解】第一章 | 概述

3.3 多個SecurityFilterChain

在Spring Security中,可以配置多個

SecurityFilterChain

,由

FilterChainProxy

決定應使用哪個

SecurityFilterChain

FilterChainProxy

會根據請求的路由比對第一個符合條件的

SecurityFilterChain

,并執行其過濾器。

【Spring Security詳解】第一章 | 概述

四、 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

并打開了一個登入頁面。

【Spring Security詳解】第一章 | 概述

F12可以看到頁面請求

http://localhost:8080/hello

之後,傳回響應302,并重定向到

http://localhost:8080/login

接口進行請求,該接口響應為一個頁面。讓我們完成登入操作。

【Spring Security詳解】第一章 | 概述

輸入賬号密碼後,點選登入(使用者:

user

,密碼:

71c36beb-7af5-4116-b807-ab84e484e6fa

),此時可以看到頁面傳回了接口

hello

,這也意味着隻有認證成功才會允許通路資源。

這就是Spring Security的魅力。部落客隻是引入了一個Spring Security依賴就做到了所有資源的保護,那他是怎麼做到的呢,且聽我慢慢道來。

4.3 為什麼預設通路資源會傳回登入頁面?

【Spring Security詳解】第一章 | 概述
該圖檔來自《深入淺出Spring Security》

當用戶端發起一個資源的請求時(

http://localhost:8080/hello

),會經過上文所述的15個Spring Security的過濾器依次執行。

直到走到

FilterSecurityInterceptor

這個過濾器的時候,抛出一個通路被拒絕的異常。

【Spring Security詳解】第一章 | 概述

此處代碼走到

AbstractSecurityInterceptor

類的原因是FilterSecurityInterceptor的

doFilter()

調用到了父類的代碼,在父類的方法中抛出了

AccessDeniedException

,該異常會繼續往上抛出。

直到ExceptionTranslationFilter的catch子產品捕獲到了這個異常。

【Spring Security詳解】第一章 | 概述

并最終調用

【Spring Security詳解】第一章 | 概述

最終将請求重定向到

http://localhost:8080/login

頁面。

【Spring Security詳解】第一章 | 概述

緊接着,用戶端再次向服務請求

http://localhost:8080/login

老規矩又開始按順序執行這15個過濾器,直到到達

DefaultLoginPageGeneratingFilter

過濾器的時候,會判斷若是通路登入請求URL或是登入失敗或是退出成功中的一個,會執行下面的邏輯。

【Spring Security詳解】第一章 | 概述

很明顯

isLoginUrlRequest(request) == true

然後代碼來到了

generateLoginPageHtml()

可以看到通過StringBuilder拼接了一個HTML的登入頁面。

【Spring Security詳解】第一章 | 概述

後續操作就是往response寫入了這個html的登入頁面,并傳回。是以就有了當初請求

http://localhost:8080/hello

時,出現了一個登入頁面。

這便是內建Spring Security後,Spring Security的預設安全政策。

部落客簡單梳理一下這塊邏輯。

  1. 用戶端請求一個資源URL。
  2. 請求會按照順序經過Spring Security預設提供的15個過濾器,在FilterSecurityInterceptor過濾器中發現使用者沒有認證會抛出AccessDeniedException異常。
  3. 異常會被ExceptionTranslationFilter過濾器被捕獲到,并調用authenticationEntryPoint#commence方法将請求重定向到/login接口。
  4. 用戶端再次請求/login接口。
  5. 請求被DefaultLoginPageGeneratingFilter過濾器攔截,并生成了一個登入頁面并傳回給用戶端。

五、 小結

本章部落客主要給大家介紹了Spring Security在市場上的使用情況,以及Spring Security的整體架構。并舉了一個簡單的例子說明為什麼僅僅引入了Spring Security的maven依賴就對資源做了保護。

接下來部落客會帶來大家進一步了解Spring Security的認證細節,盡情期待!