资讯专栏INFORMATION COLUMN

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

zzir / 2716人阅读

摘要:通过上面我们知道对于表单登录的认证请求是交给了处理的,那么具体的认证流程如下从上图可知,继承于抽象类。中维护这一个对象列表,通过遍历判断并且最后选择对象来完成最后的认证。发布一个登录事件。

概要

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

过滤器链

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

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

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

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

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

具体认证是:

进入doFilter方法,判断是否要认证,如果需要认证则进入attemptAuthentication方法,如果不需要直接结束

attemptAuthentication方法中根据username跟password构造一个UsernamePasswordAuthenticationToken对象(此时的token是未认证的),并且将它交给ProviderManger来完成认证。

ProviderManger中维护这一个AuthenticationProvider对象列表,通过遍历判断并且最后选择DaoAuthenticationProvider对象来完成最后的认证。

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主要分析就到这。我们可以从源码中知道,当请求进入该过滤器中具体的流程是

判断该请求是否要被认证

调用attemptAuthentication方法开始认证,由于是抽象方法具体认证逻辑给子类

如果登录成功,则将认证结果Authentication对象根据session策略写入session中,将认证结果写入到SecurityContextHolder,如果开启了记住我功能,则根据记住我功能,生成token并且写入cookie中,最后调用一个successHandler对象的方法,这个对象可以是我们配置注入的,用于处理我们的自定义登录成功的一些逻辑(比如记录登录成功日志等等)。

如果登录失败,则清空SecurityContextHolder中的信息,并且调用我们自己注入的failureHandler对象,处理我们自己的登录失败逻辑。

UsernamePasswordAuthenticationFilter

从上面分析我们可以知道,UsernamePasswordAuthenticationFilter是继承于AbstractAuthenticationProcessingFilter,并且实现它的attemptAuthentication方法,来实现认证具体的逻辑实现。接下来,我们通过阅读UsernamePasswordAuthenticationFilter的源码来解读,它是如何完成认证的。 由于这里会涉及UsernamePasswordAuthenticationToken对象构造,所以我们先看看UsernamePasswordAuthenticationToken的源码

1、UsernamePasswordAuthenticationToken
// 继承至AbstractAuthenticationToken 
// AbstractAuthenticationToken主要定义一下在SpringSecurity中toke需要存在一些必须信息
// 例如权限集合  Collection 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 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 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"));
        }
    }
总结

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74129.html

相关文章

  • SpringSecurity01(springSecurity执行流程02)

    摘要:里面配置的过滤器链当用户使用表单请求时进入返回一个的实例一般是从数据库中查询出来的实例然后直接到最后一个如果有错则抛错给前面一个进行抛错如果没有错则放行可以访问对应的资源上面是总的执行流程下面单独说一下的认证流程这个图应该都看得懂和里面的配 showImg(https://segmentfault.com/img/bVbvO0O?w=1258&h=261);web.xml里面配置的过滤...

    Dr_Noooo 评论0 收藏0
  • SpringSecurity系列01】初识SpringSecurity

    摘要:什么是是一个能够为基于的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它来自于,那么它与整合开发有着天然的优势,目前与对应的开源框架还有。通常大家在做一个后台管理的系统的时候,应该采用判断用户是否登录。 ​ 什么是SpringSecurity ? ​ Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全...

    elva 评论0 收藏0
  • 前后端分离项目 — 基于SpringSecurity OAuth2.0用户认证

    摘要:前言现在的好多项目都是基于移动端以及前后端分离的项目,之前基于的前后端放到一起的项目已经慢慢失宠并淡出我们视线,尤其是当基于的微服务架构以及单页面应用流行起来后,情况更甚。使用生成是什么请自行百度。 1、前言 现在的好多项目都是基于APP移动端以及前后端分离的项目,之前基于Session的前后端放到一起的项目已经慢慢失宠并淡出我们视线,尤其是当基于SpringCloud的微服务架构以及...

    QLQ 评论0 收藏0
  • springSecurity02(mybatis+springmvc+spring) 01

    摘要:建立一个模块继承上一个模块然后添加依赖解决打包时找不到文件建立数据源文件数据库连接相关修改配置数据源和整合,以及事务管理自动扫描扫描时跳过注解的类控制器扫描配置文件这里指向的是 1.建立一个模块继承上一个模块然后添加依赖 junit junit 4.11 test ...

    FrancisSoung 评论0 收藏0
  • SpringSecurity01(使用传统的xml方式开发,且不连接数据库)

    摘要:创建一个工程在里面添加依赖,依赖不要随便改我改了出错了好几次都找不到原因可以轻松的将对象转换成对象和文档同样也可以将转换成对象和配置 1.创建一个web工程2.在pom里面添加依赖,依赖不要随便改,我改了出错了好几次都找不到原因 UTF-8 1.7 1.7 2.5.0 1.2 3.0-alpha-1 ...

    Gilbertat 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<