Spring Security系列之認證過程(六)

蔣老溼發表於2018-12-19

Spring Security系列之認證過程(六)

類圖

為了方便理解Spring Security認證流程,特意畫了如下的類圖,包含相關的核心認證類

Spring Security系列之認證過程(六)

概述

AuthenticationManager

該物件提供了認證方法的入口,接收一個Authentiaton物件作為引數;

public interface AuthenticationManager {
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
}
複製程式碼

ProviderManager

它是 AuthenticationManager 的一個實現類,提供了基本的認證邏輯和方法;它包含了一個 List<AuthenticationProvider> 物件,通過 AuthenticationProvider 介面來擴充套件出不同的認證提供者(當Spring Security預設提供的實現類不能滿足需求的時候可以擴充套件AuthenticationProvider 覆蓋supports(Class<?> authentication) 方法);

驗證邏輯

AuthenticationManager 接收 Authentication 物件作為引數,並通過 authenticate(Authentication) 方法對其進行驗證;AuthenticationProvider實現類用來支撐對 Authentication 物件的驗證動作;UsernamePasswordAuthenticationToken實現了 Authentication主要是將使用者輸入的使用者名稱和密碼進行封裝,並供給AuthenticationManager 進行驗證;驗證完成以後將返回一個認證成功的 Authentication 物件;

Authentication

Authentication物件中的主要方法

public interface Authentication extends Principal, Serializable {
	//#1.許可權結合,可使用AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_ADMIN")返回字串許可權集合
	Collection<? extends GrantedAuthority> getAuthorities();
	//#2.使用者名稱密碼認證時可以理解為密碼
	Object getCredentials();
	//#3.認證時包含的一些資訊。
	Object getDetails();
	//#4.使用者名稱密碼認證時可理解時使用者名稱
	Object getPrincipal();
	#5.是否被認證,認證為true	
	boolean isAuthenticated();
	#6.設定是否能被認證
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
複製程式碼

ProviderManager

ProviderManagerAuthenticationManager的實現類,提供了基本認證實現邏輯和流程;

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		//#1.獲取當前的Authentication的認證型別
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		Authentication result = null;
		boolean debug = logger.isDebugEnabled();
		//#2.遍歷所有的providers使用supports方法判斷該provider是否支援當前的認證型別,不支援的話繼續遍歷
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
				#3.支援的話呼叫provider的authenticat方法認證
				result = provider.authenticate(authentication);

				if (result != null) {
					#4.認證通過的話重新生成Authentication對應的Token
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			}
			catch (InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				throw e;
			}
			catch (AuthenticationException e) {
				lastException = e;
			}
		}

		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				#5.如果#1 沒有驗證通過,則使用父型別AuthenticationManager進行驗證
				result = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
				lastException = e;
			}
		}
		#6. 是否擦出敏感資訊
		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}

			eventPublisher.publishAuthenticationSuccess(result);
			return result;
		}

		## Parent was null, or didn't authenticate (or throw an exception).

		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		prepareException(lastException, authentication);

		throw lastException;
	}
複製程式碼
  1. 遍歷所有的 Providers,然後依次執行該 Provider 的驗證方法
    • 如果某一個 Provider 驗證成功,則跳出迴圈不再執行後續的驗證;
    • 如果驗證成功,會將返回的 result 既 Authentication 物件進一步封裝為 Authentication Token; 比如 UsernamePasswordAuthenticationToken、RememberMeAuthenticationToken 等;這些 Authentication Token 也都繼承自 Authentication 物件;
  2. 如果 #1 沒有任何一個 Provider 驗證成功,則試圖使用其 parent Authentication Manager 進行驗證;
  3. 是否需要擦除密碼等敏感資訊;

AuthenticationProvider

ProviderManager通過 AuthenticationProvider 擴充套件出更多的驗證提供的方式;而 AuthenticationProvider 本身也就是一個介面,從類圖中我們可以看出它的實現類AbstractUserDetailsAuthenticationProviderAbstractUserDetailsAuthenticationProvider的子類DaoAuthenticationProviderDaoAuthenticationProvider 是Spring Security中一個核心的Provider,對所有的資料庫提供了基本方法和入口。

DaoAuthenticationProvider

DaoAuthenticationProvider主要做了以下事情

  1. 對使用者身份盡心加密操作;
#1.可直接返回BCryptPasswordEncoder,也可以自己實現該介面使用自己的加密演算法核心方法String encode(CharSequence rawPassword);和boolean matches(CharSequence rawPassword, String encodedPassword);
private PasswordEncoder passwordEncoder;
複製程式碼
  1. 實現了 AbstractUserDetailsAuthenticationProvider 兩個抽象方法,
    • 獲取使用者資訊的擴充套件點
      protected final UserDetails retrieveUser(String username,
           UsernamePasswordAuthenticationToken authentication)
           throws AuthenticationException {
       UserDetails loadedUser;
      
       try {
           loadedUser = this.getUserDetailsService().loadUserByUsername(username);
       }
      複製程式碼
      主要是通過注入UserDetailsService介面物件,並呼叫其介面方法 loadUserByUsername(String username) 獲取得到相關的使用者資訊。UserDetailsService介面非常重要。
    • 實現 additionalAuthenticationChecks 的驗證方法(主要驗證密碼);

AbstractUserDetailsAuthenticationProvider

AbstractUserDetailsAuthenticationProviderDaoAuthenticationProvider提供了基本的認證方法;

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));

		// Determine username
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();

		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);

		if (user == null) {
			cacheWasUsed = false;

			try {
				#1.獲取使用者資訊由子類實現即DaoAuthenticationProvider
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
				logger.debug("User '" + username + "' not found");

				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException(messages.getMessage(
							"AbstractUserDetailsAuthenticationProvider.badCredentials",
							"Bad credentials"));
				}
				else {
					throw notFound;
				}
			}

			Assert.notNull(user,
					"retrieveUser returned null - a violation of the interface contract");
		}

		try {
			#2.前檢查由DefaultPreAuthenticationChecks類實現(主要判斷當前使用者是否鎖定,過期,凍結User介面)
			preAuthenticationChecks.check(user);
			#3.子類實現
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				# 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);
				preAuthenticationChecks.check(user);
				additionalAuthenticationChecks(user,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			else {
				throw exception;
			}
		}
		#4.檢測使用者密碼是否過期對應#2 的User介面
		postAuthenticationChecks.check(user);

		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}

		Object principalToReturn = user;

		if (forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}

		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
複製程式碼

AbstractUserDetailsAuthenticationProvider主要實現了AuthenticationProvider的介面方法 authenticate 並提供了相關的驗證邏輯;

  1. 獲取使用者返回UserDetails AbstractUserDetailsAuthenticationProvider定義了一個抽象的方法
    protected abstract UserDetails retrieveUser(String username,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException;
    複製程式碼
  2. 三步驗證工作
    1. preAuthenticationChecks
    2. additionalAuthenticationChecks(抽象方法,子類實現)
    3. postAuthenticationChecks
  3. 將已通過驗證的使用者資訊封裝成 UsernamePasswordAuthenticationToken 物件並返回;該物件封裝了使用者的身份資訊,以及相應的許可權資訊,相關原始碼如下,
    protected Authentication createSuccessAuthentication(Object principal,
         UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
                 principal, authentication.getCredentials(),
                 authoritiesMapper.mapAuthorities(user.getAuthorities()));
         result.setDetails(authentication.getDetails());
    
         return result;
     }
    複製程式碼

UserDetailsService

UserDetailsService是一個介面,提供了一個方法

public interface UserDetailsService {
 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
複製程式碼

通過使用者名稱 username 呼叫方法 loadUserByUsername 返回了一個UserDetails介面物件(對應AbstractUserDetailsAuthenticationProvider的三步驗證方法);

public interface UserDetails extends Serializable {
 #1.許可權集合
 Collection<? extends GrantedAuthority> getAuthorities();
 #2.密碼	
 String getPassword();
 #3.使用者民
 String getUsername();
 #4.使用者是否過期
 boolean isAccountNonExpired();
 #5.是否鎖定	
 boolean isAccountNonLocked();
 #6.使用者密碼是否過期	
 boolean isCredentialsNonExpired();
 #7.賬號是否可用(可理解為是否刪除)
 boolean isEnabled();
}
複製程式碼

Spring 為UserDetailsService預設提供了一個實現類 org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl

JdbcUserDetailsManager

該實現類主要是提供基於JDBC對 User 進行增、刪、查、改的方法

public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsManager,
		GroupManager {
	// ~ Static fields/initializers
	// =====================================================================================

	// UserDetailsManager SQL
	#1.定義了一些列對資料庫操作的語句
	public static final String DEF_CREATE_USER_SQL = "insert into users (username, password, enabled) values (?,?,?)";
	public static final String DEF_DELETE_USER_SQL = "delete from users where username = ?";
	public static final String DEF_UPDATE_USER_SQL = "update users set password = ?, enabled = ? where username = ?";
	public static final String DEF_INSERT_AUTHORITY_SQL = "insert into authorities (username, authority) values (?,?)";
	public static final String DEF_DELETE_USER_AUTHORITIES_SQL = "delete from authorities where username = ?";
	public static final String DEF_USER_EXISTS_SQL = "select username from users where username = ?";
	public static final String DEF_CHANGE_PASSWORD_SQL = "update users set password = ? where username = ?";
....
複製程式碼

InMemoryUserDetailsManager

該實現類主要是提供基於記憶體對 User 進行增、刪、查、改的方法 `public class InMemoryUserDetailsManager implements UserDetailsManager { protected final Log logger = LogFactory.getLog(getClass()); #1.用MAP 儲存 private final Map<String, MutableUserDetails> users = new HashMap<String, MutableUserDetails>();

private AuthenticationManager authenticationManager;

public InMemoryUserDetailsManager() {
}

public InMemoryUserDetailsManager(Collection<UserDetails> users) {
	for (UserDetails user : users) {
		createUser(user);
	}
}`
複製程式碼

總結

UserDetailsService介面作為橋樑,是DaoAuthenticationProvier與特定使用者資訊來源進行解耦的地方,UserDetailsServiceUserDetailsUserDetailsManager所構成;UserDetailsUserDetailsManager各司其責,一個是對基本使用者資訊進行封裝,一個是對基本使用者資訊進行管理;

特別注意,UserDetailsServiceUserDetails以及UserDetailsManager都是可被使用者自定義的擴充套件點,我們可以繼承這些介面提供自己的讀取使用者來源和管理使用者的方法,比如我們可以自己實現一個 與特定 ORM 框架,比如 Mybatis 或者 Hibernate,相關的UserDetailsService``和UserDetailsManager

時序圖

Spring Security系列之認證過程(六)

文章來源

相關文章