Spring Security源码(二) 登录认证流程

一 引言

通过上篇文章分析我们知道了通过FilterChainProxy我们管理许多的过滤器链,其中UsernamePasswordAuthenticationFilter默认拦截“/login”登录请求,处理表单提交的登录认证,将请求中的认证信息包括username,password等封装成UsernamePasswordAuthenticationToken,然后调用AuthenticationManager的认证方法进行认证,其局具体流程如下:
在这里插入图片描述

  • 请求过来会被过滤器链中的UsernamePasswordAuthenticationFilter拦截到,请求中的用户名和密码被封装成UsernamePasswordAuthenticationToken(Authentication的实现类)
  • 过滤器将UsernamePasswordAuthenticationToken提交给认证管理器(AuthenticationManager)进行认证
  • AuthenticationManager委托AuthenticationProvider(DaoAuthenticationProvider)进行认证,AuthenticationProvider通过调用UserDetailsService获取到数据库或缓存中存储的用户信息(UserDetails),然后调用passwordEncoder密码编码器对UsernamePasswordAuthenticationToken中的密码和UserDetails中的密码进行比较
  • AuthenticationProvider认证成功后封装Authentication并设置好用户的信息(用户名,密码,权限等)返回
  • Authentication被返回到UsernamePasswordAuthenticationFilter,通过调用SecurityContextHolder工具把Authentication封装成SecurityContext中存储起来。然后UsernamePasswordAuthenticationFilter调用AuthenticationSuccessHandler.onAuthenticationSuccess做认证成功后续处理操作
  • 最后SecurityContextPersistenceFilter通过SecurityContextHolder.getContext()获取到SecurityContext对象然后调用SecurityContextRepositorySecurityContext存储起来,然后调用SecurityContextHolder.clearContext方法清理SecurityContext。
    注意:SecurityContext是一个和当前线程绑定的工具,在代码的任何地方都可以通过SecurityContextHolder.getContext()获取到登陆信息

二 ProviderManager分析

在Spring Security中,所有的认证服务,均通过ProviderManager认证管理中心进行认证。通过分析ProviderManager,可以理解Spring Security认证的细节。也是整个流程中最核心的环节之一。

2.1 ProviderManager 的类结构

ProviderManager 继承实现AuthenticationManager接口。

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {
    
    
		.....
		}

AuthenticationManager 接口只有一个方法:Authentication authenticate(Authentication authentication)
throws AuthenticationException,该方法传入一个Authentication 的实例,认证成功返回的是 Authentication 的一个具体实例。

public interface AuthenticationManager {
    
    
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
}

Authentication 接口描述:

public interface Authentication extends Principal, Serializable {
    
    
// 返回当前用户的权限列表
	Collection<? extends GrantedAuthority> getAuthorities();
// 获取当前认证的密码
	Object getCredentials();
//存储有关身份验证请求的其他详细信息。 这些可能是IP地址,证书序列号等
	Object getDetails();
//获取当前用户名
	Object getPrincipal();
// 获取是否已经认证
	boolean isAuthenticated();
// 设置是否通过认证
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

2.2 AuthenticationManager 接口实现类

AuthenticationManager 部分实现类,在当前的用户名密码认证的过程中,主要的核心类是:ProviderManager
在这里插入图片描述

2.3 Authentication 接口实现类

列举出Authentication 部分实现核心类,在用户名密码认证过程中,主要使用的核心类是:UsernamePasswordAuthenticationToken
在这里插入图片描述

2.3.ProviderManager 类的成员变量和代码

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {
    
    
	// 认证提供者列表。认证提供者主要根据传入的不同的 Authentication 决定使用哪种认证方式
	// providers 集合在项目启动的时候会被初始化进去。主要初始化的提供者有两个:
	// AnonymousAuthenticationProvider  和 DaoAuthenticationProvider
	private List<AuthenticationProvider> providers = Collections.emptyList();
	
	// 上一级认证管理者,一般情况下用不到
	private AuthenticationManager parent;
	
	// 构造函数初始化
	public ProviderManager(List<AuthenticationProvider> providers,
			AuthenticationManager parent) {
    
    
		Assert.notNull(providers, "providers list cannot be null");
		this.providers = providers;
		this.parent = parent;
		checkState();
	}
	
	// 通过传入不同的 authentication ,决定使用哪种认证方式去认证当前用户
	// 每个authentication 对应着一个认证策略
	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
    
    
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		Authentication result = null;
		boolean debug = logger.isDebugEnabled();
		// 遍历当前集合,找到和authentication 匹配的认证方式。
		for (AuthenticationProvider provider : getProviders()) {
    
    
		 // 判断是否支持当前认证方式
			if (!provider.supports(toTest)) {
    
    
				continue;
			}
			if (debug) {
    
    
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}
			try {
    
    
				// 调用当前认证方式的具体实现类
				result = provider.authenticate(authentication);
				if (result != null) {
    
    
					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 {
    
    
				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;
			}
		}
		if (result != null) {
    
    
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
    
    
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				// 对当前认证通过的authentication 信息进行脱敏处理
				((CredentialsContainer) result).eraseCredentials();
			}
			eventPublisher.publishAuthenticationSuccess(result);
			//返回当前的认证结果
			return result;
		}
		if (lastException == null) {
    
    
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] {
    
     toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}
		prepareException(lastException, authentication);
		throw lastException;
	}
}

ProviderManager 认证管理中心,相当于一个策略模板,根据传入的认证信息,选择不同的认证处理器进行处理

2.4.AuthenticationProvider 接口详解

针对不同认证信息,AuthenticationProvider 提供了不同的对应策略。

public interface AuthenticationProvider {
    
    
 // 具体的认证细节
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
	// 判断是否支持当前认证
	boolean supports(Class<?> authentication);
}

AuthenticationProvider 核心实现类
在这里插入图片描述

在密码认证的过程中,其核心的认证类是:AbstractUserDetailsAuthenticationProviderDaoAuthenticationProvider。这里使用了设计模式中的模板方法进行实现。

三 SpringSecurity中认证源码分析

SecurityContextPersistenceFilter

这个filter是整个filter链的入口和出口,请求开始会从SecurityContextRepository中 获取SecurityContext对象并设置给SecurityContextHolder。在请求完成后将
SecurityContextHolder持有的SecurityContext再保存到配置好的
DecurityContextRepository中,同时清除SecurityContextHolder中的SecurityContext

总结一下:SecurityContextPersistenceFilter它的作用就是请求来的时候将包含了认证授权信息的SecurityContext对象从SecurityContextRepository中取出交给SecurityContextHolder工具类,方便我们通过SecurityContextHolder获取SecurityContext从而获取到认证授权信息,请求走的时候又把SecurityContextHolder清空,源码如下:


public class SecurityContextPersistenceFilter extends GenericFilterBean {
    
    
  ...省略...
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    
    
  ...省略部分代码...
  HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
  			response);
  //从SecurityContextRepository获取到SecurityContext 
  	SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

  	try {
    
    
  	 //把 securityContext设置到SecurityContextHolder,如果没认证通过,这个SecurtyContext就是空的
  		SecurityContextHolder.setContext(contextBeforeChainExecution);
  		//调用后面的filter,比如掉用usernamepasswordAuthenticationFilter实现认证
  		chain.doFilter(holder.getRequest(), holder.getResponse());

  	}
  	finally {
    
    
  		//如果认证通过了,这里可以从SecurityContextHolder.getContext();中获取到SecurityContext
  		SecurityContext contextAfterChainExecution = SecurityContextHolder
  				.getContext();
  		// Crucial removal of SecurityContextHolder contents - do this before anything
  		// else.
  		 //删除SecurityContextHolder中的SecurityContext 
  		SecurityContextHolder.clearContext();
  		//把SecurityContext 存储到SecurityContextRepository
  		repo.saveContext(contextAfterChainExecution, holder.getRequest(),
  				holder.getResponse());
  		request.removeAttribute(FILTER_APPLIED);

  		if (debug) {
    
    
  			logger.debug("SecurityContextHolder now cleared, as request processing completed");
  		}
  	}
...省略...

UsernamePasswordAuthenticationFilter

它的重用是,拦截“/login”登录请求,处理表单提交的登录认证,将请求中的认证信息包括username,password等封装成UsernamePasswordAuthenticationToken,然后调用
AuthenticationManager的认证方法进行认证。

public class UsernamePasswordAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter {
    
    
	// ~ Static fields/initializers
	// =====================================================================================
	//从登录请求中获取参数:username,password的名字
	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
	//默认支持POST登录
	private boolean postOnly = true;
	//默认拦截/login请求,Post方式
	public UsernamePasswordAuthenticationFilter() {
    
    
		super(new AntPathRequestMatcher("/login", "POST"));
	}

	// ~ Methods
	// ========================================================================================================

	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
    
    
			//判断请求是否是POST
		if (postOnly && !request.getMethod().equals("POST")) {
    
    
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
		//获取到用户名和密码
		String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
    
    
			username = "";
		}

		if (password == null) {
    
    
			password = "";
		}

		username = username.trim();
		//用户名和密码封装Token
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
		//设置details属性
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		//调用AuthenticationManager().authenticate进行认证,参数就是Token对象
		return this.getAuthenticationManager().authenticate(authRequest);
	}

AuthenticationManager

请求通过UsernamePasswordAuthenticationFilter调用AuthenticationManager,默认走的实现类是ProviderManager它会找到能支持当前认证的AuthenticationProvider实现类调用器authenticate方法执行认证,认证成功后会清除密码,然后抛出AuthenticationSuccessEvent事件

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {
    
    
		...省略...
		//这里authentication 是封装了登录请求的认证参数,
		//即:UsernamePasswordAuthenticationFilter传入的Token对象
	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
    
    
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();
		//找到所有的AuthenticationProvider ,选择合适的进行认证
		for (AuthenticationProvider provider : getProviders()) {
    
    
			//是否支持当前认证
			if (!provider.supports(toTest)) {
    
    
				continue;
			}

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

			try {
    
    
				//调用provider执行认证
				result = provider.authenticate(authentication);

				if (result != null) {
    
    
					copyDetails(authentication, result);
					break;
				}
			}
				...省略...
		}
		...省略...
		//result就是Authentication ,使用的实现类依然是UsernamepasswordAuthenticationToken,
		//封装了认证成功后的用户的认证信息和授权信息
		if (result != null) {
    
    
			if (eraseCredentialsAfterAuthentication
				&& (result instanceof CredentialsContainer)) {
    
    
			// Authentication is complete. Remove credentials and other secret data
			// from authentication
			//这里在擦除登录密码
			((CredentialsContainer) result).eraseCredentials();
		}

		// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
		// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
		if (parentResult == null) {
    
    
			//发布事件
			eventPublisher.publishAuthenticationSuccess(result);
		}
		return result;
	}

DaoAuthenticationProvider

请求到达AuthenticationProvider,默认实现是DaoAuthenticationProvider,它的作用是根据传入的Token中的username调用UserDetailService加载数据库中的认证授权信息(UserDetails),然后使用PasswordEncoder对比用户登录密码是否正确

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    
    
		//密码编码器
		private PasswordEncoder passwordEncoder;
		//UserDetailsService ,根据用户名加载UserDetails对象,从数据库加载的认证授权信息
		private UserDetailsService userDetailsService;
		//认证检查方法
		protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
    
    
		if (authentication.getCredentials() == null) {
    
    
			logger.debug("Authentication failed: no credentials provided");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
		//获取密码
		String presentedPassword = authentication.getCredentials().toString();
		//通过passwordEncoder比较密码,presentedPassword是用户传入的密码,userDetails.getPassword()是从数据库加载到的密码
		//passwordEncoder编码器不一样比较密码的方式也不一样
		if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
    
    
			logger.debug("Authentication failed: password does not match stored value");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
	}

	//检索用户,参数为用户名和Token对象
	protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
    
    
		prepareTimingAttackProtection();
		try {
    
    
			//调用UserDetailsService的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);
		}
	}
	//创建认证成功的认证对象Authentication,使用的实现是UsernamepasswordAuthenticationToken,
	//封装了认证成功后的认证信息和授权信息,以及账户的状态等
	@Override
	protected Authentication createSuccessAuthentication(Object principal,
			Authentication authentication, UserDetails user) {
    
    
		boolean upgradeEncoding = this.userDetailsPasswordService != null
				&& this.passwordEncoder.upgradeEncoding(user.getPassword());
		if (upgradeEncoding) {
    
    
			String presentedPassword = authentication.getCredentials().toString();
			String newPassword = this.passwordEncoder.encode(presentedPassword);
			user = this.userDetailsPasswordService.updatePassword(user, newPassword);
		}
		return super.createSuccessAuthentication(principal, authentication, user);
	}
	...省略...

这里提供了三个方法

  • additionalAuthenticationChecks:通过passwordEncoder比对密码
  • retrieveUser:根据用户名调用UserDetailsService加载用户认证授权信息
  • createSuccessAuthentication:登录成功,创建认证对象Authentication
    然而你发现 DaoAuthenticationProvider 中并没有authenticate认证方法,真正的认证逻辑是通过父类AbstractUserDetailsAuthenticationProvider.authenticate方法完成的
public abstract class AbstractUserDetailsAuthenticationProvider implements
		AuthenticationProvider, InitializingBean, MessageSourceAware {
    
    
		//认证逻辑
		public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
    
    
			//得到传入的用户名
			String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();
				//从缓存中得到UserDetails
			boolean cacheWasUsed = true;
			UserDetails user = this.userCache.getUserFromCache(username);
			if (user == null) {
    
    
			cacheWasUsed = false;

			try {
    
    
				//检索用户,底层会调用UserDetailsService加载数据库中的UserDetails对象,保护认证信息和授权信息
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
    
    
				...省略...
			}

			try {
    
    
				//前置检查,主要检查账户是否锁定,账户是否过期等
				preAuthenticationChecks.check(user);
				//比对密码在这个方法里面比对的
				additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (AuthenticationException exception) {
    
    
			...省略...
			}
			//后置检查
			postAuthenticationChecks.check(user);
	
			if (!cacheWasUsed) {
    
    
				//设置UserDetails缓存
				this.userCache.putUserInCache(user);
			}
	
			Object principalToReturn = user;
	
			if (forcePrincipalAsString) {
    
    
				principalToReturn = user.getUsername();
			}
			//认证成功,创建Auhentication认证对象
			return createSuccessAuthentication(principalToReturn, authentication, user);
}

UsernamePasswordAuthenticationFilter

认证成功,请求会重新回到UsernamePasswordAuthenticationFilter,然后会通过其父类AbstractAuthenticationProcessingFilter.successfulAuthentication方法将认证对象封装成SecurityContext设置到SecurityContextHolder中

protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {
    
    

		if (logger.isDebugEnabled()) {
    
    
			logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
					+ authResult);
		}

		//认证成功,吧Authentication 设置到SecurityContextHolder
		SecurityContextHolder.getContext().setAuthentication(authResult);
		//处理记住我业务逻辑
		rememberMeServices.loginSuccess(request, response, authResult);

		// Fire event
		if (this.eventPublisher != null) {
    
    
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}
		//重定向登录成功地址
		successHandler.onAuthenticationSuccess(request, response, authResult);
	}

然后后续请求又会回到SecurityContextPersistenceFilter它就可以从SecurityContextHolder获取到SecurityContext持久到SecurityContextRepository(默认实现是HttpSessionSecurityContextRepository基于Session存储)

四 案列说明

在笔者基于springsecurity和jwt实现的单体项目token认证中笔者并没有使用默认的授权模式 而是对外提供一个接口,在接口中通过AuthManager来进行用户校验,校验成功后生成jwt
在这里插入图片描述

在这里插入图片描述
到此整个认证过程就完成了

猜你喜欢

转载自blog.csdn.net/Instanceztt/article/details/128117703