SpringSecurity?默認(rèn)登錄認(rèn)證的實(shí)現(xiàn)原理解析
Spring Security 默認(rèn)登錄認(rèn)證的實(shí)現(xiàn)原理
一、默認(rèn)配置登錄認(rèn)證過程
二、流程分析
由默認(rèn)的 SecurityFilterChain 為例(即表單登錄),向服務(wù)器請求 /hello 資源Spring Security 的流程分析如下:
- 請求 /hello 接口,在引入 Spring Security 之后會先經(jīng)過一系列過濾器(一中請求的是 /test 接口);
- 在請求到達(dá)
FilterSecurityInterceptor
時(shí),發(fā)現(xiàn)請求并未認(rèn)證。請求被攔截下來,并拋出AccessDeniedException
異常; - 拋出
AccessDeniedException
的異常會被ExceptionTranslationFilter
捕獲,這個(gè)Filter中會去調(diào)用 LoginUrlAuthenticationEntryPoint#commence 方法給客戶端返回302(暫時(shí)重定向)
,要求客戶端進(jìn)行重定向到 /login 頁面。 - 客戶端發(fā)送 /login 請求;
- /login 請求再次當(dāng)遇到
DefaultLoginPageGeneratingFilter
過濾器時(shí),會返回登錄頁面。
登錄頁面的由來
下面是DefaultLoginPageGeneratingFilter
重寫的doFilter
方法,也可以解釋默認(rèn)配置下為什么會返回登錄頁,登錄頁就由下面的過濾器實(shí)現(xiàn)而來。
// DefaultLoginPageGeneratingFilter @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain); } private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { boolean loginError = this.isErrorPage(request); boolean logoutSuccess = this.isLogoutSuccess(request); // 判斷是否是登錄請求、登錄錯(cuò)誤和注銷確認(rèn) // 不是的話給用戶返回登錄界面 if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) { chain.doFilter(request, response); } else { // generateLoginPageHtml方法中有對頁面登錄代碼進(jìn)行了字符串拼接 // 太長了,這里就不給出來了 String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess); response.setContentType("text/html;charset=UTF-8"); response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length); response.getWriter().write(loginPageHtml); } }
表單登錄認(rèn)證過程(源碼分析)
在重定向到登錄頁面后,會有個(gè)疑問,它是怎么校驗(yàn)的,怎么對用戶名和密碼進(jìn)行認(rèn)證的呢?
首先知道默認(rèn)加載中是開啟了表單認(rèn)證的,在【深入淺出Spring Security(二)】Spring Security的實(shí)現(xiàn)原理 中小編指出了默認(rèn)加載的過濾器中有一個(gè)UsernamePasswordAuthenticationFilter
,它是來處理表單請求的,其實(shí)它是在調(diào)用 HttpSecurity
中的 formLogin
方法配置的過濾器的。
接下來分析一個(gè) UsernamePasswordAuthenticationFilter 干了什么(它不是原生的過濾器,里面是attemptAuthetication進(jìn)行過濾,而不是doFilter,參數(shù)與原生過濾器相比少了個(gè)chain):
@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()); } // 獲取用戶名和密碼 // 這是通過獲取表單輸入框名為username的數(shù)據(jù) String username = obtainUsername(request); username = (username != null) ? username.trim() : ""; // 這是獲取表單輸入框名為password的數(shù)據(jù) String password = obtainPassword(request); password = (password != null) ? password : ""; UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); // 在一中小編也說了,這是Security中的認(rèn)證 // 通過調(diào)用AuthenticationManager中的authenticate方法 // 需要傳遞的參數(shù)的Authentication對象,當(dāng)時(shí)是這樣解釋的 return this.getAuthenticationManager() .authenticate(authRequest); }
這邊經(jīng)過調(diào)試進(jìn)入到 authenticate
方法觀察如何認(rèn)證的,下面是調(diào)試的認(rèn)證過程:
1.進(jìn)入 authenticate 方法后會調(diào)用 ProviderManager
下的 authenticate 方法,它是重寫 AuthenticationManager 的,第一次 providers 里只有 AnoymousAuthenticationProvider 對象,用來匿名認(rèn)證的,最后會判斷支不支持此認(rèn)證,不支持換Provider;
2.此時(shí)匿名認(rèn)證匹配不了,往下執(zhí)行,由于parent
屬性不為空,所以會調(diào)用 parent 的 authenticate 進(jìn)行認(rèn)證。(其parent也是一個(gè)ProviderManager對象,但其 providers 集合中有且存在 DaoAuthenticationProvider
認(rèn)證對象)。
從這可以間接推出在 UsernamePasswordAuthenticationFilter
中的 AuthenticationManager對象 是通過以下構(gòu)造方法得出來的。
3.既然 provider.supports
方法匹配成功,那就讓provider去驗(yàn)證,然后將驗(yàn)證后的結(jié)果集返回。
DaoAuthenticationProvider 中未重寫 AuthenticationProvider 中的 authenticate 方法,由其抽象父類 AbstractUserDetailsAuthenticationProvider
實(shí)現(xiàn)的。核心方法通過retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
去獲取UserDetails
對象,然后結(jié)合一些其他參數(shù)去創(chuàng)Authentication對象將其返回。
AbstractUserDetailsAuthenticationProvider下的authenticate方法 @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 斷言 authentication 是否是UsernamePasswordAuthenticationToken對象 Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); // 獲取一下用戶名 String username = determineUsername(authentication); boolean cacheWasUsed = true; // 從緩存中拿UserDetails 對象,顯然沒有,咱剛調(diào)試呢,哪來的緩存 UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { // 既然為空呢,就說明這不是從緩存中拿的,調(diào)為false cacheWasUsed = false; try { // 核心代碼,獲取UserDetails對象去 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException ex) { this.logger.debug("Failed to find user '" + username + "'"); if (!this.hideUserNotFoundExceptions) { throw ex; } throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { this.preAuthenticationChecks.check(user); // 這里是驗(yàn)證密碼的,通過子類DaoAuthenticationProvider的這個(gè)方法對密碼去進(jìn)行驗(yàn)證 // 傳過去的參數(shù)是user(UserDetails對象)和authentication對象 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException ex) { if (!cacheWasUsed) { throw ex; } // There was a problem, so try again after checking // we're using latest data (i.e. not from the cache) cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); this.preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); }
4.接下來就是核心方法 retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication)
的概述了,它是 DaoAuthenticationProvider
下的一個(gè)方法,用來返回 UserDetails 對象,即用戶的詳細(xì)信息,方便等等封裝到認(rèn)證信息 Authentication 中然后返回結(jié)果,判斷是否認(rèn)證成功。
// 一共兩個(gè)參數(shù),一個(gè)是用戶名,一個(gè)是傳過來的認(rèn)證信息 @Override protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { // 核心方法就是這個(gè),通過UserDetatilsService中的loadUserByUsername方法去獲取UserDetails對象 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; } catch (UsernameNotFoundException ex) { mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } }
我們可以看見默認(rèn)配置下它是一個(gè) InMemoryUserDetailsManager 對象,是一個(gè)基于內(nèi)存的關(guān)于UserDetails 的操作對象。
簡單看看它里面的loadUserByUsername方法,寫的也是非常簡單,它這里面用戶名不區(qū)分大小寫。
6.再說說密碼驗(yàn)證,密碼驗(yàn)證在3
源碼里指出了,在獲取UserDetails對象user后,會調(diào)用子類的additionalAuthenticationChecks
方法進(jìn)行密碼驗(yàn)證。主要就是和輸出框輸入的密碼和那個(gè)UserDetails對象中的密碼進(jìn)行比較,UserDetails 密碼可以理解為是通過 PasswordEncoder
編碼后的密碼(密文),而輸入框輸入的是可以理解為是明文,可以簡單這樣先理解。然后通過 PasswordEncoder
去看看是否匹配。默認(rèn)是 DelegatingPasswordEncoder
密碼編碼器;
三、UserDetailsService
Spring Security 中 UserDetailsService 的實(shí)現(xiàn)
- UserDetailsManager 在 UserDetailsService 的基礎(chǔ)上,繼續(xù)定義了添加用戶、更新用戶、刪除用戶、修改密碼以及判斷用戶是否存在共 5 種方法。
- JdbcDaoImpl 在 UserDetailsService 的基礎(chǔ)上,通過 spring-jdbc 實(shí)現(xiàn)了從數(shù)據(jù)庫中查詢用戶的方法。
- InMemoryUserDetailsManager 實(shí)現(xiàn)了 UserDetailsManager 中關(guān)于用戶的增刪改查方法,不過都是基于內(nèi)存的操作,數(shù)據(jù)并沒有持久化。
- JdbcUserDetailsManager 繼承自 JdbcDaoImpl 同時(shí)又實(shí)現(xiàn)了 UserDetailsManager 接口,因此可以通過 JdbcUserDetailsManager 實(shí)現(xiàn)對用戶的增刪改查操作,這些操作都會持久化到數(shù)據(jù)庫中。不過 JdbcUserDetailsManager 有一個(gè)局限性,就是操作數(shù)據(jù)庫中用戶的 SQL 都是提前寫好的,不夠靈活,因此在實(shí)際開發(fā)中 JdbcUserDetailsManager 使用并不多。
- CachingUserDetailsService 的特點(diǎn)是會將 UserDetailsService 緩存起來。
- UserDetailsServiceDelegator 則是提供了 UserDetailsService 的懶加載功能。
- ReactiveUserDetailsServiceAdapter 是 webflux-web-security 模塊定義的 UserDetailsService 的實(shí)現(xiàn)。
默認(rèn)的 UserDetailsService 配置(源碼分析)
關(guān)于UserDetailsService的默認(rèn)配置在UserDetailsServiceAutoConfiguration
自動(dòng)配置類中。(由于代碼很長,這里只提取核心部分)
@AutoConfiguration @ConditionalOnClass(AuthenticationManager.class) @ConditionalOnBean(ObjectPostProcessor.class) @ConditionalOnMissingBean( value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class }, type = { "org.springframework.security.oauth2.jwt.JwtDecoder", "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector", "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository", "org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" }) public class UserDetailsServiceAutoConfiguration { @Bean @Lazy public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) { // 這里是從SecurityProperties中獲取User對象(這里的User對象是SecurityProperties的靜態(tài)內(nèi)部類) SecurityProperties.User user = properties.getUser(); List<String> roles = user.getRoles(); // 然后創(chuàng)建InMemoryUserDetailsManager對象返回 // 交給Spring容器管理 return new InMemoryUserDetailsManager(User.withUsername(user.getName()) .password(getOrDeducePassword(user, passwordEncoder.getIfAvailable())) .roles(StringUtils.toStringArray(roles)) .build()); } }
觀察 UserDetailsServiceAutoConfiguration 上的注解 @ConditionalOnMissingBean
,聯(lián)想到啥?自動(dòng)化配置 SecurityFilterChain 遇到過。
上面配置意思的,要想使用默認(rèn)配置,得先滿足容器中不含 AuthenticationManager、AuthenticationProvider、UserDetailsService、AuthenticationManagerResolver實(shí)例這個(gè)條件。
默認(rèn)用戶名和密碼
從上面自動(dòng)化配置 UserDetailsService 中,我們也發(fā)現(xiàn)了使用的User對象是從 SecurityProperties
中獲取的,那咱看一下是怎么個(gè) User 對象吧。
首先是調(diào)用的 getUser 去獲取的,而這個(gè)user 就一直接 new 的一個(gè)User對象,它是一個(gè)靜態(tài)內(nèi)部類實(shí)例。
看下面靜態(tài)內(nèi)部類User屬性可以看見,其用戶名name是"user",而密碼則是一個(gè)UUID字符串,roles是一個(gè)list集合,可以指定多個(gè)。
注意:下面的 getter、setter 方法沒有截取出來。
那可不可以自己配置用戶名和密碼呢?
當(dāng)然是可以滴。
可以看見,SecurityProperties
被 @ConfigurationProperties
注解修飾了(這里得知道SecurityProperties是由Spring容器管理的一個(gè)對象)。
而 @ConfigurationProperties 注解是通過 setter 注入的方式,將配置文件配置的值,映射到被該注解修飾的對象中。
所以我們可以在配置文件中進(jìn)行自己的配置,可以配置自己的用戶名和密碼。
比如我這么配置:
# application.yml spring: security: user: name: xxx password: 123
用戶名、密碼就被更改。
四、總結(jié)
AuthenticationManager、ProviderManager、AuthenticationProvider關(guān)系。
- 得知道 DaoAuthenticationProvider retrieveUser 方法和 additionalAuthenticationChecks 方法(這倆方法分別應(yīng)用了UserDetailsService和PasswordEncoder對象)。UsernamePasswordAuthenticationFilter 最后也是去通過 ProviderManager 中的 authenticate 去認(rèn)證,最后還是調(diào)到 DaoAuthenticationProvider 的父類 AbstractUserDetailsAuthenticationProvider 的 authenticate 去認(rèn)證,我們得清楚這個(gè)流程和這些類、方法,方便后期需要以及調(diào)試可用。
- 我們可以通過去實(shí)現(xiàn) UserDetailsService 接口(自定義UserDetailsService),然后將實(shí)現(xiàn)類實(shí)例交給 Spring 容器管理,這樣就不會用默認(rèn)實(shí)現(xiàn)了,而是用我們的自定義實(shí)現(xiàn)。
- UserDetails 是用戶的詳情對象,里面封裝了用戶名、密碼、權(quán)限等信息。也是 UserDetailsService 的返回值,這些都是可以自定義的。
到此這篇關(guān)于SpringSecurity 默認(rèn)登錄認(rèn)證的實(shí)現(xiàn)原理的文章就介紹到這了,更多相關(guān)SpringSecurity登錄認(rèn)證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- springsecurity實(shí)現(xiàn)用戶登錄認(rèn)證快速使用示例代碼(前后端分離項(xiàng)目)
- Springboot整合SpringSecurity實(shí)現(xiàn)登錄認(rèn)證和鑒權(quán)全過程
- SpringSecurity實(shí)現(xiàn)前后端分離登錄token認(rèn)證詳解
- Java SpringSecurity+JWT實(shí)現(xiàn)登錄認(rèn)證
- SpringSecurity構(gòu)建基于JWT的登錄認(rèn)證實(shí)現(xiàn)
- SpringBoot整合SpringSecurity和JWT的示例
- springsecurity?登錄認(rèn)證流程分析一(ajax)
相關(guān)文章
Spring細(xì)數(shù)兩種代理模式之靜態(tài)代理和動(dòng)態(tài)代理概念及使用
代理是一種設(shè)計(jì)模式,提供了對目標(biāo)對象另外的訪問方式,即通過代理對象訪問目標(biāo)對象??梢圆恍薷哪繕?biāo)對象,對目標(biāo)對象功能進(jìn)行拓展。在我們學(xué)習(xí)Spring的時(shí)候就會發(fā)現(xiàn),AOP(面向切面編程)的底層就是代理2023-02-02基于Java設(shè)計(jì)一個(gè)高并發(fā)的秒殺系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了如何基于Java設(shè)計(jì)一個(gè)高并發(fā)的秒殺系統(tǒng),文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,有需要的小伙伴可以參考下2023-10-10Java數(shù)組,去掉重復(fù)值、增加、刪除數(shù)組元素的實(shí)現(xiàn)方法
下面小編就為大家?guī)硪黄狫ava數(shù)組,去掉重復(fù)值、增加、刪除數(shù)組元素的實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-08-08SpringBoot整合SSO(single sign on)單點(diǎn)登錄
這篇文章主要介紹了SpringBoot整合SSO(single sign on)單點(diǎn)登錄,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06