Анализ исходного кода Spring Security 1: процесс аутентификации Spring Security

задняя часть Spring Безопасность API

Spring Security — это инфраструктура безопасности, которая может предоставлять решения для декларативного контроля доступа к безопасности для систем корпоративных приложений на основе Spring. Он предоставляет набор bean-компонентов, которые можно настроить в контексте приложения Spring, в полной мере используя функции Spring IoC, DI (инверсия управления, DI: внедрение зависимостей) и AOP (аспектно-ориентированное программирование) для предоставления прикладным системам декларативного безопасного доступа. возможности управления уменьшают усилия по написанию большого количества повторяющегося кода для средств управления безопасностью корпоративной системы.

Диаграмма классов

Чтобы облегчить понимание процесса аутентификации Spring Security, специально нарисована следующая диаграмма классов, включая соответствующие основные классы аутентификации.

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/1/3/160b9fd026c8e4fb~tplv-t2oaga2asx-image.image
https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/1/3/160b9fd026c8e4fb~tplv-t2oaga2asx-image.image

Обзор

основной валидатор

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

ProviderManagerдаAuthenticationManagerКласс реализации обеспечивает базовую логику и процесс реализации аутентификации;

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. Пройдите все провайдеры, а затем по очереди выполните метод проверки провайдера.
    • Если провайдер успешно проверен, он выйдет из цикла, и последующая проверка не будет выполняться;
    • Если проверка прошла успешно, возвращенный результат и объект проверки подлинности будут дополнительно инкапсулированы как токен проверки подлинности; Например, UsernamePasswordAuthenticationToken, RememberMeAuthenticationToken и т. д. Эти токены аутентификации также наследуются от объекта Authentication;
  2. Если ни один из провайдеров № 1 не прошел аутентификацию успешно, попробуйте пройти аутентификацию с помощью родительского диспетчера аутентификации;
  3. Необходимо ли стирать конфиденциальную информацию, такую ​​как пароли;

AuthenticationProvider

ProviderManagerпройти черезAuthenticationProviderРасширяет способ предоставления дополнительной проверки; а такжеAuthenticationProviderЭто сам интерфейс, и мы можем увидеть его класс реализации на диаграмме классов.AbstractUserDetailsAuthenticationProviderиAbstractUserDetailsAuthenticationProviderподклассDaoAuthenticationProvider.DaoAuthenticationProviderдаSpring Securityодин из основныхProvider, предоставляет основные методы и запись для всех баз данных.

DaoAuthenticationProvider

DaoAuthenticationProviderВ основном делать следующее

  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

AbstractUserDetailsAuthenticationProviderзаDaoAuthenticationProviderПредоставляет основные методы аутентификации;

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;
}

Вызов метода loadUserByUsername через имя пользователя username возвращает объект интерфейса UserDetails (соответствующийAbstractUserDetailsAuthenticationProvider3-этапный метод проверки);

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();
}

ВеснаUserDetailsServiceКласс реализации org.springframework.security.core.userdetails.jdbc предоставляется по умолчанию.JdbcDaoImpl

JdbcUserDetailsManager

Класс реализации в основном основан наJDBCСпособы добавления, удаления, проверки и изменения Пользователя

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

Класс реализации в основном основан на内存Способы добавления, удаления, проверки и изменения Пользователя `открытый класс InMemoryUserDetailsManager реализует UserDetailsManager { защищенный окончательный журнал регистрации = LogFactory.getLog(getClass()); # 1. Магазин с картой private final Map users = new HashMap();

private AuthenticationManager authenticationManager;

public InMemoryUserDetailsManager() {
}

public InMemoryUserDetailsManager(Collection<UserDetails> users) {
	for (UserDetails user : users) {
		createUser(user);
	}
}`

Суммировать

UserDetailsServiceИнтерфейс действует как мост иDaoAuthenticationProvierгде отделение от конкретных источников информации о пользователях,UserDetailsServiceЗависит отUserDetailsиUserDetailsManageр состоит из;UserDetailsиUserDetailsManagerУ каждого свои обязанности: один — инкапсулировать основную информацию о пользователе, другой — управлять основной информацией о пользователе;

特别注意,UserDetailsService,UserDetailsа такжеUserDetailsManagerВсе они являются определяемыми пользователем точками расширения. Мы можем наследовать эти интерфейсы, чтобы предоставлять собственные методы чтения пользовательских источников и управления пользователями. Например, мы можем реализовать структуру ORM, связанную с определенной структурой ORM, такой как Mybatis или Hibernate.UserDetailsServiceиUserDetailsManager;

Временная диаграмма

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/1/3/160b9fd026a57728~tplv-t2oaga2asx-image.image
https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/1/3/160b9fd026a57728~tplv-t2oaga2asx-image.image