SpringSecurity?默認登錄認證的實現(xiàn)原理解析
Spring Security 默認登錄認證的實現(xiàn)原理
一、默認配置登錄認證過程
二、流程分析
由默認的 SecurityFilterChain 為例(即表單登錄),向服務(wù)器請求 /hello 資源Spring Security 的流程分析如下:
- 請求 /hello 接口,在引入 Spring Security 之后會先經(jīng)過一系列過濾器(一中請求的是 /test 接口);
- 在請求到達
FilterSecurityInterceptor
時,發(fā)現(xiàn)請求并未認證。請求被攔截下來,并拋出AccessDeniedException
異常; - 拋出
AccessDeniedException
的異常會被ExceptionTranslationFilter
捕獲,這個Filter中會去調(diào)用 LoginUrlAuthenticationEntryPoint#commence 方法給客戶端返回302(暫時重定向)
,要求客戶端進行重定向到 /login 頁面。 - 客戶端發(fā)送 /login 請求;
- /login 請求再次當(dāng)遇到
DefaultLoginPageGeneratingFilter
過濾器時,會返回登錄頁面。
登錄頁面的由來
下面是DefaultLoginPageGeneratingFilter
重寫的doFilter
方法,也可以解釋默認配置下為什么會返回登錄頁,登錄頁就由下面的過濾器實現(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); // 判斷是否是登錄請求、登錄錯誤和注銷確認 // 不是的話給用戶返回登錄界面 if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) { chain.doFilter(request, response); } else { // generateLoginPageHtml方法中有對頁面登錄代碼進行了字符串拼接 // 太長了,這里就不給出來了 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); } }
表單登錄認證過程(源碼分析)
在重定向到登錄頁面后,會有個疑問,它是怎么校驗的,怎么對用戶名和密碼進行認證的呢?
首先知道默認加載中是開啟了表單認證的,在【深入淺出Spring Security(二)】Spring Security的實現(xiàn)原理 中小編指出了默認加載的過濾器中有一個UsernamePasswordAuthenticationFilter
,它是來處理表單請求的,其實它是在調(diào)用 HttpSecurity
中的 formLogin
方法配置的過濾器的。
接下來分析一個 UsernamePasswordAuthenticationFilter 干了什么(它不是原生的過濾器,里面是attemptAuthetication進行過濾,而不是doFilter,參數(shù)與原生過濾器相比少了個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中的認證 // 通過調(diào)用AuthenticationManager中的authenticate方法 // 需要傳遞的參數(shù)的Authentication對象,當(dāng)時是這樣解釋的 return this.getAuthenticationManager() .authenticate(authRequest); }
這邊經(jīng)過調(diào)試進入到 authenticate
方法觀察如何認證的,下面是調(diào)試的認證過程:
1.進入 authenticate 方法后會調(diào)用 ProviderManager
下的 authenticate 方法,它是重寫 AuthenticationManager 的,第一次 providers 里只有 AnoymousAuthenticationProvider 對象,用來匿名認證的,最后會判斷支不支持此認證,不支持換Provider;
2.此時匿名認證匹配不了,往下執(zhí)行,由于parent
屬性不為空,所以會調(diào)用 parent 的 authenticate 進行認證。(其parent也是一個ProviderManager對象,但其 providers 集合中有且存在 DaoAuthenticationProvider
認證對象)。
從這可以間接推出在 UsernamePasswordAuthenticationFilter
中的 AuthenticationManager對象 是通過以下構(gòu)造方法得出來的。
3.既然 provider.supports
方法匹配成功,那就讓provider去驗證,然后將驗證后的結(jié)果集返回。
DaoAuthenticationProvider 中未重寫 AuthenticationProvider 中的 authenticate 方法,由其抽象父類 AbstractUserDetailsAuthenticationProvider
實現(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); // 這里是驗證密碼的,通過子類DaoAuthenticationProvider的這個方法對密碼去進行驗證 // 傳過去的參數(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
下的一個方法,用來返回 UserDetails 對象,即用戶的詳細信息,方便等等封裝到認證信息 Authentication 中然后返回結(jié)果,判斷是否認證成功。
// 一共兩個參數(shù),一個是用戶名,一個是傳過來的認證信息 @Override protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { // 核心方法就是這個,通過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); } }
我們可以看見默認配置下它是一個 InMemoryUserDetailsManager 對象,是一個基于內(nèi)存的關(guān)于UserDetails 的操作對象。
簡單看看它里面的loadUserByUsername方法,寫的也是非常簡單,它這里面用戶名不區(qū)分大小寫。
6.再說說密碼驗證,密碼驗證在3
源碼里指出了,在獲取UserDetails對象user后,會調(diào)用子類的additionalAuthenticationChecks
方法進行密碼驗證。主要就是和輸出框輸入的密碼和那個UserDetails對象中的密碼進行比較,UserDetails 密碼可以理解為是通過 PasswordEncoder
編碼后的密碼(密文),而輸入框輸入的是可以理解為是明文,可以簡單這樣先理解。然后通過 PasswordEncoder
去看看是否匹配。默認是 DelegatingPasswordEncoder
密碼編碼器;
三、UserDetailsService
Spring Security 中 UserDetailsService 的實現(xiàn)
- UserDetailsManager 在 UserDetailsService 的基礎(chǔ)上,繼續(xù)定義了添加用戶、更新用戶、刪除用戶、修改密碼以及判斷用戶是否存在共 5 種方法。
- JdbcDaoImpl 在 UserDetailsService 的基礎(chǔ)上,通過 spring-jdbc 實現(xiàn)了從數(shù)據(jù)庫中查詢用戶的方法。
- InMemoryUserDetailsManager 實現(xiàn)了 UserDetailsManager 中關(guān)于用戶的增刪改查方法,不過都是基于內(nèi)存的操作,數(shù)據(jù)并沒有持久化。
- JdbcUserDetailsManager 繼承自 JdbcDaoImpl 同時又實現(xiàn)了 UserDetailsManager 接口,因此可以通過 JdbcUserDetailsManager 實現(xiàn)對用戶的增刪改查操作,這些操作都會持久化到數(shù)據(jù)庫中。不過 JdbcUserDetailsManager 有一個局限性,就是操作數(shù)據(jù)庫中用戶的 SQL 都是提前寫好的,不夠靈活,因此在實際開發(fā)中 JdbcUserDetailsManager 使用并不多。
- CachingUserDetailsService 的特點是會將 UserDetailsService 緩存起來。
- UserDetailsServiceDelegator 則是提供了 UserDetailsService 的懶加載功能。
- ReactiveUserDetailsServiceAdapter 是 webflux-web-security 模塊定義的 UserDetailsService 的實現(xiàn)。
默認的 UserDetailsService 配置(源碼分析)
關(guān)于UserDetailsService的默認配置在UserDetailsServiceAutoConfiguration
自動配置類中。(由于代碼很長,這里只提取核心部分)
@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)想到啥?自動化配置 SecurityFilterChain 遇到過。
上面配置意思的,要想使用默認配置,得先滿足容器中不含 AuthenticationManager、AuthenticationProvider、UserDetailsService、AuthenticationManagerResolver實例這個條件。
默認用戶名和密碼
從上面自動化配置 UserDetailsService 中,我們也發(fā)現(xiàn)了使用的User對象是從 SecurityProperties
中獲取的,那咱看一下是怎么個 User 對象吧。
首先是調(diào)用的 getUser 去獲取的,而這個user 就一直接 new 的一個User對象,它是一個靜態(tài)內(nèi)部類實例。
看下面靜態(tài)內(nèi)部類User屬性可以看見,其用戶名name是"user",而密碼則是一個UUID字符串,roles是一個list集合,可以指定多個。
注意:下面的 getter、setter 方法沒有截取出來。
那可不可以自己配置用戶名和密碼呢?
當(dāng)然是可以滴。
可以看見,SecurityProperties
被 @ConfigurationProperties
注解修飾了(這里得知道SecurityProperties是由Spring容器管理的一個對象)。
而 @ConfigurationProperties 注解是通過 setter 注入的方式,將配置文件配置的值,映射到被該注解修飾的對象中。
所以我們可以在配置文件中進行自己的配置,可以配置自己的用戶名和密碼。
比如我這么配置:
# 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 去認證,最后還是調(diào)到 DaoAuthenticationProvider 的父類 AbstractUserDetailsAuthenticationProvider 的 authenticate 去認證,我們得清楚這個流程和這些類、方法,方便后期需要以及調(diào)試可用。
- 我們可以通過去實現(xiàn) UserDetailsService 接口(自定義UserDetailsService),然后將實現(xiàn)類實例交給 Spring 容器管理,這樣就不會用默認實現(xiàn)了,而是用我們的自定義實現(xiàn)。
- UserDetails 是用戶的詳情對象,里面封裝了用戶名、密碼、權(quán)限等信息。也是 UserDetailsService 的返回值,這些都是可以自定義的。
到此這篇關(guān)于SpringSecurity 默認登錄認證的實現(xiàn)原理的文章就介紹到這了,更多相關(guān)SpringSecurity登錄認證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring細數(shù)兩種代理模式之靜態(tài)代理和動態(tài)代理概念及使用
代理是一種設(shè)計模式,提供了對目標(biāo)對象另外的訪問方式,即通過代理對象訪問目標(biāo)對象??梢圆恍薷哪繕?biāo)對象,對目標(biāo)對象功能進行拓展。在我們學(xué)習(xí)Spring的時候就會發(fā)現(xiàn),AOP(面向切面編程)的底層就是代理2023-02-02基于Java設(shè)計一個高并發(fā)的秒殺系統(tǒng)
這篇文章主要為大家詳細介紹了如何基于Java設(shè)計一個高并發(fā)的秒殺系統(tǒng),文中的示例代碼講解詳細,具有一定的借鑒價值,有需要的小伙伴可以參考下2023-10-10Java數(shù)組,去掉重復(fù)值、增加、刪除數(shù)組元素的實現(xiàn)方法
下面小編就為大家?guī)硪黄狫ava數(shù)組,去掉重復(fù)值、增加、刪除數(shù)組元素的實現(xiàn)方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-08-08SpringBoot整合SSO(single sign on)單點登錄
這篇文章主要介紹了SpringBoot整合SSO(single sign on)單點登錄,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06