網站首頁 編程語言 正文
前言
上回我們探討了關于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,我們來感受一下他的實現:
之所以會有這么多,是因為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管理的時候再說。
小結
- 核心認證流程
- 從HttpServletRequest中獲取到用戶名和密碼
- 交給ProviderManager進行認證。
DaoAuthenticationProvider會通過UserDetailsService從數據庫獲取用戶信息,然后驗證入參的密碼。 - 認證成功后,創建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
- 上一篇:沒有了
- 下一篇:沒有了
相關推薦
- 2022-09-22 遞歸和迭代(深度優先,廣度優先)的差異
- 2023-01-21 python?flask自定義404錯誤頁面方式_python
- 2022-01-16 npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR!
- 2024-01-11 spring 事務控制 設置手動回滾 TransactionAspectSupport.curren
- 2022-10-03 react進階教程之異常處理機制error?Boundaries_React
- 2022-06-16 基于Python+Matplotlib實現直方圖的繪制_python
- 2022-05-04 shell腳本配合zabbix實現tomcat的故障自愈功能_linux shell
- 2022-05-03 詳解Python利用APScheduler框架實現定時任務_python
- 欄目分類
-
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支