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

學(xué)無(wú)先后,達(dá)者為師

網(wǎng)站首頁(yè) 編程語(yǔ)言 正文

Spring Security之認(rèn)證過(guò)濾器

作者:Evan_L 更新時(shí)間: 2024-07-18 編程語(yǔ)言

前言

上回我們探討了關(guān)于Spring Security,著實(shí)復(fù)雜。這次咱們聊的認(rèn)證過(guò)濾器就先聊聊認(rèn)證功能。涉及到多方協(xié)同的功能,咱分開聊。也給小伙伴喘口氣,嘻嘻。此外也是因?yàn)橹挥械卿浾J(rèn)證了,才有后續(xù)的更多功能集成的可能。

認(rèn)證過(guò)濾器

認(rèn)證過(guò)濾器是Web應(yīng)用中負(fù)責(zé)處理用戶認(rèn)證請(qǐng)求的。這意味著他要驗(yàn)證用戶的身份,確認(rèn)你是誰(shuí)。只有確認(rèn)用戶身份后,才能授予對(duì)應(yīng)的權(quán)限,才能在后續(xù)訪問(wèn)對(duì)應(yīng)的受保護(hù)資源。因此,在我看來(lái),認(rèn)證過(guò)濾器實(shí)際上需要完成兩個(gè)事情:

  • 確認(rèn)用戶身份。
  • 授權(quán)。例如,角色。

SpringSecurity沒(méi)有“鑒權(quán)”這概念?

其實(shí)我認(rèn)為前面說(shuō)到的AuthorizationFilter應(yīng)該叫鑒權(quán)過(guò)濾器,在識(shí)別用戶身份后甄別用戶是否具備訪問(wèn)權(quán)限。我刻意找了一下英文,Authentication可以認(rèn)證、鑒定、身份驗(yàn)證的意思,Authorization可以是授權(quán)、批準(zhǔn)的意思。第一眼,竟然有點(diǎn)懵逼。。沒(méi)有上下文的情況下,Authentication可以是認(rèn)證,也可以是鑒權(quán)。而Authorization可以是授權(quán)、也可以是鑒權(quán)(批準(zhǔn)-訪問(wèn))。

得,我一直以來(lái)的疑惑也找到了:為什么Spring Security里面沒(méi)有“鑒權(quán)”相關(guān)概念,而只有認(rèn)證、授權(quán)?如果放到Spring Security的語(yǔ)境中,Authentication就是認(rèn)證的意思,而且還非常準(zhǔn)確,因?yàn)樗€有身份驗(yàn)證的意思。Authorization則是鑒權(quán)的含義,授權(quán)/批準(zhǔn)你訪問(wèn)某個(gè)請(qǐng)求。如果非要區(qū)分開,則還應(yīng)使用Identification作為認(rèn)證這個(gè)概念最為合適。

實(shí)際上,SpringSecurity在獲取用戶信息的時(shí)候就已經(jīng)把用戶的完整信息包括權(quán)限也加載了,所以認(rèn)證也包括了我們?cè)诹募兏拍畹摹笆跈?quán)”。而SpringSecurity的Authorization是“授權(quán)訪問(wèn)”,也就是“鑒權(quán)”。因此,可以說(shuō)在SpringSecurity中只有“認(rèn)證”和“鑒權(quán)”。因?yàn)樗蠹虞d的用戶信息是包括權(quán)限的,跟認(rèn)證在一塊了。別把“授權(quán)”概念跟RABC這些“授權(quán)方式”混為一談了,鄙人就懵圈了好久。關(guān)于授權(quán)和認(rèn)證不妨再回頭看看之前的文章:Spring Security之認(rèn)證與授權(quán)的概念

SpringSecurity支持認(rèn)證方式

Spring Security支持多種認(rèn)證方式:

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

PS: OAuth2比較復(fù)雜,有四種登錄方式,還分客戶端應(yīng)用、用戶、資源。后面有機(jī)會(huì)再細(xì)聊。

基于第三方系統(tǒng)認(rèn)證的方式,也有幾個(gè)原生的實(shí)現(xiàn):
基于J2EE認(rèn)證:J2eePreAuthenticatedProcessingFilter-JeeConfigurer
基于WebSphere: WebSpherePreAuthenticatedProcessingFilter
基于X509證書認(rèn)證:X509AuthenticationFilter-X509Configurer
基于Header:RequestHeaderAuthenticationFilter
基于RequestAttribute:RequestAttributeAuthenticationFilter

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

基于用戶名和密碼認(rèn)證

好了,下面我們來(lái)重點(diǎn)關(guān)注一下基于用戶名和密碼的認(rèn)證方式。首先我們來(lái)認(rèn)識(shí)一些必要的核心組件:

組件 作用 備注
UserDetails 提供用戶信息,包括權(quán)限 提供了默認(rèn)實(shí)現(xiàn):User
UserDetailsService 用于加載裝載用戶信息 在認(rèn)證之前需要先根據(jù)用戶名查詢用戶
AuthenticationManager 負(fù)責(zé)完成認(rèn)證邏輯 實(shí)現(xiàn)類ProviderManager

前面兩個(gè)比較容易理解,無(wú)非就是加載用戶。而后者,對(duì)于ProviderManager而言,相較于我們之前基于方法進(jìn)行權(quán)限配置的方式所使用AuthorizationManager來(lái)說(shuō),無(wú)異于單獨(dú)開辟了一塊新天地。

  • ProviderManager
    就名字而言,他就是基于AuthenticationProvider來(lái)完成。額,沒(méi)錯(cuò),他就是一個(gè)新的接口,也就是一個(gè)新的組件。而ProviderManager主要的作用就是,根據(jù)Authentication的類型,尋找匹配的AuthenticationProvider,然后調(diào)用匹配的AuthenticationProvider來(lái)完成認(rèn)證。

核心代碼:

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;
			}
			// 找到第一個(gè)能處理的AuthenticationProvider就執(zhí)行authenticate
			result = provider.authenticate(authentication);
			if (result != null) {
				copyDetails(authentication, result);
				break;
			}
		}
		// 如果認(rèn)證失敗就嘗試由父AuthenticationManager認(rèn)證,這部代碼省略了
		// 認(rèn)證成功則返回結(jié)果
		// 處理異常-省略代碼
	}
}

關(guān)于這個(gè)AuthenticationProvider,我們來(lái)感受一下他的實(shí)現(xiàn):
AuthenticationProvider
之所以會(huì)有這么多,是因?yàn)锳uthentication有很多,每個(gè)用來(lái)表示憑證的都需要不同的處理,然后才能進(jìn)行認(rèn)證。例如:JwtAuthenticationToken需要將jwtToken解析后就能得到當(dāng)前用戶已經(jīng)認(rèn)證的用戶信息了(OAuth2)。這些實(shí)現(xiàn)有不少是這種類似于token的。不過(guò)我們的用戶名和密碼方式,則是更為復(fù)雜。

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. 進(jìn)行認(rèn)證:驗(yàn)證用戶狀態(tài)、驗(yàn)證用戶憑證(密碼)
		try {
			// 驗(yàn)證用戶狀態(tài)
			this.preAuthenticationChecks.check(user);
			// 驗(yàn)證用戶憑證(密碼)
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException ex) {
			// 重試
		}
		// 驗(yàn)證成功后,檢查憑證有效期
		this.postAuthenticationChecks.check(user);
		if (!cacheWasUsed) {
			// 緩存用戶
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
		// 創(chuàng)建UsernamePasswordAuthenticationToken
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}	

	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
			UserDetails user) {
		// 構(gòu)建認(rèn)證信息(已驗(yàn)證憑證),里面包含當(dāng)前用戶信息和權(quán)限
		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) {
			// 沒(méi)有憑證
			throw new BadCredentialsException(this.messages
				.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
		String presentedPassword = authentication.getCredentials().toString();
		// 校驗(yàn)密碼
		if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			throw new BadCredentialsException(this.messages
				.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}
}

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

UsernamePasswordAuthenticationFilter

其核心實(shí)現(xiàn)在父類:

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		// 當(dāng)前請(qǐng)求是否為認(rèn)證請(qǐng)求
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		try {
			// 執(zhí)行驗(yàn)證。這是個(gè)抽象方法,由子類實(shí)現(xiàn)。
			Authentication authenticationResult = attemptAuthentication(request, response);
			if (authenticationResult == null) {
				// 沒(méi)有返回結(jié)果,表示子類還沒(méi)有完成。正常情況走不到這里
				return;
			}
			// 執(zhí)行session策略
			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
			// 認(rèn)證成功,執(zhí)行后續(xù)處理
			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. 將當(dāng)前認(rèn)證信息保存到SecurityContextHolder中,一般是ThreadLocal,以便后續(xù)處理直接使用。
		SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
		context.setAuthentication(authResult);
		this.securityContextHolderStrategy.setContext(context);
		// 2. 將SecurityContext保存起來(lái),一般是session中。這樣后續(xù)的每個(gè)請(qǐng)求都能從中恢復(fù)當(dāng)前用戶信息,實(shí)現(xiàn)可連續(xù)交互式會(huì)話。
		this.securityContextRepository.saveContext(context, request, response);
		// 記住我功能
		this.rememberMeServices.loginSuccess(request, response, authResult);
		// 發(fā)布認(rèn)證成功事件
		if (this.eventPublisher != null) {
			this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
		}
		// 認(rèn)證成功后的處理。與認(rèn)證成功后需要重定向跳轉(zhuǎn)有關(guān)。
		this.successHandler.onAuthenticationSuccess(request, response, authResult);
	}
}

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		// 是否為POST請(qǐng)求
		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 : "";
		// 構(gòu)建尚未認(rèn)證的token,此時(shí)沒(méi)有權(quán)限
		UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
				password);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		// 通過(guò)ProviderManager認(rèn)證成功后,也就能獲取到數(shù)據(jù)庫(kù)中保存的權(quán)限了。
		return this.getAuthenticationManager().authenticate(authRequest);
	}
}

認(rèn)證成功涉及的組件

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

由于這些都與session管理有著不可分割的關(guān)系,因此,我們留待后續(xù)聊session管理的時(shí)候再說(shuō)。

小結(jié)

  • 核心認(rèn)證流程
    1. 從HttpServletRequest中獲取到用戶名和密碼
    2. 交給ProviderManager進(jìn)行認(rèn)證。
      DaoAuthenticationProvider會(huì)通過(guò)UserDetailsService從數(shù)據(jù)庫(kù)獲取用戶信息,然后驗(yàn)證入?yún)⒌拿艽a。
    3. 認(rèn)證成功后,創(chuàng)建SecurityContext并保存到ThreadLocal中,同時(shí)將其保存到Session中。當(dāng)然還有其他擴(kuò)展功能,后面再細(xì)聊。

后記

本文中,我們探討了Spring Security的認(rèn)證過(guò)濾器,并從源碼層面分析了UsernamePasswordAuthenticationFilter的原理和處理流程。但是我們并沒(méi)有仔細(xì)探索認(rèn)證成功之后的操作。因?yàn)檫@些涉及到Session管理,這就與另一個(gè)過(guò)濾器SessionManagementFilter有著密不可分的關(guān)系了。所以下次,我們就聊SessionManagementFilter。屆時(shí)會(huì)仔細(xì)說(shuō)說(shuō)。

參照

01 認(rèn)證、授權(quán)、鑒權(quán)和權(quán)限控制
Authentication
JAAS 認(rèn)證
【揭秘SAML協(xié)議 — Java安全認(rèn)證框架的核心基石】 從初識(shí)到精通,帶你領(lǐng)略Saml協(xié)議的奧秘,告別SSO的迷茫與困惑

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

  • 上一篇:沒(méi)有了
  • 下一篇:沒(méi)有了
欄目分類
最近更新