Spring Security源码解析一:UsernamePasswordAuthenticationFilter之登录流程

2022/1/19 14:50:50

本文主要是介绍Spring Security源码解析一:UsernamePasswordAuthenticationFilter之登录流程,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一.前言

spring security安全框架作为spring系列组件中的一个,被广泛的运用在各项目中,那么spring security在程序中的工作流程是个什么样的呢,它是如何进行一系列的鉴权和认证呢,下面让我们走进源码,从源码的角度来从头走一遍spring security的工作流程。

二.spring security核心结构

当一个外部请求进入到我们应用中的时候,首先会通过我们的应用过滤器链ApplicationFilterChain,我们将遍历该过滤器链中每一个Filter进行对应的处理,下面我们来看下ApplicationFiterChain一般情况下有哪些Filter

  
//ApplicationFilterChain遍历内部Fiter进行doFilter()处理
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
        if (this.pos < this.n) {
            ApplicationFilterConfig filterConfig = this.filters[this.pos++];

            try {
                Filter filter = filterConfig.getFilter();
                if (request.isAsyncSupported() && "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {
                    request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
                }

                if (Globals.IS_SECURITY_ENABLED) {
                    Principal principal = ((HttpServletRequest)request).getUserPrincipal();
                    Object[] args = new Object[]{request, response, this};
                    SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
                } else {
                    filter.doFilter(request, response, this);
                }

 

 

 

 从上面的图中我们可以看到,chain在一般情况下中主要存在着这么几个filter,其中有我们比较熟悉的characterEncodeingFilter字符编码的过滤器等等,以及我们本次内容的主角:springSecurityFiterChain spring security的过滤器链,可以说这就是spring security的核心所在,springSecurityFiterChain虽然为filter,但他在这里实际扮演的是一个filterChain的角色,从他的的BeanName也可以看出,那我们接下来进入springSecurityFiterChain.doFilter()方法中,看看它内部又有哪些filter,以及内部的逻辑是怎样的

 

 

 可以看到springSecurityFiterChain其实是个代理bean,它的doFilter()中实际用的delegate.doFilter(),delegate是个FilterChainProxy,下面来看下FiterChainProxy的内部实现。

FilterChainProxy.doFilter()方法内部逻辑

        @Override
        public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
               //如果当前游标==additionalFilters的长度,即已经遍历完该列表内的Filter,则结束FilterChainProxy.doFilter()
            if (this.currentPosition == this.size) {
                if (logger.isDebugEnabled()) {
                    logger.debug(LogMessage.of(() -> "Secured " + requestLine(this.firewalledRequest)));
                }
                // Deactivate path stripping as we exit the security filter chain
                this.firewalledRequest.reset();
                this.originalChain.doFilter(request, response);
                return;
            }

            this.currentPosition++;
             //获取列表中下一个Filter
            Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1);
            if (logger.isTraceEnabled()) {
                logger.trace(LogMessage.format("Invoking %s (%d/%d)", nextFilter.getClass().getSimpleName(),
                        this.currentPosition, this.size));
            }
            //执行下一个Filter的doFilter()方法
            nextFilter.doFilter(request, response, this);
        }

    }

我们看到FilterChainProxy中维护了一个Filter列表 additionalFilters,doFilter()中会顺序遍历这个列表,执行每一个Filter的doFilters,那这个列表中具体有哪些Filter呢,让我们来看一下

 

 

 

可以看到该列表中一共有13个spring security内置实现的Filter,系列文章中我们也主要来看SecurityContextPersistenceFilter security上下文持久化的过滤器,主要用来将认证过后的Authentication从session中提取注入本地线程变量中,以及UsernamePasswordAuthenticationFilter,用户密码认证过滤器,主要用来处理通过指定的登录的POST方式的请求url来进行认证.....如果我们的项目中实现了JWT+Spring security的话,一般我们的我们会将自定义实现的JWT过滤器也加入到这条执行链中,并且执行位置放到UserNamePasswordAuthenticationFilter之前。

那么Spring security的大体工作流程就如下图:

 

 

 

三.UserNamePasswordAuthenticationFilter之登录的认证流程

这里我们并没有按照上面的列表顺序从头开始讲,第一个原因是本系列不会解析列表里所有的过滤器,第二个则是个人觉得登录是开启security的入口,从登录开始解析,之后再反过头串联前面的Filter,会有更好的效果。

 

通常情况下我们在security的配置中配置了哪些请求路径是开放的,哪些路径的需要权限的,访问了需要权限的请求时,如果没有权限便会跳转到security默认的登录页中,这时候我们可以进行输入账号密码进行登录,那这次登录请求security是如何处理的呢,让我们来看看UserNamePasswordAuthenticationFilter.doFilter()方法

 

//UserNamePasswordAuthenticationFilter本身并没有实现doFilter()方法,使用的是其父类的doFilter()方法
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
}

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
        implements ApplicationEventPublisherAware, MessageSourceAware {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
    }

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
            //判断当前请求是否满足认证条件,不满足则结束流程,进行下一个过滤器的工作,如何判断满不满足认证条件的逻辑,在下面进行解析
        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
            return;
        }
        try {
            //当前请求满足认证条件,开始尝试去认证,attemptAuthentication()由UserNamePasswordAuthencationFilter自己实现
            Authentication authenticationResult = attemptAuthentication(request, response);
            //如果没有一个认证管理器能认证,结果为null,则直接结束
            if (authenticationResult == null) {
                // return immediately as subclass has indicated that it hasn't completed
                return;
            }
            //调用session策略将认证通过的凭证储存在Session中,保证登陆之后后续同浏览器登陆不需要再登录,这个详细逻辑等到后续展开讲解
            this.sessionStrategy.onAuthentication(authenticationResult, request, response);
            // Authentication success
            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }
            //认证通过后的处理,这里会将当前的凭证填充到本地线程变量中,以及会如何在security的配置中配置了successForwardUrl,将会跳转至该URL
            successfulAuthentication(request, response, chain, authenticationResult);
        }
        catch (InternalAuthenticationServiceException failed) {
            this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
            unsuccessfulAuthentication(request, response, failed);
        }
        catch (AuthenticationException ex) {
            // Authentication failed
            unsuccessfulAuthentication(request, response, ex);
        }
    }
}

 

requiresAuthentication()判断请求是否满足认证条件

    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
            //内部调用了成员属性中的RequestMatcher去进行匹配,下面逻辑可以看到只有请求是POST的并且路径跟pattern完全匹配才会返回true
        if (this.requiresAuthenticationRequestMatcher.matches(request)) {
            return true;
        }
        if (this.logger.isTraceEnabled()) {
            this.logger
                    .trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
        }
        return false;
    }

我们来看下UserNamePasswordAuthencationFilter内置的AntPathRequestMatcher的属性以及匹配逻辑

 

 

 可以看到该AntPathRequestMatcher有两个重要的属性,pattern="/login", httpMethod="POST",这两个属性将在matchs()方法中起到决定性作用。

    
public boolean matches(HttpServletRequest request) {
            //如果请求方式不是httpMthod不一致,也就是非POST请求,则不匹配,返回false
        if (this.httpMethod != null && StringUtils.hasText(request.getMethod())
                && this.httpMethod != HttpMethod.resolve(request.getMethod())) {
            return false;
        }
            //如果当前的pattern=/**,即任意请求,那么匹配,返回true
        if (this.pattern.equals(MATCH_ALL)) {
            return true;
        }
            //获取当前请求的路径(去除掉项目根路径)
        String url = getRequestPath(request);
            //通过debug源码,这里采取的是完全匹配,即需要请求路径和pattern(/login)完全一致才返回true,当然这个pattern是可以配置的,之后会详细指出
        return this.matcher.matches(url);
    }

 

来看一下UserNamePasswordAuthenticationFilter实现的attemptAuthentication()方法

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
           //判断security当前是否只支持POST请求
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        //从请求中获取当前登录的用户名,即从request.getParamter("username")中获取
        String username = obtainUsername(request);
        //没有该参数的话默认空串
        username = (username != null) ? username : "";
        //去除空格
        username = username.trim();
        //获取密码,即request.getPassword("password")
        String password = obtainPassword(request);
        password = (password != null) ? password : "";
        //根据用户密码初始化Authentication凭证,具体为UsernamePasswordAuthentication这种类型的凭证
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        //调用当前环境的认证管理器进行认证, 这里内置的是ProviderManager
        return this.getAuthenticationManager().authenticate(authRequest);
    }

 

ProviderManager.authenticate()认证方法

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //获取传入参数中的认证凭证的类型,当前为UsernamePasswordAuthenticationToken
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        int currentPosition = 0;
        int size = this.providers.size();
        //遍历该认证管理器下所有的授权者(或叫认证者)  
        for (AuthenticationProvider provider : getProviders()) {
                //判断当前授权者是否支持对传入的这种类型的凭证进行认证授权
            if (!provider.supports(toTest)) {
                continue;
            }
            if (logger.isTraceEnabled()) {
                logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
                        provider.getClass().getSimpleName(), ++currentPosition, size));
            }
            try {
                //当前授权者对该凭证进行认证授权,并将结果保存在result中
                result = provider.authenticate(authentication);
                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException | InternalAuthenticationServiceException ex) {
                prepareException(ex, authentication);
                throw ex;
            }
            catch (AuthenticationException ex) {
                lastException = ex;
            }
        }
        //如果遍历完所有的授权者都不能认证,并且当前认证管理器存在父亲认证管理器,那么就调用他父亲认证管理器重复上述操作
        if (result == null && this.parent != null) {
            try {
                //保存父亲认证管理器的认证结果(即授权通过的凭证)
                parentResult = this.parent.authenticate(authentication);
                //赋值到当前的result
                result = parentResult;
            }
            catch (ProviderNotFoundException ex) {
            }
            catch (AuthenticationException ex) {
                parentException = ex;
                lastException = ex;
            }
        }
        if (result != null) {
             //身份验证已完成。删除凭据和其他机密数据
            if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
                ((CredentialsContainer) result).eraseCredentials();
            }

                //如果尝试了父级AuthenticationManager并成功,则
                //将发布AuthenticationSuccessEvent
                //如果父级验证失败,此检查将防止重复AuthenticationSuccessEvent
                //AuthenticationManager已经发布了它

            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }
            //返回认证结果
            return result;
        }

        if (lastException == null) {
            lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
        }
        if (parentException == null) {
            prepareException(lastException, authentication);
        }
        throw lastException;
    }

 

可以看到当前ProviderManager下只有一个授权者,并且该授权者也不支持对UsernamePasswordAuthenticationToken这种凭证进行授权,所以需要调用该ProviderManager的父亲ProviderManager进行尝试认证

 

 

 

 来看下它的父亲ProviderManager有哪些授权者,是否能够对UsernamePasswordAuthenticationToken进行认证授权

 

 

 可以看到这个父亲认证管理器中也只有一个授权者,DaoAuthenticationProvider,利用数据库数据进行认证授权的,而这个就是能够支持对UsernamePasswordAuthenticationToken这种凭证进行认证授权的,下面我们就来看下DaoAuthenticationProvider是如何认证授权的

 

DaoAuthenticationProvider.authenticate()方法,该方法由它的父类AbstractUserDetailsAuthenticationProvider实现

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
         //该授权者只支持对UsernamePasswordAuthenticationToken这种类型凭证进行认证授权,不是的话即抛出异常
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));
        //获取登录的用户名
        String username = determineUsername(authentication);
        boolean cacheWasUsed = true;
        //通过用户名尝试从缓存中获取该用户的信息
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;
            try {
                //缓存中没有,则直接根据username从数据库中查,这里用的其实就是我们自己实现的UserDetailsService.loadUserByUsername(username)方法,下面会展示代码
                user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
            }
               //数据库也查不到该用户便抛出异常
            catch (UsernameNotFoundException ex) {
                this.logger.debug("Failed to find user '" + username + "'");
                if (!this.hideUserNotFoundExceptions) {
                    throw ex;
                }
                throw new BadCredentialsException(this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }
        try {
            //对查询出来的用户信息进行预校验,主要检测的就是UserDetails实体中的isAccountNonExpired是否过期,isAccountNonLocked是否被锁定,
               isEnabled是否可用,只要有一个不满足就抛出异常
       this.preAuthenticationChecks.check(user);
         //附加信息的认证,这里主要是对密码进行认证
      additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
   }
   catch (AuthenticationException ex) {
      if (!cacheWasUsed) {
         throw ex;
      }
      // 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);
      this.preAuthenticationChecks.check(user);
      additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
   }
   //预检通过,进行后置检查,这里主要检查UserDeatils中的isCredentialNonExpired, 即检查密码是否过期,同样由我们自定义的UserDetailsService逻辑的来判断,是否过期
   this.postAuthenticationChecks.check(user);
   if (!cacheWasUsed) {
       //不是从缓存中拿的就存到缓存中
      this.userCache.putUserInCache(user);
   }
   Object principalToReturn = user;
   if (this.forcePrincipalAsString) {
      principalToReturn = user.getUsername();
   }
      //检验通过,将传入的凭证构造成认证通过的凭证
   return createSuccessAuthentication(principalToReturn, authentication, user);
}

 

DaoAuthenticationProvider.retrieveUser()从数据库中获取用户信息

    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
             //通常我们使用security 都会自己实现UserDetailService进行配置,这里就用到了,它会通过我们自定义的逻辑来找寻用户并返回用户信息
            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);
        }
    }

 

AbstractUserDetailsAuthenticationProvider的内部类实现的check方法()

    private class DefaultPreAuthenticationChecks implements UserDetailsChecker {

        @Override
        public void check(UserDetails user) {
                //校验是否被锁定
            if (!user.isAccountNonLocked()) {
                AbstractUserDetailsAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account is locked");
                throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
            }
               //校验是否可用
            if (!user.isEnabled()) {
                AbstractUserDetailsAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account is disabled");
                throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
            }
              //校验是否过期
            if (!user.isAccountNonExpired()) {
                AbstractUserDetailsAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account has expired");
                throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
            }
        }

    }

 

DaoAauthenticationProvider.additionalAuthenticationChecks()对凭证进行附加信息的校验,主要是校验密码

    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
           //如果凭证中的密码==null则直接抛出异常
        if (authentication.getCredentials() == null) {
            this.logger.debug("Failed to authenticate since no credentials provided");
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
        //从凭证中获取当前登录的密码
        String presentedPassword = authentication.getCredentials().toString();
            //调用security环境中的PasswordEncoder将凭证中的密码与数据库查询出的密码进行匹配,匹配不成功则抛出异常,
              这个PasswordEncoder通常我们在使用security的
              时候都会替换成自定义的Encoder,根据自己项目的需求进行自定义的实现其中的逻辑
        if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            this.logger.debug("Failed to authenticate since password does not match stored value");
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }

 

AbstractAuthenticationProcessingFilter.successfulAuthentication()方法:认证通过后一些收尾处理

    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
            Authentication authResult) throws IOException, ServletException {
        //创建一个新的security上下文
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        //将凭证填充进上下文
        context.setAuthentication(authResult);
        //将上下文保存到本地线程变量中
        SecurityContextHolder.setContext(context);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
        }
        //security 开启了remember me功能的话的处理
        this.rememberMeServices.loginSuccess(request, response, authResult);
        if (this.eventPublisher != null) {
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }
        //security中配置了successForwardUrl的话,这里登录成功后就会跳转到指定url上
        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    }

 

四:最后

这篇文章是spring security源码解析的第一章,主要是解析了下security 是如何处理登录流程的,主要就是来看UsernamePasswordAuthenticationFilter的内部处理逻辑,通过源码我们也发现,默认情况下只有你的请求的POST方式的 /login,security才会认为这是登录请求,才会让该请求走UsernamePasswordAuthenticationFilter的处理逻辑,当然这个路径也是可以配置的,如下代码,配置了loginProcessingUrl的路径之后,再次登录,security就会以该路径当做登录请求。

    protected void configure(HttpSecurity http) throws Exception {
        http.csrf()
                .disable() 
                .logout()
                .and()
                .formLogin()  //该路径也是UserNamePasswordAuthenticationFilter用来识别当前请求是不是登录请求,是的话才进行登录处理
                .loginProcessingUrl("/loginMy") 
                /...略../
}

 

关于UsernamePasswordAuthenticationFilter的内容暂时告一段落,下篇内容将继续通过源码解析剩下的过滤器SecurityContextPersistenceFilter security上下文持久化的过滤器等等....。

由于笔者水平有限,有些地方可能讲解的有错误,希望大家能够帮忙指出,共同进步。

 



这篇关于Spring Security源码解析一:UsernamePasswordAuthenticationFilter之登录流程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程