日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Spring Security之安全異常處理

作者:Evan_L 更新時間: 2024-07-18 編程語言

前言

在我們的安全框架中,不管是什么框架(包括通過過濾器自定義)都需要處理涉及安全相關的異常,例如:登錄失敗要跳轉到登錄頁,訪問權限不足要返回頁面亦或是json。接下來,我們就看看Spring Security是怎么處理異常的!

什么是異常處理

在Spring Security中,特指對于安全異常的處理。

我們知道Spring Security主要是基于過濾器來實現的,因此每個安全過濾器都可能發生安全異常,所以處理邏輯會被散落在各個過濾器中。

Spring自然是不能忍受這種設計,于是就有了專門的安全異常處理。

注:下文我們都用異常處理來代指安全異常處理。

異常處理設計

Spring Security將安全異常分為兩類。

  • AuthenticationException —— 認證異常
    認證異常

    認證異常 觸發原因 描述
    BadCredentialsException 無法識別憑證 可能是沒有憑證/無法解密/格式不對等
    UsernameNotFoundException 沒有找到用戶 用戶名沒有對應的賬號
    SessionAuthenticationException 認證過程中與session相關的校驗。例如,控制多點登錄時,當某用戶多點登錄超過規定數量就會發生 session認證異常
    AuthenticationServiceException 認證服務遇到無法處理的情況是觸發 認證服務異常
    ProviderNotFoundException ProviderManager沒有配置任何的Provider 沒有Provider
    PreAuthenticatedCredentialsNotFoundException 與第三方認證系統集成時,發現客戶端沒有傳憑證 前認證憑證沒有找到
    AuthenticationCredentialsNotFoundException 這個主要是鑒權的時候發現沒有認證,就會拋出 沒有找到認證憑證。
    RememberMeAuthenticationException - 主要與記住我功能,恢復登錄態有關
    NonceExpiredException - 這個主要與Digest認證方式有關
    AccountStatusException 校驗賬號狀態時觸發 賬號狀態異常
  • AccessDeniedException —— 訪問拒絕異常
    訪問拒絕異常

    訪問拒絕異常 觸發原因 描述
    AuthorizationServiceException 遇到無法處理的鑒權時觸發 例如配置錯誤,數據類型錯誤
    CrsfException 防御Crsf時觸發 這里有兩個,分別對應WebFlux和WebMvc

    其實對于鑒權來說,只要發現權限不滿足,都是直接拋出AccessDeniedException的。

認證異常和訪問拒絕異常的區別

與訪問拒絕異常相比,認證異常要復雜不少。這是由認證過程和認證方式的多樣性導致的。

  • 認證過程:
    一個完整的用戶密碼認證過程各組件的調用關系和簡化
    一個完整的用戶密碼認證過程各組件的調用關系和簡化組件都有不少,更何況要捋清楚調用關系。上面也只能是給大家看看認證過程中需要干啥,有哪些組件負責。
  • 認證方式:
    這個就不多啰嗦,前面說認證過濾器的時候有說過。

異常處理器

異常 類定義 異常處理器
認證異常 AuthenticationException AuthenticationFailureHandler
訪問異常 AccessDeniedException AccessDeniedHandler

為什么要搞兩個異常,還要搞兩個組件來處理呢?

  1. 從安全業務上說,本來就是兩種業務,訪問跟認證是兩個事情。
  2. 從單一職責原則來說,肯定要進行拆分,因為這兩個組件處理的是不同的異常。
  3. 一般而言,登錄異常我們是需要重定向到登錄頁面的,而接口訪問異常則不然,一般通過返回錯誤拒絕請求。

實際上,這兩個組件定義的可以說一模一樣:

public interface AuthenticationFailureHandler {
	void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
			throws IOException, ServletException;
}

public interface AccessDeniedHandler {
	void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
			throws IOException, ServletException;
}

除了方法名,和入參的異常不同,其他的都是一樣的。甚至,如果我們進一步看看異常的定義的話,連異常定義也是類似的,都是繼承于RuntimeException,沒有任何其他多余的字段和邏輯。

AuthenticationFailureHandler的實現

AuthenticationFailureHandler 描述
AuthenticationEntryPointFailureHandler 通過AuthenticationEntryPoint組件處理
SimpleUrlAuthenticationFailureHandler 重定向到指定URL,如果沒有指定,則退化返回401
ForwardAuthenticationFailureHandler 重定向到指定的URL,必須指定URL
ExceptionMappingAuthenticationFailureHandler 通過匹配異常尋找對應的處理器,一般由用戶自行配置。
DelegatingAuthenticationFailureHandler 委托其他的處理器處理

這里有一個特殊的,他使用另一個組件AuthenticationEntryPoint進行處理。

AuthenticationEntryPoint

  • Http403ForbiddenEntryPoint
    他是處理登錄異常的通用的可選方案,通常是AbstractPreAuthenticatedProcessingFilter(基于外部認證服務器進行認證)。核心邏輯:總是返回403。
    這個實現是用來兜底的,如果找不到其他的,那就會用他。

  • HttpStatusEntryPoint
    他是一種可選方案,直接返回一個用戶指定的http狀態,response.setStatus(this.httpStatus.value())。

  • LoginUrlAuthenticationEntryPoint
    如果我們使用的是UsernamePasswordAuthenticationFilter,那么默認使用的就是這個。其核心邏輯也比較簡單明了,就是重定向到登錄頁面。如果我們往上一層對比到SimpleUrlAuthenticationFailureHandler 、ForwardAuthenticationFailureHandler ,他的區別在于如果我們指定了loginPage,那么就會使用他。他會自動識別是絕對地址還是相對地址進行拼接。

  • DigestAuthenticationEntryPoint
    顯然,他是為DigestAuthenticationFilter服務的。他會設置一些與Digest相關的請求頭,然后調用response.sendError方法處理。

  • BasicAuthenticationEntryPoint
    為BasicAuthenticationFilter服務。核心邏輯與Digest類似,也是設置相關請求頭,通過response.sendError方法處理。

  • DelegatingAuthenticationEntryPoint
    委托。沒有自己的邏輯,而是交給別的AuthenticationEntryPoint。

AccessDeniedHandler的實現

  • AccessDeniedHandlerImpl
    基礎實現,也是默認實現。設置HTTP錯誤碼-403,并轉發到錯誤頁面。
  • InvalidSessionAccessDeniedHandler
    顯然是為了處理session失效異常的。不過有趣的是,官方在CsrfConfigurer中引入這個。并且是為了處理MissingCsrfTokenException的。并且為了單一職責,還構建了下面的委托處理器。
  • DelegatingAccessDeniedHandler
    委托處理器。他管理著哪些異常對應哪個處理器,并將當前異常的處理交付給對應的處理器處理。故而得名“委托”處理器,算是個代理人吧。當然,他要求必須有個兜底的默認處理器。
  • RequestMatcherDelegatingAccessDeniedHandler
    他也是委托處理器,不同點在于,他是RequestMatcherDelegating,也即基于RequestMatcher進行Request匹配處理器。
  • ObservationMarkingAccessDeniedHandler
    他是用來統計數據的,觀察標記。
  • CompositeAccessDeniedHandler
    組合模式的實現,用來管理多個處理器。目前看的話,主要是為了統計服務,因為他會調用每一個處理器,這可能會出現問題。只有統計這個處理器,需要其他的處理器來實現真正的處理,需要配合。

到這里問一句,這里我們看到了幾種設計模式?策略模式、委托模式、組合模式。可以看到Spring對于代碼的追求,這也是我們閱讀源碼的目的之一,學習好的設計。而這背后都是設計原則。

異常處理原理

前面我們大概了解了異常處理的來龍去脈,知道了其核心組件。現在我們來深入了解其原理。

認證異常處理原理

要理解這個,就必須回顧一下認證流程(這里以默認的用戶密碼登錄為例):

AbstractAuthenticationProcessingFilter#doFilter
> UsernamePasswordAuthenticationFilter#attemptAuthentication
|-> ProviderManager#authenticate
|-|-> AbstractUserDetailsAuthenticationProvider#authenticate
|-|-|->DaoAuthenticationProvider#retrieveUser
|-|-|-|->JdbcDaoImpl#loadUserByUsername
|-|-|->DaoAuthenticationProvider#additionalAuthenticationChecks
|-|-|->DaoAuthenticationProvider#createSuccessAuthentication
|-|-|->AbstractUserDetailsAuthenticationProvider#createSuccessAuthentication
// 同層級的表示順序調用,不同層級的:上層方法調用下層方法,是遞進關系。層級減少表示方法返回

負責處理認證請求的AbstractAuthenticationProcessingFilter#doFilter方法中會捕獲異常,并交給unsuccessfulAuthentication方法處理。

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
	
	protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException failed) throws IOException, ServletException {
		// 1. 清空安全上下文
		this.securityContextHolderStrategy.clearContext();
		// 2. 記住我功能,清空cookie,認證失敗的處理
		this.rememberMeServices.loginFail(request, response);
		// 3. 通過認證失敗處理處理
		this.failureHandler.onAuthenticationFailure(request, response, failed);
	}
}

默認情況下,會使用SimpleUrlAuthenticationFailureHandler重定向到登錄頁面。

什么?怎么知道是這個處理器?行吧,我們來看看FormLoginConfigurer的源碼吧。

public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
		AbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> {
	
	public FormLoginConfigurer() {
		// 調用父類構造器
		super(new UsernamePasswordAuthenticationFilter(), null);
		usernameParameter("username");
		passwordParameter("password");
	}
	
	/**
	 * 在配置過目標過濾器之前,會先調用這個方法進行Configurer的初始化
	 */
	@Override
	public void init(H http) throws Exception {
		// 初始化父類
		super.init(http);
		// 初始化默認的登錄頁面
		initDefaultLoginFilter(http);
	}
}

public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecurityBuilder<B>, T extends AbstractAuthenticationFilterConfigurer<B, T, F>, F extends AbstractAuthenticationProcessingFilter>
		extends AbstractHttpConfigurer<T, B> {
	
	protected AbstractAuthenticationFilterConfigurer(F authenticationFilter, String defaultLoginProcessingUrl) {
		// 調用另一個構造器
		this();
		this.authFilter = authenticationFilter;
		if (defaultLoginProcessingUrl != null) {
			// 由于FormLoginConfigurer的構造器中傳的是null,因此不會走到這
			// 當然,由于這個方法是public,因此也可以在配置時被我們調用
			// 他無非就是指定什么地址是認證請求罷了
			loginProcessingUrl(defaultLoginProcessingUrl);
		}
	}
	protected AbstractAuthenticationFilterConfigurer() {
		// 構造器中設置登錄頁面uri
		setLoginPage("/login");
	}
	private void setLoginPage(String loginPage) {
		this.loginPage = loginPage;
		// 指定AuthenticationEntryPoint,后面異常處理過濾器用的是這個來處理沒有登錄的異常。
		this.authenticationEntryPoint = new LoginUrlAuthenticationEntryPoint(loginPage);
	}
	
	@Override
	public void init(B http) throws Exception {
		// 更新/初始化認證相關的默認組件
		updateAuthenticationDefaults();
		// 更新訪問權限-認證頁面、認證請求、認證失敗
		updateAccessDefaults(http);
		// 注冊默認的AuthenticationEntryPoint
		registerDefaultAuthenticationEntryPoint(http);
	}
	protected final void updateAuthenticationDefaults() {
		if (this.loginProcessingUrl == null) {
			loginProcessingUrl(this.loginPage);
		}
		if (this.failureHandler == null) {
			// 指定默認的異常跳轉頁面
			failureUrl(this.loginPage + "?error");
		}
		LogoutConfigurer<B> logoutConfigurer = getBuilder().getConfigurer(LogoutConfigurer.class);
		if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) {
			logoutConfigurer.logoutSuccessUrl(this.loginPage + "?logout");
		}
	}
	public final T failureUrl(String authenticationFailureUrl) {
		// 就是這個啦
		T result = failureHandler(new SimpleUrlAuthenticationFailureHandler(authenticationFailureUrl));
		this.failureUrl = authenticationFailureUrl;
		return result;
	}
	protected final void registerAuthenticationEntryPoint(B http, AuthenticationEntryPoint authenticationEntryPoint) {
		// 所謂注冊就是注冊到異常處理過濾器上
		// 思考個問題:下面這種處理方式不就耦合ExceptionHandlingConfigurer了嗎?
		// 為什么不是像其他的sharedObject那樣,直接放到HttpSecurityd#sharedObjects中,在ExceptionHandlingConfigurer再自行獲取設置。
		// 答:如果這樣的話,會導致BUG。init方法是在執行了用戶配置方法之后在HttpSecurity構建過濾器鏈的時候調用的。有可能將用戶配置的覆蓋了。
		ExceptionHandlingConfigurer<B> exceptionHandling = http.getConfigurer(ExceptionHandlingConfigurer.class);
		if (exceptionHandling == null) {
			return;
		}
		exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint),
				getAuthenticationEntryPointMatcher(http));
	}
}

從FormLoginConfigurer出發,我們知道UsernamePasswordAuthenticationFilter使用的是SimpleUrlAuthenticationFailureHandler,同時ExceptionTranslationFilter使用的是LoginUrlAuthenticationEntryPoint。但這個設計我沒有很理解,個人覺得應該在頂層都使用AuthenticationFailureHandler才合理。不知道是不是為了區分場景。

  • 場景一:登錄處理時發生的異常,直接被捕獲處理了。
  • 場景二:是鑒權時發現沒有任何憑證,由異常處理過濾器處理。

但SimpleUrlAuthenticationFailureHandler、LoginUrlAuthenticationEntryPoint,內部處理沒有太大區別,都是為了跳轉到登錄頁面。

訪問拒絕異常處理原理

鑒權相關的,之前我們聊過,忘記的同學可以通過下面的鏈接再回憶復習一下。可能文章的題目可能說的權限配置,但同時也從原理上給大家分析了如何鑒權的。也正是因為有兩種方式,所以沒有單獨寫鑒權過濾器。因為基于HttpRequest的配置方式的鑒權原理是通過AuthorizationFilter,也就是過濾器實現的。而另一種權限配置方式-基于方法配置權限-則是通過AOP實現的。

  • Spring Security之基于方法配置權限
  • Spring Security之基于HttpRequest配置權限

ExceptionTranslationFilter

他主要負責處理的是鑒權過程中發生的異常。這里就包括用戶權限不足的AccessDeniedException,以及鑒權時發現用戶還沒有登錄而拋出的認證異常。

public class AuthorizationFilter extends GenericFilterBean {
	private Authentication getAuthentication() {
		Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
		if (authentication == null) {
			throw new AuthenticationCredentialsNotFoundException(
					"An Authentication object was not found in the SecurityContext");
		}
		return authentication;
	}
}
public class ExceptionTranslationFilter extends GenericFilterBean implements MessageSourceAware {

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		try {
			chain.doFilter(request, response);
		}
		catch (IOException ex) {
			throw ex;
		}
		catch (Exception ex) {
			// 嘗試從異常堆棧中找到安全異常
			Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
			RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
				.getFirstThrowableOfType(AuthenticationException.class, causeChain);
			if (securityException == null) {
				securityException = (AccessDeniedException) this.throwableAnalyzer
					.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
			}
			if (securityException == null) {
				// 不是安全異常,直接重新拋出
				rethrow(ex);
			}
			if (response.isCommitted()) {
				// 如果response已經提交,則拋出servlet異常
				throw new ServletException("Unable to handle the Spring Security Exception "
						+ "because the response is already committed.", ex);
			}
			// 處理安全異常
			handleSpringSecurityException(request, response, chain, securityException);
		}
	}
	
	private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
			FilterChain chain, RuntimeException exception) throws IOException, ServletException {
		if (exception instanceof AuthenticationException) {
			// 處理認證異常
			handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
		}
		else if (exception instanceof AccessDeniedException) {
			// 處理訪問異常
			handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
		}
	}
	private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response,
			FilterChain chain, AuthenticationException exception) throws ServletException, IOException {
		// 這里是轉換方法名,是一種代碼追求、也是一種代碼的自解釋:對于上層方法的作用是處理認證異常,而處理認證異常的手段是發送開始認證(其實就是跳轉到登錄頁面開始登錄流程),因為走到這的都是鑒權時不存在憑證導致的認證異常。
		sendStartAuthentication(request, response, chain, exception);
	}

	private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
			FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
		Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
		boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
		if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
			// 對于匿名用戶或者不是記住我用戶,直接跳轉登錄頁開始登錄流程
			sendStartAuthentication(request, response, chain,
					new InsufficientAuthenticationException(
							this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
									"Full authentication is required to access this resource")));
		}
		else {
			// 正常用戶則通過AccessDeniedHandler處理
			this.accessDeniedHandler.handle(request, response, exception);
		}
	}
	protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
			AuthenticationException reason) throws ServletException, IOException {
		// SEC-112: Clear the SecurityContextHolder's Authentication, as the
		// existing Authentication is no longer considered valid
		SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
		// 清空安全上下文		
		this.securityContextHolderStrategy.setContext(context);
		// 記錄當前權限不足的請求,登錄成功后可能需要自動跳轉
		this.requestCache.saveRequest(request, response);
		// 通過AuthenticationEntryPoint處理,這里是跳轉到登錄頁面
		this.authenticationEntryPoint.commence(request, response, reason);
	}
}

總結

  1. 異常處理體系包括
    異常定義分兩類 —— 認證異常、訪問拒絕異常(鑒權異常)
    異常處理器 —— AuthenticationFailureHandler、AccessDeniedHandler分別對應異常分類
    異常的處理 —— 認證過濾器和異常處理器
  2. 認證異常的處理一般是跳轉到登錄頁面。
    訪問異常的處理默認則是AccessDeniedHandlerImpl處理,發送403錯誤碼或者跳轉到錯誤頁。

后記

前陣子搬家,一直在適應新屋子的生活節奏,拖了不少時間,對不住了各位。后面應該會回復正常。
至此,咱們聊了認證、鑒權、session、異常處理,接下來咱們聊聊認證過程中一些小的功能點,例如:登錄后跳轉到之前異常的請求、RemenberMe、多處登錄控制。

原文鏈接:https://blog.csdn.net/Evan_L/article/details/139102963

  • 上一篇:沒有了
  • 下一篇:沒有了
欄目分類
最近更新