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

Python微信订餐小程序课程视频

https://edu.csdn.net/course/detail/36074

Python实战量化交易理财系统

https://edu.csdn.net/course/detail/35475

一.前言

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);
 }

Spring Security源码解析一:UsernamePasswordAuthenticationFilter之登录流程_第1张图片

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

Spring Security源码解析一:UsernamePasswordAuthenticationFilter之登录流程_第2张图片

可以看到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呢,让我们来看一下

Spring Security源码解析一:UsernamePasswordAuthenticationFilter之登录流程_第3张图片

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

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

Spring Security源码解析一:UsernamePasswordAuthenticationFilter之登录流程_第4张图片

三.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的属性以及匹配逻辑

Spring Security源码解析一:UsernamePasswordAuthenticationFilter之登录流程_第5张图片

可以看到该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  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进行尝试认证

Spring Security源码解析一:UsernamePasswordAuthenticationFilter之登录流程_第6张图片

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

Spring Security源码解析一:UsernamePasswordAuthenticationFilter之登录流程_第7张图片

可以看到这个父亲认证管理器中也只有一个授权者,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上下文持久化的过滤器等等…。

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

你可能感兴趣的:(it,spring,java,后端)