一、默認配置登錄認證過程
二、流程分析
由默認的 SecurityFilterChain 為例(即表單登錄),向服務器請求 /hello 資源Spring Security 的流程分析如下:
- 請求 /hello 接口,在引入 Spring Security 之后會先經過一系列過濾器(一中請求的是 /test 接口);
- 在請求到達
FilterSecurityInterceptor
時,發(fā)現請求并未認證。請求被攔截下來,并拋出AccessDeniedException
異常; - 拋出
AccessDeniedException
的異常會被ExceptionTranslationFilter
捕獲,這個Filter中會去調用 LoginUrlAuthenticationEntryPoint#commence 方法給客戶端返回302(暫時重定向)
,要求客戶端進行重定向到 /login 頁面。 - 客戶端發(fā)送 /login 請求;
- /login 請求再次當遇到
DefaultLoginPageGeneratingFilter
過濾器時,會返回登錄頁面。
登錄頁面的由來
下面是DefaultLoginPageGeneratingFilter
重寫的doFilter
方法,也可以解釋默認配置下為什么會返回登錄頁,登錄頁就由下面的過濾器實現而來。
// 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的實現原理 中小編指出了默認加載的過濾器中有一個UsernamePasswordAuthenticationFilter
,它是來處理表單請求的,其實它是在調用 HttpSecurity
中的 formLogin
方法配置的過濾器的。
接下來分析一個 UsernamePasswordAuthenticationFilter 干了什么(它不是原生的過濾器,里面是attemptAuthetication進行過濾,而不是doFilter,參數與原生過濾器相比少了個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的數據
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
// 這是獲取表單輸入框名為password的數據
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中的認證
// 通過調用AuthenticationManager中的authenticate方法
// 需要傳遞的參數的Authentication對象,當時是這樣解釋的
return this.getAuthenticationManager()
.authenticate(authRequest);
}
這邊經過調試進入到 authenticate
方法觀察如何認證的,下面是調試的認證過程:
-
進入 authenticate 方法后會調用
ProviderManager
下的 authenticate 方法,它是重寫 AuthenticationManager 的,第一次 providers 里只有 AnoymousAuthenticationProvider 對象,用來匿名認證的,最后會判斷支不支持此認證,不支持換Provider; -
此時匿名認證匹配不了,往下執(zhí)行,由于
parent
屬性不為空,所以會調用 parent 的 authenticate 進行認證。(其parent也是一個ProviderManager對象,但其 providers 集合中有且存在DaoAuthenticationProvider
認證對象)。從這可以間接推出在
UsernamePasswordAuthenticationFilter
中的 AuthenticationManager對象 是通過以下構造方法得出來的。 -
既然
provider.supports
方法匹配成功,那就讓provider去驗證,然后將驗證后的結果集返回。DaoAuthenticationProvider 中未重寫 AuthenticationProvider 中的 authenticate 方法,由其抽象父類
AbstractUserDetailsAuthenticationProvider
實現的。核心方法通過retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
去獲取UserDetails
對象,然后結合一些其他參數去創(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 對象,顯然沒有,咱剛調試呢,哪來的緩存
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
// 既然為空呢,就說明這不是從緩存中拿的,調為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的這個方法對密碼去進行驗證
// 傳過去的參數是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);
}
- 接下來就是核心方法
retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication)
的概述了,它是DaoAuthenticationProvider
下的一個方法,用來返回 UserDetails 對象,即用戶的詳細信息,方便等等封裝到認證信息 Authentication 中然后返回結果,判斷是否認證成功。
// 一共兩個參數,一個是用戶名,一個是傳過來的認證信息
@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 對象,是一個基于內存的關于UserDetails 的操作對象。簡單看看它里面的loadUserByUsername方法,寫的也是非常簡單,它這里面用戶名不區(qū)分大小寫。
- 再說說密碼驗證,密碼驗證在
3
源碼里指出了,在獲取UserDetails對象user后,會調用子類的additionalAuthenticationChecks
方法進行密碼驗證。主要就是和輸出框輸入的密碼和那個UserDetails對象中的密碼進行比較,UserDetails 密碼可以理解為是通過PasswordEncoder
編碼后的密碼(密文),而輸入框輸入的是可以理解為是明文,可以簡單這樣先理解。然后通過PasswordEncoder
去看看是否匹配。默認是DelegatingPasswordEncoder
密碼編碼器;
三、UserDetailsService
Spring Security 中 UserDetailsService 的實現
- UserDetailsManager 在 UserDetailsService 的基礎上,繼續(xù)定義了添加用戶、更新用戶、刪除用戶、修改密碼以及判斷用戶是否存在共 5 種方法。
- JdbcDaoImpl 在 UserDetailsService 的基礎上,通過 spring-jdbc 實現了從數據庫中查詢用戶的方法。
- InMemoryUserDetailsManager 實現了 UserDetailsManager 中關于用戶的增刪改查方法,不過都是基于內存的操作,數據并沒有持久化。
- JdbcUserDetailsManager 繼承自 JdbcDaoImpl 同時又實現了 UserDetailsManager 接口,因此可以通過 JdbcUserDetailsManager 實現對用戶的增刪改查操作,這些操作都會持久化到數據庫中。不過 JdbcUserDetailsManager 有一個局限性,就是操作數據庫中用戶的 SQL 都是提前寫好的,不夠靈活,因此在實際開發(fā)中 JdbcUserDetailsManager 使用并不多。
- CachingUserDetailsService 的特點是會將 UserDetailsService 緩存起來。
- UserDetailsServiceDelegator 則是提供了 UserDetailsService 的懶加載功能。
- ReactiveUserDetailsServiceAdapter 是 webflux-web-security 模塊定義的 UserDetailsService 的實現。
默認的 UserDetailsService 配置(源碼分析)
關于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)內部類)
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
,聯想到啥?自動化配置 SecurityFilterChain 遇到過。
上面配置意思的,要想使用默認配置,得先滿足容器中不含 AuthenticationManager、AuthenticationProvider、UserDetailsService、AuthenticationManagerResolver實例這個條件。
默認用戶名和密碼
從上面自動化配置 UserDetailsService 中,我們也發(fā)現了使用的User對象是從 SecurityProperties
中獲取的,那咱看一下是怎么個 User 對象吧。
首先是調用的 getUser 去獲取的,而這個user 就一直接 new 的一個User對象,它是一個靜態(tài)內部類實例。
看下面靜態(tài)內部類User屬性可以看見,其用戶名name是"user",而密碼則是一個UUID字符串,roles是一個list集合,可以指定多個。注意:下面的 getter、setter 方法沒有截取出來。
那可不可以自己配置用戶名和密碼呢?
當然是可以滴。可以看見,
SecurityProperties
被 @ConfigurationProperties
注解修飾了(這里得知道SecurityProperties是由Spring容器管理的一個對象)。
而 @ConfigurationProperties 注解是通過 setter 注入的方式,將配置文件配置的值,映射到被該注解修飾的對象中。
所以我們可以在配置文件中進行自己的配置,可以配置自己的用戶名和密碼。
比如我這么配置:
# application.yml
spring:
security:
user:
name: xxx
password: 123
用戶名、密碼就被更改。文章來源:http://www.zghlxwxcb.cn/news/detail-484753.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-484753.html
四、總結
- AuthenticationManager、ProviderManager、AuthenticationProvider關系。
- 得知道
DaoAuthenticationProvider
retrieveUser 方法和 additionalAuthenticationChecks 方法(這倆方法分別應用了UserDetailsService和PasswordEncoder對象)。UsernamePasswordAuthenticationFilter
最后也是去通過ProviderManager
中的 authenticate 去認證,最后還是調到 DaoAuthenticationProvider 的父類AbstractUserDetailsAuthenticationProvider
的 authenticate 去認證,我們得清楚這個流程和這些類、方法,方便后期需要以及調試可用。 - 我們可以通過去實現
UserDetailsService
接口(自定義UserDetailsService),然后將實現類實例交給 Spring 容器管理,這樣就不會用默認實現了,而是用我們的自定義實現。 - UserDetails 是用戶的詳情對象,里面封裝了用戶名、密碼、權限等信息。也是 UserDetailsService 的返回值,這些都是可以自定義的。
到了這里,關于【深入淺出Spring Security(三)】默認登錄認證的實現原理的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!