Spring Security源碼分析一:Spring Security認證過程

Spring Security是一個可以爲基於Spring的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組能夠在Spring應用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面編程)功能,爲應用系統提供聲明式的安全訪問控制功能,減小了爲企業系統安全控制編寫大量重複代碼的工做。java

類圖

爲了方便理解Spring Security認證流程,特地畫了以下的類圖,包含相關的核心認證類 算法

https://user-gold-cdn.xitu.io/2018/1/3/160b9fd026c8e4fb?w=1620&h=1622&f=png&s=53539
https://user-gold-cdn.xitu.io/2018/1/3/160b9fd026c8e4fb?w=1620&h=1622&f=png&s=53539

概述

核心驗證器spring

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對象中的主要方法bash

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的實現類,提供了基本認證明現邏輯和流程;app

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的子類DaoAuthenticationProviderDaoAuthenticationProviderSpring Security中一個核心的Provider,對全部的數據庫提供了基本方法和入口。框架

DaoAuthenticationProvider

DaoAuthenticationProvider主要作了如下事情ide

  1. 對用戶身份盡心加密操做;
    #1.可直接返回BCryptPasswordEncoder,也能夠本身實現該接口使用本身的加密算法核心方法String encode(CharSequence rawPassword);和boolean matches(CharSequence rawPassword, String encodedPassword);
    複製代碼

private PasswordEncoder passwordEncoder;

2. 實現了 `AbstractUserDetailsAuthenticationProvider` 兩個抽象方法,
	1. 獲取用戶信息的擴展點
	```java
protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		UserDetails loadedUser;

		try {
			loadedUser = this.getUserDetailsService().loadUserByUsername(username);
		}
複製代碼
主要是經過注入`UserDetailsService`接口對象,並調用其接口方法 `loadUserByUsername(String username)` 獲取獲得相關的用戶信息。`UserDetailsService`接口很是重要。
2. 實現 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 對象並返回;該對象封裝了用戶的身份信息,以及相應的權限信息,相關源碼以下,
	```java
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,相關的UserDetailsServiceUserDetailsManager

時序圖

https://user-gold-cdn.xitu.io/2018/1/3/160b9fd026a57728?w=1829&h=1122&f=png&s=74065
https://user-gold-cdn.xitu.io/2018/1/3/160b9fd026a57728?w=1829&h=1122&f=png&s=74065
相關文章
相關標籤/搜索