Spring Security 源碼分析(二)【認證流程】

Spring Security 源碼分析(一)【結構總覽】:https://yuanyu.blog.csdn.net/article/details/105469059


1 認證流程

認證流程

 

  1. 用戶提交用戶名、密碼被SecurityFilterChain中的UsernamePasswordAuthenticationFilter過濾器獲取到,封裝爲請求Authentication,通常情況下是UsernamePasswordAuthenticationToken這個實現類。
  2. 然後過濾器將Authentication提交至認證管理器(AuthenticationManager)進行認證
  3. 認證成功後, AuthenticationManager身份管理器返回一個被填充滿了信息的(包括上面提到的權限信息,身份信息,細節信息,但密碼通常會被移除) Authentication實例
  4. SecurityContextHolder安全上下文容器將第3步填充了信息的 Authentication ,通過SecurityContextHolder.getContext().setAuthentication(…)方法,設置到其中;可以看出AuthenticationManager接口(認證管理器)是認證相關的核心接口,也是發起認證的出發點,它的實現類爲ProviderManager;而Spring Security支持多種認證方式,因此ProviderManager維護着一個List<AuthenticationProvider> 列表,存放多種認證方式,最終實際的認證工作是由AuthenticationProvider完成的;咱們知道web表單的對應的AuthenticationProvider實現類爲DaoAuthenticationProvider,它的內部又維護着一個UserDetailsService負責UserDetails的獲取;最終AuthenticationProvider將UserDetails填充至Authentication

//org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
	//...
    Authentication authResult;
    try {
        //
        authResult = attemptAuthentication(request, response);
        if (authResult == null) {return;}
        //
        essionStrategy.onAuthentication(authResult, request, response);
    }
    //...
    //SecurityContextHolder中設置Authentication 
    successfulAuthentication(request, response, chain, authResult);
}

//org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#successfulAuthentication
protected void successfulAuthentication(HttpServletRequest request,
		HttpServletResponse response, FilterChain chain, Authentication authResult) {
	SecurityContextHolder.getContext().setAuthentication(authResult);
}
//org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { //打斷點
	//...
	String username = obtainUsername(request);
	String password = obtainPassword(request);
	//...
	//封裝Authentication
	UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
	//...
	//AuthenticationManager
	return this.getAuthenticationManager().authenticate(authRequest);
}

認證

//public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider

//org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate
public Authentication authenticate(Authentication authentication) {
    UserDetails user = this.userCache.getUserFromCache(username); 
    if (user == null) { //緩存沒有
        cacheWasUsed = false;
        try {
            //查詢數據庫 打斷點
            user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); 
        }
    }
    try {
        preAuthenticationChecks.check(user);
        //比對密碼  打斷點
        additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
    }    
//org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser
protected final UserDetails retrieveUser(String username, //打斷點
		UsernamePasswordAuthenticationToken authentication)
    //根據賬號查詢用戶信息
    UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
//org.springframework.security.authentication.dao.DaoAuthenticationProvider#additionalAuthenticationChecks
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication){
	//
	String presentedPassword = authentication.getCredentials().toString();
	if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { //打斷點
認證核心組件的大體關係

2 AuthenticationProvider

通過前面的Spring Security認證流程我們得知,認證管理器(AuthenticationManager)委託 AuthenticationProvider完成認證工作

AuthenticationProvider是一個接口,定義如下:

public interface AuthenticationProvider {
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
	boolean supports(Class<?> authentication);
}

authenticate()方法定義了認證的實現過程,它的參數是一個Authentication,裏面包含了登錄用戶所提交的用戶名、密碼等;而返回值也是一個Authentication,這個Authentication則是在認證成功後,將用戶的權限及其他信 息重新組裝後生成

Spring Security中維護着一個List<AuthenticationProvider> 列表,存放多種認證方式,不同的認證方式使用不 同的AuthenticationProvider;如使用用戶名密碼登錄時,使用AuthenticationProvider1,短信登錄時使用 AuthenticationProvider2等等這樣的例子很多

每個AuthenticationProvider需要實現supports()方法來表明自己支持的認證方式,如我們使用表單方式認證, 在提交請求時Spring Security會生成UsernamePasswordAuthenticationToken,它是一個Authentication,裏面封裝着用戶提交的用戶名、密碼信息;而對應的,哪個AuthenticationProvider來處理它?

我們在DaoAuthenticationProvider的基類AbstractUserDetailsAuthenticationProvider發現以下代碼:

//org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#supports
public boolean supports(Class<?> authentication) {
	return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}

也就是說當web表單提交用戶名密碼時,Spring Security由DaoAuthenticationProvider處理

最後,我們來看一下Authentication(認證信息)的結構,它是一個接口,我們之前提到的UsernamePasswordAuthenticationToken就是它的實現之一

//org.springframework.security.core.Authentication
public interface Authentication extends Principal, Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();
	Object getCredentials();
	Object getDetails();
	Object getPrincipal();
	boolean isAuthenticated();
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
  1. Authentication是spring security包中的接口,直接繼承自Principal類,而Principal是位於 java.security包中的;它是表示着一個抽象主體身份,任何主體都有一個名稱,因此包含一個getName()方法
  2. getAuthorities(),權限信息列表,默認是GrantedAuthority接口的一些實現類,通常是代表權限信息的一系列字符串
  3. getCredentials(),憑證信息,用戶輸入的密碼字符串,在認證過後通常會被移除,用於保障安全
  4. getDetails(),細節信息,web應用中的實現接口通常爲 WebAuthenticationDetails,它記錄了訪問者的ip地址和sessionId的值。
  5. getPrincipal(),身份信息,大部分情況下返回的是UserDetails接口的實現類,UserDetails代表用戶的詳細信息,那從Authentication中取出來的UserDetails就是當前登錄用戶信息,它也是框架中的常用接口之一

3 UserDetailsService

3.1 認識UserDetailsService

現在咱們現在知道DaoAuthenticationProvider處理了web表單的認證邏輯,認證成功後既得到一個 Authentication(UsernamePasswordAuthenticationToken實現),裏面包含了身份信息(Principal);這個身份 信息就是一個 Object ,大多數情況下它可以被強轉爲UserDetails對象

DaoAuthenticationProvider中包含了一個UserDetailsService實例,它負責根據用戶名提取用戶信息 UserDetails(包含密碼),而後DaoAuthenticationProvider會去對比UserDetailsService提取的用戶密碼與用戶提交 的密碼是否匹配作爲認證成功的關鍵依據,因此可以通過將自定義的 UserDetailsService 公開爲spring bean來定義自定義身份驗證

//org.springframework.security.core.userdetails.UserDetailsService
public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

很多人把DaoAuthenticationProvider和UserDetailsService的職責搞混淆,其實UserDetailsService只負責從特定 的地方(通常是數據庫)加載用戶信息,僅此而已;而DaoAuthenticationProvider的職責更大,它完成完整的認 證流程,同時會把UserDetails填充至Authentication

上面一直提到UserDetails是用戶信息,咱們看一下它的真面目:

//org.springframework.security.core.userdetails.UserDetails
public interface UserDetails extends Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();
	String getPassword();
	String getUsername();
	boolean isAccountNonExpired();
	boolean isAccountNonLocked();
	boolean isCredentialsNonExpired();
	boolean isEnabled();
}

它和Authentication接口很類似,比如它們都擁有username,authorities;Authentication的getCredentials()與 UserDetails中的getPassword()需要被區分對待,前者是用戶提交的密碼憑證,後者是用戶實際存儲的密碼,認證 其實就是對這兩者的比對;Authentication中的getAuthorities()實際是由UserDetails的getAuthorities()傳遞而形 成的;還記得Authentication接口中的getDetails()方法嗎?其中的UserDetails用戶詳細信息便是經過了 AuthenticationProvider認證之後被填充的

通過實現UserDetailsService和UserDetails,我們可以完成對用戶信息獲取方式以及用戶信息字段的擴展

Spring Security提供的InMemoryUserDetailsManager(內存認證),JdbcUserDetailsManager(jdbc認證)就是 UserDetailsService的實現類,主要區別無非就是從內存還是從數據庫加載用戶


4 PasswordEncoder

4.1 認識PasswordEncoder

DaoAuthenticationProvider認證處理器通過UserDetailsService獲取到UserDetails後,它是如何與請求 Authentication中的密碼做對比呢?

在這裏Spring Security爲了適應多種多樣的加密類型,又做了抽象,DaoAuthenticationProvider通過 PasswordEncoder接口的matches方法進行密碼的對比,而具體的密碼對比細節取決於實現:

//org.springframework.security.crypto.password.PasswordEncoder
public interface PasswordEncoder {
	String encode(CharSequence rawPassword);
	boolean matches(CharSequence rawPassword, String encodedPassword);
	default boolean upgradeEncoding(String encodedPassword) {
		return false;
	}
}

而Spring Security提供很多內置的PasswordEncoder,能夠開箱即用,使用某種PasswordEncoder只需要進行如 下聲明即可,如下:

@Bean
public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}

NoOpPasswordEncoder採用字符串匹配方法,不對密碼進行加密比較處理,密碼比較流程如下:

  1. 用戶輸入密碼(明文 )
  2. DaoAuthenticationProvider獲取UserDetails(其中存儲了用戶的正確密碼)
  3. DaoAuthenticationProvider使用PasswordEncoder對輸入的密碼和正確的密碼進行校驗,密碼一致則校驗通 過,否則校驗失敗

NoOpPasswordEncoder的校驗規則拿 輸入的密碼和UserDetails中的正確密碼進行字符串比較,字符串內容一致 則校驗通過,否則 校驗失敗

實際項目中推薦使用BCryptPasswordEncoder, Pbkdf2PasswordEncoder, SCryptPasswordEncoder等,感興趣 的大家可以看看這些PasswordEncoder的具體實現。

4.2 使用BCryptPasswordEncoder

配置BCryptPasswordEncoder(在安全配置類中定義)

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

密碼格式不對會報錯:Encoded password does not look like BCrypt

//org.springframework.security.authentication.dao.DaoAuthenticationProvider#additionalAuthenticationChecks
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
	if (authentication.getCredentials() == null) {
		//
	}
	String presentedPassword = authentication.getCredentials().toString();
	//passwordEncoder
	if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {

 

@Test
public void testBCrypt(){
    //對密碼進行加密
    String hashpw = BCrypt.hashpw("123456", BCrypt.gensalt());
    System.out.println(hashpw);
    //校驗密碼
    boolean checkpw = BCrypt.checkpw("123456", "$2a$10$lVFG0yY/LsonTXN9dV/czOdIbiwbu9g7Mjno0mqDFRXy5b2k0TGUu");
    System.out.println(checkpw);
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章