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

栏目: 后端 · 前端 · 发布时间: 5年前

内容简介:前面一节,通过简单配置即可实现SpringSecurity表单认证功能,而今天这一节将通过阅读源码的形式来学习SpringSecurity是如何实现这些功能, 前方高能预警,前面我说过SpringSecurity是基于过滤器链的形式,那么我解析将会介绍一下具体有哪些过滤器。由于过滤器链中的过滤器实在太多,我没有一一列举,调了几个比较重要的介绍一下。

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

过滤器链

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

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

由于过滤器链中的过滤器实在太多,我没有一一列举,调了几个比较重要的介绍一下。

通过上面我们知道SpringSecurity对于表单登录的认证请求是交给了UsernamePasswordAuthenticationFilter处理的,那么具体的认证流程如下:

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

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

具体认证是:

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

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

AbstractAuthenticationProcessingFilter

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

现在我们分析一下 它里面比较重要的方法

1、doFilter

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

2、successfulAuthentication

登录成功处理器

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

3、unsuccessfulAuthentication

登录失败处理器

protected void unsuccessfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException failed)
			throws IOException, ServletException {
    //1、登录失败,将SecurityContextHolder中的信息清空
		SecurityContextHolder.clearContext();
    //2、关于记住我功能的登录失败处理
		rememberMeServices.loginFail(request, response);
    //3、调用我们自己定义的登录失败处理器,这里可以扩展记录登录失败的日志。
		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 的源码

1、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方法了。

2、attemptAuthentication

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

ProviderManager

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

1、authenticate

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

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

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

	}
复制代码

AbstractUserDetailsAuthenticationProvider

1、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 {
    
    // 1、获取usernmae
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();

    // 2、尝试去缓存中获取UserDetails对象
		UserDetails user = this.userCache.getUserFromCache(username);
    // 3、如果为空,则代表当前对象没有缓存。
		if (user == null) {
			cacheWasUsed = false;
			try {
        //4、调用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 {
      // 5、前置检查  判断当前用户是否锁定,禁用等等
			preAuthenticationChecks.check(user);
      // 6、其他的检查,在DaoAuthenticationProvider是检查密码是否一致
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
		
		}

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

	 
		// 8、登录成功通过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对象。

1、retrieveUser

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

2、additionalAuthenticationChecks

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

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

    // 3、调用passwordEncoder的matche方法 判断密码是否一致
		if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			logger.debug("Authentication failed: password does not match stored value");

      // 4、如果不一致 则抛出异常。
			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
	}
复制代码

总结

至此,整认证流程已经分析完毕,大家如果有什么不懂可以关注我的公众号一起讨论。

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

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

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Out of their Minds

Out of their Minds

Dennis Shasha、Cathy Lazere / Springer / 1998-07-02 / USD 16.00

This best-selling book is now available in an inexpensive softcover format. Imagine living during the Renaissance and being able to interview that eras greatest scientists about their inspirations, di......一起来看看 《Out of their Minds》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具