【SpringSecurity系列02】SpringSecurity 表单认证逻辑源码解读

概要

前面一节,经过简单配置便可实现SpringSecurity表单认证功能,而今天这一节将经过阅读源码的形式来学习SpringSecurity是如何实现这些功能, 前方高能预警,本篇分析源码篇幅较长java

过滤器链

前面我说过SpringSecurity是基于过滤器链的形式,那么我解析将会介绍一下具体有哪些过滤器。数据库

Filter Class 介绍
SecurityContextPersistenceFilter 判断当前用户是否登陆
CrsfFilter 用于防止csrf攻击
LogoutFilter 处理注销请求
UsernamePasswordAuthenticationFilter 处理表单登陆的请求(也是咱们今天的主角)
BasicAuthenticationFilter 处理http basic认证的请求

因为过滤器链中的过滤器实在太多,我没有一一列举,调了几个比较重要的介绍一下。设计模式

经过上面咱们知道SpringSecurity对于表单登陆的认证请求是交给了UsernamePasswordAuthenticationFilter处理的,那么具体的认证流程以下:缓存

Spring Security ç™

从上图可知,UsernamePasswordAuthenticationFilter继承于抽象类AbstractAuthenticationProcessingFiltercookie

具体认证是:session

  1. 进入doFilter方法,判断是否要认证,若是须要认证则进入attemptAuthentication方法,若是不须要直接结束
  2. attemptAuthentication方法中根据username跟password构造一个UsernamePasswordAuthenticationToken对象(此时的token是未认证的),而且将它交给ProviderManger来完成认证。
  3. ProviderManger中维护这一个AuthenticationProvider对象列表,经过遍历判断而且最后选择DaoAuthenticationProvider对象来完成最后的认证。
  4. DaoAuthenticationProvider根据ProviderManger传来的token取出username,而且调用咱们写的UserDetailsService的loadUserByUsername方法从数据库中读取用户信息,而后对比用户密码,若是认证经过,则返回用户信息也是就是UserDetails对象,在从新构造UsernamePasswordAuthenticationToken(此时的token是 已经认证经过了的)。

接下来咱们将经过源码来分析具体的整个认证流程。app

AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter 是一个抽象类。全部的认证认证请求的过滤器都会继承于它,它主要将一些公共的功能实现,而具体的验证逻辑交给子类实现,有点相似于父类设置好认证流程,子类负责具体的认证逻辑,这样跟设计模式的模板方法模式有点类似。ide

如今咱们分析一下 它里面比较重要的方法函数

一、doFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
		// 省略不相干代码。。。
    // 一、判断当前请求是否要认证
		if (!requiresAuthentication(request, response)) {
      // 不须要直接走下一个过滤器
			chain.doFilter(request, response);
			return;
		}
		try {
      // 二、开始请求认证,attemptAuthentication具体实现给子类,若是认证成功返回一个认证经过的Authenticaion对象
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				return;
			}
      // 三、登陆成功 将认证成功的用户信息放入session SessionAuthenticationStrategy接口,用于扩展
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
      //2.一、发生异常,登陆失败,进入登陆失败handler回调
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		catch (AuthenticationException failed) {
      //2.一、发生异常,登陆失败,进入登陆失败处理器
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		// 3.一、登陆成功,进入登陆成功处理器。
		successfulAuthentication(request, response, chain, authResult);
	}
复制代码

二、successfulAuthentication

登陆成功处理器post

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
    //一、登陆成功 将认证成功的Authentication对象存入SecurityContextHolder中
    // SecurityContextHolder本质是一个ThreadLocal
		SecurityContextHolder.getContext().setAuthentication(authResult);
    //二、若是开启了记住我功能,将调用rememberMeServices的loginSuccess 将生成一个token
  	// 将token放入cookie中这样 下次就不用登陆就能够认证。具体关于记住我rememberMeServices的相关分析我 们下面几篇文章会深刻分析的。
		rememberMeServices.loginSuccess(request, response, authResult);
		// Fire event
    //三、发布一个登陆事件。
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}
    //四、调用咱们本身定义的登陆成功处理器,这样也是咱们扩展得知登陆成功的一个扩展点。
		successHandler.onAuthenticationSuccess(request, response, authResult);
	}
复制代码

三、unsuccessfulAuthentication

登陆失败处理器

protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
    //一、登陆失败,将SecurityContextHolder中的信息清空
		SecurityContextHolder.clearContext();
    //二、关于记住我功能的登陆失败处理
		rememberMeServices.loginFail(request, response);
    //三、调用咱们本身定义的登陆失败处理器,这里能够扩展记录登陆失败的日志。
		failureHandler.onAuthenticationFailure(request, response, failed);
	}
复制代码

关于AbstractAuthenticationProcessingFilter主要分析就到这。咱们能够从源码中知道,当请求进入该过滤器中具体的流程是

  1. 判断该请求是否要被认证
  2. 调用attemptAuthentication方法开始认证,因为是抽象方法具体认证逻辑给子类
  3. 若是登陆成功,则将认证结果Authentication对象根据session策略写入session中,将认证结果写入到SecurityContextHolder,若是开启了记住我功能,则根据记住我功能,生成token而且写入cookie中,最后调用一个successHandler对象的方法,这个对象能够是咱们配置注入的,用于处理咱们的自定义登陆成功的一些逻辑(好比记录登陆成功日志等等)。
  4. 若是登陆失败,则清空SecurityContextHolder中的信息,而且调用咱们本身注入的failureHandler对象,处理咱们本身的登陆失败逻辑。

UsernamePasswordAuthenticationFilter

从上面分析咱们能够知道,UsernamePasswordAuthenticationFilter是继承于AbstractAuthenticationProcessingFilter,而且实现它的attemptAuthentication方法,来实现认证具体的逻辑实现。接下来,咱们经过阅读UsernamePasswordAuthenticationFilter的源码来解读,它是如何完成认证的。 因为这里会涉及UsernamePasswordAuthenticationToken对象构造,因此咱们先看看UsernamePasswordAuthenticationToken的源码

一、UsernamePasswordAuthenticationToken

// 继承至AbstractAuthenticationToken 
// AbstractAuthenticationToken主要定义一下在SpringSecurity中toke须要存在一些必须信息
// 例如权限集合 Collection<GrantedAuthority> authorities; 是否定证经过boolean authenticated = false;认证经过的用户信息Object details;
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
 
  // 未登陆状况下 存的是用户名 登陆成功状况下存的是UserDetails对象
	private final Object principal;
  // 密码
	private Object credentials;

  /** * 构造函数,用户没有登陆的状况下,此时的authenticated是false,表明还没有认证 */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

 /** * 构造函数,用户登陆成功的状况下,多了一个参数 是用户的权限集合,此时的authenticated是true,表明认证成功 */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true); // must use super, as we override
	}
}
复制代码

接下来咱们就能够分析attemptAuthentication方法了。

二、attemptAuthentication

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
     // 一、判断是否是post请求,若是不是则抛出AuthenticationServiceException异常,注意这里抛出的异常都在AbstractAuthenticationProcessingFilter#doFilter方法中捕获,捕获以后会进入登陆失败的逻辑。
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
    // 二、从request中拿用户名跟密码
		String username = obtainUsername(request);
		String password = obtainPassword(request);
		// 三、非空处理,防止NPE异常
		if (username == null) {
			username = "";
		}
		if (password == null) {
			password = "";
		}
    // 四、除去空格
		username = username.trim();
    // 五、根据username跟password构造出一个UsernamePasswordAuthenticationToken对象 从上文分析可知道,此时的token是未认证的。
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
    // 六、配置一下其余信息 ip 等等
		setDetails(request, authRequest);
   // 七、调用ProviderManger的authenticate的方法进行具体认证逻辑
		return this.getAuthenticationManager().authenticate(authRequest);
	}
复制代码

ProviderManager

维护一个AuthenticationProvider列表,进行认证逻辑验证

一、authenticate

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    // 一、拿到token的类型。
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		Authentication result = null;
   // 二、遍历AuthenticationProvider列表
		for (AuthenticationProvider provider : getProviders()) {
      // 三、AuthenticationProvider不支持当前token类型,则直接跳过
			if (!provider.supports(toTest)) {
				continue;
			}

			try {
        // 四、若是Provider支持当前token,则交给Provider完成认证。
				result = provider.authenticate(authentication);
     
			}
			catch (AccountStatusException e) {

				throw e;
			}
			catch (InternalAuthenticationServiceException e) {
				throw e;
			}
			catch (AuthenticationException e) {
				lastException = e;
			}
		}
    // 五、登陆成功 返回登陆成功的token
		if (result != null) {
			eventPublisher.publishAuthenticationSuccess(result);
			return result;
		}

	}
复制代码

AbstractUserDetailsAuthenticationProvider

一、authenticate

AbstractUserDetailsAuthenticationProvider实现了AuthenticationProvider接口,而且实现了部分方法,DaoAuthenticationProvider继承于AbstractUserDetailsAuthenticationProvider类,因此咱们先来看看AbstractUserDetailsAuthenticationProvider的实现。

public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {

  // 国际化处理
	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();


	/** * 对token一些检查,具体检查逻辑交给子类实现,抽象方法 */
	protected abstract void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;


  /** * 认证逻辑的实现,调用抽象方法retrieveUser根据username获取UserDetails对象 */
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    
    // 一、获取usernmae
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();

    // 二、尝试去缓存中获取UserDetails对象
		UserDetails user = this.userCache.getUserFromCache(username);
    // 三、若是为空,则表明当前对象没有缓存。
		if (user == null) {
			cacheWasUsed = false;
			try {
        //四、调用retrieveUser去获取UserDetail对象,为何这个方法是抽象方法你们很容易知道,若是UserDetail信息存在关系数据库 则能够重写该方法而且去关系数据库获取用户信息,若是UserDetail信息存在其余地方,能够重写该方法用其余的方法去获取用户信息,这样丝绝不影响整个认证流程,方便扩展。
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
      catch (UsernameNotFoundException notFound) {
				
				// 捕获异常 日志处理 而且往上抛出,登陆失败。
				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException(messages.getMessage(
							"AbstractUserDetailsAuthenticationProvider.badCredentials",
							"Bad credentials"));
				}
				else {
					throw notFound;
				}
			}
		}

		try {
      // 五、前置检查 判断当前用户是否锁定,禁用等等
			preAuthenticationChecks.check(user);
      // 六、其余的检查,在DaoAuthenticationProvider是检查密码是否一致
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
		
		}

    // 七、后置检查,判断密码是否过时
		postAuthenticationChecks.check(user);

	 
		// 八、登陆成功经过UserDetail对象从新构造一个认证经过的Token对象
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

	
	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
		// 调用第二个构造方法,构造一个认证经过的Token对象
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
				principal, authentication.getCredentials(),
				authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());

		return result;
	}

}
复制代码

接下来咱们具体看看retrieveUser的实现,没看源码你们应该也能够知道,retrieveUser方法应该是调用UserDetailsService去数据库查询是否有该用户,以及用户的密码是否一致。

DaoAuthenticationProvider

DaoAuthenticationProvider 主要是经过UserDetailService来获取UserDetail对象。

一、retrieveUser

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		try {
      // 一、调用UserDetailsService接口的loadUserByUsername方法获取UserDeail对象
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
       // 二、若是loadedUser为null 表明当前用户不存在,抛出异常 登陆失败。
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
      // 三、返回查询的结果
			return loadedUser;
		}
	}
复制代码

二、additionalAuthenticationChecks

protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    // 一、若是密码为空,则抛出异常、
		if (authentication.getCredentials() == null) {
			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}

    // 二、获取用户输入的密码
		String presentedPassword = authentication.getCredentials().toString();

    // 三、调用passwordEncoder的matche方法 判断密码是否一致
		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"));
		}
	}
复制代码

总结

至此,整认证流程已经分析完毕,你们若是有什么不懂能够关注个人公众号一块儿讨论。

学习是一个漫长的过程,学习源码可能会很困难可是只要努力必定就会有获取,你们一致共勉。

程序咖啡厅
相关文章
相关标签/搜索