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

學無先后,達者為師

網站首頁 編程語言 正文

Spring Security之認證過濾器

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

前言

上回我們探討了關于Spring Security,著實復雜。這次咱們聊的認證過濾器就先聊聊認證功能。涉及到多方協同的功能,咱分開聊。也給小伙伴喘口氣,嘻嘻。此外也是因為只有登錄認證了,才有后續的更多功能集成的可能。

認證過濾器

認證過濾器是Web應用中負責處理用戶認證請求的。這意味著他要驗證用戶的身份,確認你是誰。只有確認用戶身份后,才能授予對應的權限,才能在后續訪問對應的受保護資源。因此,在我看來,認證過濾器實際上需要完成兩個事情:

  • 確認用戶身份。
  • 授權。例如,角色。

SpringSecurity沒有“鑒權”這概念?

其實我認為前面說到的AuthorizationFilter應該叫鑒權過濾器,在識別用戶身份后甄別用戶是否具備訪問權限。我刻意找了一下英文,Authentication可以認證、鑒定、身份驗證的意思,Authorization可以是授權、批準的意思。第一眼,竟然有點懵逼。。沒有上下文的情況下,Authentication可以是認證,也可以是鑒權。而Authorization可以是授權、也可以是鑒權(批準-訪問)。

得,我一直以來的疑惑也找到了:為什么Spring Security里面沒有“鑒權”相關概念,而只有認證、授權?如果放到Spring Security的語境中,Authentication就是認證的意思,而且還非常準確,因為他還有身份驗證的意思。Authorization則是鑒權的含義,授權/批準你訪問某個請求。如果非要區分開,則還應使用Identification作為認證這個概念最為合適。

實際上,SpringSecurity在獲取用戶信息的時候就已經把用戶的完整信息包括權限也加載了,所以認證也包括了我們在聊純概念的“授權”。而SpringSecurity的Authorization是“授權訪問”,也就是“鑒權”。因此,可以說在SpringSecurity中只有“認證”和“鑒權”。因為他要求加載的用戶信息是包括權限的,跟認證在一塊了。別把“授權”概念跟RABC這些“授權方式”混為一談了,鄙人就懵圈了好久。關于授權和認證不妨再回頭看看之前的文章:Spring Security之認證與授權的概念

SpringSecurity支持認證方式

Spring Security支持多種認證方式:

認證方式 過濾器 SecurityConfigurer 描述
基于Basic認證 BasicAuthenticationFilter HttpBasicConfigurer 原生支持,開箱即用
基于Digest認證 DigestAuthenticationFilter 無,需要自己引入 原生支持,開箱即用
基于OAuth2認證-資源服務器 BearerTokenAuthenticationFilter OAuth2ResourceServerConfigurer 需要spring-boot-starter-oauth2-resource-server包
基于OAuth2認證-客戶端 OAuth2LoginAuthenticationFilter OAuth2LoginConfigurer 需要spring-boot-starter-oauth2-resource-server包
基于OAuth2認證-客戶端 OAuth2AuthorizationCodeGrantFilter OAuth2ClientConfigurer 需要spring-boot-starter-oauth2-resource-server包
基于CAS認證 CasAuthenticationFilter 本來是有Configurer的,4.0之后被棄用,再之后就移除了 需要spring-security-cas包
基于第三方系統認證 AbstractPreAuthenticatedProcessingFilter - 用戶在其他系統已經認證了,在當前系統通過RequestAttribute/Header/Cookie等等方式獲取到用戶名后直接再當前系統把用戶信息讀取出來。
基于用戶名和密碼認證 UsernamePasswordAuthenticationFilter FormLoginConfigurer 原生支持

PS: OAuth2比較復雜,有四種登錄方式,還分客戶端應用、用戶、資源。后面有機會再細聊。

基于第三方系統認證的方式,也有幾個原生的實現:
基于J2EE認證:J2eePreAuthenticatedProcessingFilter-JeeConfigurer
基于WebSphere: WebSpherePreAuthenticatedProcessingFilter
基于X509證書認證:X509AuthenticationFilter-X509Configurer
基于Header:RequestHeaderAuthenticationFilter
基于RequestAttribute:RequestAttributeAuthenticationFilter

以上就是Spring Security原生提供的支持、或者通過官方的jar包能夠開箱即用的。

基于用戶名和密碼認證

好了,下面我們來重點關注一下基于用戶名和密碼的認證方式。首先我們來認識一些必要的核心組件:

組件 作用 備注
UserDetails 提供用戶信息,包括權限 提供了默認實現:User
UserDetailsService 用于加載裝載用戶信息 在認證之前需要先根據用戶名查詢用戶
AuthenticationManager 負責完成認證邏輯 實現類ProviderManager

前面兩個比較容易理解,無非就是加載用戶。而后者,對于ProviderManager而言,相較于我們之前基于方法進行權限配置的方式所使用AuthorizationManager來說,無異于單獨開辟了一塊新天地。

  • ProviderManager
    就名字而言,他就是基于AuthenticationProvider來完成。額,沒錯,他就是一個新的接口,也就是一個新的組件。而ProviderManager主要的作用就是,根據Authentication的類型,尋找匹配的AuthenticationProvider,然后調用匹配的AuthenticationProvider來完成認證。

核心代碼:

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		Authentication result = null;
		Authentication parentResult = null;
		int size = this.providers.size();
		// 遍歷所有的AuthenticationProvider 
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			// 找到第一個能處理的AuthenticationProvider就執行authenticate
			result = provider.authenticate(authentication);
			if (result != null) {
				copyDetails(authentication, result);
				break;
			}
		}
		// 如果認證失敗就嘗試由父AuthenticationManager認證,這部代碼省略了
		// 認證成功則返回結果
		// 處理異常-省略代碼
	}
}

關于這個AuthenticationProvider,我們來感受一下他的實現:
AuthenticationProvider
之所以會有這么多,是因為Authentication有很多,每個用來表示憑證的都需要不同的處理,然后才能進行認證。例如:JwtAuthenticationToken需要將jwtToken解析后就能得到當前用戶已經認證的用戶信息了(OAuth2)。這些實現有不少是這種類似于token的。不過我們的用戶名和密碼方式,則是更為復雜。

public abstract class AbstractUserDetailsAuthenticationProvider
		implements AuthenticationProvider, InitializingBean, MessageSourceAware {
		
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		// 1. 獲取用戶名
		String username = determineUsername(authentication);
		// 2. 嘗試從緩存中獲取用戶信息
		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			// 緩存中不存在,則加載用戶:使用UserDetailsService
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
		}
		// 3. 進行認證:驗證用戶狀態、驗證用戶憑證(密碼)
		try {
			// 驗證用戶狀態
			this.preAuthenticationChecks.check(user);
			// 驗證用戶憑證(密碼)
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException ex) {
			// 重試
		}
		// 驗證成功后,檢查憑證有效期
		this.postAuthenticationChecks.check(user);
		if (!cacheWasUsed) {
			// 緩存用戶
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
		// 創建UsernamePasswordAuthenticationToken
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}	

	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
			UserDetails user) {
		// 構建認證信息(已驗證憑證),里面包含當前用戶信息和權限
		UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal,
				authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());
		return result;
	}
}

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

	@Override
	@SuppressWarnings("deprecation")
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			// 沒有憑證
			throw new BadCredentialsException(this.messages
				.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
		String presentedPassword = authentication.getCredentials().toString();
		// 校驗密碼
		if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			throw new BadCredentialsException(this.messages
				.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}
}

現在我們知道用戶是怎么完成認證的,還有很重要的一環:認證成功之后,用戶信息怎么保存?我們都知道一般保存在Session中。

UsernamePasswordAuthenticationFilter

其核心實現在父類:

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		// 當前請求是否為認證請求
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		try {
			// 執行驗證。這是個抽象方法,由子類實現。
			Authentication authenticationResult = attemptAuthentication(request, response);
			if (authenticationResult == null) {
				// 沒有返回結果,表示子類還沒有完成。正常情況走不到這里
				return;
			}
			// 執行session策略
			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
			// 認證成功,執行后續處理
			successfulAuthentication(request, response, chain, authenticationResult);
		}
		catch (InternalAuthenticationServiceException failed) {
			unsuccessfulAuthentication(request, response, failed);
		}
		catch (AuthenticationException ex) {
			unsuccessfulAuthentication(request, response, ex);
		}
	}
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
			Authentication authResult) throws IOException, ServletException {
		// 1. 將當前認證信息保存到SecurityContextHolder中,一般是ThreadLocal,以便后續處理直接使用。
		SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
		context.setAuthentication(authResult);
		this.securityContextHolderStrategy.setContext(context);
		// 2. 將SecurityContext保存起來,一般是session中。這樣后續的每個請求都能從中恢復當前用戶信息,實現可連續交互式會話。
		this.securityContextRepository.saveContext(context, request, response);
		// 記住我功能
		this.rememberMeServices.loginSuccess(request, response, authResult);
		// 發布認證成功事件
		if (this.eventPublisher != null) {
			this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
		}
		// 認證成功后的處理。與認證成功后需要重定向跳轉有關。
		this.successHandler.onAuthenticationSuccess(request, response, authResult);
	}
}

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		// 是否為POST請求
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		// 獲取用戶名
		String username = obtainUsername(request);
		username = (username != null) ? username.trim() : "";
		// 獲取密碼
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
		// 構建尚未認證的token,此時沒有權限
		UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
				password);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		// 通過ProviderManager認證成功后,也就能獲取到數據庫中保存的權限了。
		return this.getAuthenticationManager().authenticate(authRequest);
	}
}

認證成功涉及的組件

組件 作用 描述
SessionAuthenticationStrategy Session認證(成功)策略 在用戶認證成功后,調整session;修改sessionId或者重建session
SecurityContextHolderStrategy 為處理當前請求的線程提供SecurityContext 一般是保存在ThreadLocal中
SecurityContextRepository 為不同請求持久化SecurityContext 用戶認證成功后,主要是為了完成后續請求。因此需要將SecurityContext持久化。而恢復ThreadLocal中的SecurityContext,也需要從這里獲取
RememberMeServices 記住我Service 在認證成功后,若開啟記住我功能,需要生成RemenberMeToken。后面才能使用該Token進行認證而無需用戶輸入密碼
AuthenticationSuccessHandler 認證成功后的處理器 這是對于用戶而言的,認證成功后需要給用戶呈現什么內容/頁面

由于這些都與session管理有著不可分割的關系,因此,我們留待后續聊session管理的時候再說。

小結

  • 核心認證流程
    1. 從HttpServletRequest中獲取到用戶名和密碼
    2. 交給ProviderManager進行認證。
      DaoAuthenticationProvider會通過UserDetailsService從數據庫獲取用戶信息,然后驗證入參的密碼。
    3. 認證成功后,創建SecurityContext并保存到ThreadLocal中,同時將其保存到Session中。當然還有其他擴展功能,后面再細聊。

后記

本文中,我們探討了Spring Security的認證過濾器,并從源碼層面分析了UsernamePasswordAuthenticationFilter的原理和處理流程。但是我們并沒有仔細探索認證成功之后的操作。因為這些涉及到Session管理,這就與另一個過濾器SessionManagementFilter有著密不可分的關系了。所以下次,我們就聊SessionManagementFilter。屆時會仔細說說。

參照

01 認證、授權、鑒權和權限控制
Authentication
JAAS 認證
【揭秘SAML協議 — Java安全認證框架的核心基石】 從初識到精通,帶你領略Saml協議的奧秘,告別SSO的迷茫與困惑

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

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