美文网首页
SpringSecurity认证流程UsernamePasswo

SpringSecurity认证流程UsernamePasswo

作者: 念䋛 | 来源:发表于2020-12-31 20:54 被阅读0次

SpringSecurity认证流程UsernamePasswordAuthenticationFilter分析

认证的流程就是完成一系列的过滤器, Sprinig Security 启动源码详细分析文章提过,Security维护一套自己的过滤器链,也可以添加自定义过滤器,并指定order,这次分析默认的认证流程.

当前端请求到后端,经过一系列的servlet过滤器,其中包括Security过滤器DelegatingFilterProxy,而实际调用的为VirtualFilterChain类的成员变量additionalFilters,下图为Security过滤器链如何执行,以及执行结束后,切换到servlet过滤器链

image.png
image.png
UsernamePasswordAuthenticationFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
三个关键的过滤器
本文只分析UsernamePasswordAuthenticationFilter过滤器
自定义规则的代码
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf ().disable ().formLogin ()
            .loginPage ("/page/login")//请求登入页的路径
            .loginProcessingUrl ("/login/code")//输入用户名和密码 验证的路径
           // .successHandler (myAuthenticationSuccessHandler)
            .successHandler (mySavedRequestAwareAuthenticationSuccessHandler)
           // .failureHandler (myAuthenticationFailureHandler)
            .failureHandler (mySimpleUrlAuthenticationFailureHandler)
            .and ()
            .authorizeRequests ()
            .antMatchers ("/hello/**").authenticated ()//需要身份认证即可,没有角色要求,下面配置身份验证规则
            .antMatchers ("/nihao/**", "/page/login").permitAll ()
            .antMatchers ("/nihao/**").hasAnyRole ("role","admin")
            //.antMatchers("/imgs/**").permitAll() //如果有静态的资源(存放在 resources/static,resources/templates) 比图 css js 或者文件 ,注意这里不需要写static,我们还可以重写 configure(WevSecurity web)方法
            .anyRequest ().authenticated (); //访问需要身份验证,如果验证不通过则返回到登入也
}
  1. UsernamePasswordAuthenticationFilter在初始化结束后成员变量的情况,方便后续分析源码.
    [图片上传中...(image.png-9e01c-1609208956602-0)]
    UsernamePasswordAuthenticationFilter继承了AbstractAuthenticationProcessingFilter类,调用UsernamePasswordAuthenticationFilter的doFilter方法是调用父类方法
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {

   HttpServletRequest request = (HttpServletRequest) req;
   HttpServletResponse response = (HttpServletResponse) res;
//判断request的请求路径是否为需要校验用户名和密码的路径 默认为 /login
   if (!requiresAuthentication(request, response)) {
      chain.doFilter(request, response);
      return;
   }
   if (logger.isDebugEnabled()) {
      logger.debug("Request is to process authentication");
   }
   Authentication authResult;
   try {
//校验用户名密码
      authResult = attemptAuthentication(request, response);
      if (authResult == null) {
         // return immediately as subclass has indicated that it hasn't completed
         // authentication
         return;
      }
      sessionStrategy.onAuthentication(authResult, request, response);
   }
   catch (InternalAuthenticationServiceException failed) {
      logger.error(
            "An internal error occurred while trying to authenticate the user.",
            failed);
      unsuccessfulAuthentication(request, response, failed);
      return;
   }
   catch (AuthenticationException failed) {
      // Authentication failed
      unsuccessfulAuthentication(request, response, failed);
      return;
   }
   // Authentication success
   if (continueChainBeforeSuccessfulAuthentication) {
      chain.doFilter(request, response);
   }
   successfulAuthentication(request, response, chain, authResult);
}
  1. requiresAuthentication(request, response)则是调用了AndRequestMatcher的match()方法,在创建UsernamePasswordAuthenticationFilter对象时创建了AndRequestMatcher类并初始化路径为/login,方法为POST,在调用自定义规则方法中 loginProcessingUrl ("/login/code"),方法时可以修改默认的/login路径,我们这里就修改为/login/code,如果request的请求路径为/login/code并且为POST方法则返回true,取反为false,
  2. 返回步骤1方法向下执行到方法向下执行
    authResult = attemptAuthentication(request, response);执行UsernamePasswordAuthenticationFilter的attemptAuthentication方法
public Authentication attemptAuthentication(HttpServletRequest request,
      HttpServletResponse response) throws AuthenticationException {
//再次判断请求方法是不是POST
   if (postOnly && !request.getMethod().equals("POST")) {
      throw new AuthenticationServiceException(
            "Authentication method not supported: " + request.getMethod());
   }
//从reqeust中获取username和password 
   String username = obtainUsername(request);
   String password = obtainPassword(request);

   if (username == null) {
      username = "";
   }

   if (password == null) {
      password = "";
   }

   username = username.trim();
//初始化UsernamePasswordAuthenticationToken将用户名和密码传入
   UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
         username, password);

   // Allow subclasses to set the "details" property
   setDetails(request, authRequest);

   return this.getAuthenticationManager().authenticate(authRequest);
}
  1. 初始化UsernamePasswordAuthrnticationToken
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
      Collection<? extends GrantedAuthority> authorities) {
   super(authorities);
   this.principal = principal;
   this.credentials = credentials;
//将父类AbstractAuthenticationToken的成员变量authenticated赋值为false
   super.setAuthenticated(true); // must use super, as we override
}

  1. 返回步骤3 setDetails(request, authRequest); 追踪源码初始化WebAuthenticationDetails,并赋值给UsernamePasswordAuthenticationToken的details
public WebAuthenticationDetails(HttpServletRequest request) {
   this.remoteAddress = request.getRemoteAddr();

   HttpSession session = request.getSession(false);
   this.sessionId = (session != null) ? session.getId() : null;
}
  1. 返回步骤3 return this.getAuthenticationManager().authenticate(authRequest);

this.getAuthenticationManager()获取的是ProviderManager对象,执行ProviderManager

  1. authenticate方法

authenticate会被递归两次调用

public Authentication authenticate(Authentication authentication)
      throws AuthenticationException {
//为UsernamePasswordAuthencationToken
   Class<? extends Authentication> toTest = authentication.getClass();
   AuthenticationException lastException = null;
   AuthenticationException parentException = null;
   Authentication result = null;
   Authentication parentResult = null;
   boolean debug = logger.isDebugEnabled();
// 第一次getProviders()获取的是ProbiderManager的providers 成员变量是list里面只有一个数据AnonymousAuthenticationPrivoder
//第二次调用getProviders()获取的是ProbiderManager的providers 成员变量是list里面只有一个数据DaoAuthenticationProvider

   for (AuthenticationProvider provider : getProviders()) {
//第一次AnonymousAuthenticationPrivoder不支持UsernamePasswordAuthencationToken
//第二次DaoAuthenticationProvider支持UsernamePasswordAuthencationToken
      if (!provider.supports(toTest)) {
         continue;
      }
      if (debug) {
         logger.debug("Authentication attempt using "
               + provider.getClass().getName());
      }
      try {
//第二次DaoAuthenticationProvider支持UsernamePasswordAuthencationToken,所以这里调用DaoAuthenticationProvider的authenticate方法
         result = provider.authenticate(authentication);
         if (result != null) {
            copyDetails(authentication, result);
            break;
         }
      }
      catch (AccountStatusException | InternalAuthenticationServiceException e) {
         prepareException(e, authentication);
         throw e;
      } catch (AuthenticationException e) {
         lastException = e;
      }
   }
//第一次返回null
   if (result == null && parent != null) {

      try {
//parent为ProviderManager,递归调用
         result = parentResult = parent.authenticate(authentication);
      }
      catch (ProviderNotFoundException e) {
      }
      catch (AuthenticationException e) {
         lastException = parentException = e;
      }
   }
   if (result != null) {
      if (eraseCredentialsAfterAuthentication
            && (result instanceof CredentialsContainer)) {
         ((CredentialsContainer) result).eraseCredentials();
      }

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

  1. DaoAuthenticationProvider的authenticate方法
public Authentication authenticate(Authentication authentication)
      throws AuthenticationException {
   String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
         : authentication.getName();
   boolean cacheWasUsed = true;
//从缓存中获取user对象,已经校验成功的对象会被放到缓存中
  UserDetails user = this.userCache.getUserFromCache(username);
   if (user == null) {
      cacheWasUsed = false;
      try {
//获取user对象,调用本类(DaoAuthenticationProvider)的retrieveUser方法,
        user = retrieveUser(username,
               (UsernamePasswordAuthenticationToken) authentication);
      }
      catch (UsernameNotFoundException notFound) {
         logger.debug("User '" + username + "' not found");
         if (hideUserNotFoundExceptions) {
            throw new BadCredentialsException(messages.getMessage(
                  "AbstractUserDetailsAuthenticationProvider.badCredentials",
                  "Bad credentials"));
         }
         else {
            throw notFound;
         }
      }
   try {
//一直到return都是校验用户名和密码和检验用户是否锁定,是否过期等
      preAuthenticationChecks.check(user);
      additionalAuthenticationChecks(user,
            (UsernamePasswordAuthenticationToken) authentication);
   }
   catch (AuthenticationException exception) {
      if (cacheWasUsed) {
         // 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);
         preAuthenticationChecks.check(user);
         additionalAuthenticationChecks(user,
               (UsernamePasswordAuthenticationToken) authentication);
      }
      else {
         throw exception;
      }
   }
   postAuthenticationChecks.check(user);
   if (!cacheWasUsed) {
      this.userCache.putUserInCache(user);
   }
   Object principalToReturn = user;
   if (forcePrincipalAsString) {
      principalToReturn = user.getUsername();
   }
//返回UsernamePasswordAuthencationToken
   return createSuccessAuthentication(principalToReturn, authentication, user);
}

  1. user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
protected final UserDetails retrieveUser(String username,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
//从ioc容器中获取PassWordEncoder,我们在配置类中将 BCryptPasswordEncoder放到容器中 
prepareTimingAttackProtection();
   try {
// this.getUserDetailsService()是我们自定义的UserDetailServiceImpl类,从数据库中获取的user信息
      UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
      if (loadedUser == null) {
         throw new InternalAuthenticationServiceException(
               "UserDetailsService returned null, which is an interface contract violation");
      }
//返回user
      return loadedUser;
   }
   catch (UsernameNotFoundException ex) {
      mitigateAgainstTimingAttack(authentication);
      throw ex;
   }
   catch (InternalAuthenticationServiceException ex) {
      throw ex;
   }
   catch (Exception ex) {
      throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
   }
}

  1. 回到步骤8代码向下执行到
    return createSuccessAuthentication(principalToReturn, authentication, user);
    下面的图片就是最终得到的UsernamePasswordAuthencationToken


    image.png
  2. 返回到步骤1的authResult = attemptAuthentication(request, response);

authResult就是上图得到的结果代码向下执行

成功则执行successfulAuthentication(request, response, chain, authResult);

successfulAuthentication方法中的这段代码SecurityContextHolder.getContext().setAuthentication(authResult);因为是ThreadLocalSecurityContextHolderStrategy所以供当前线程使用,在调用SecurityContextPersistenceFilter的finally的时候,将session和context绑定起来,再次携带session访问的时候,就会验证是否是已经登入成功的用户在访问.

失败则执行unsuccessfulAuthentication(request, response, failed);

最终执行的是我们自定义的 MySavedRequestAwareAuthenticationSuccessHandler

MySimpleUrlAuthenticationFailureHandler两个类,当然在执行successfulAuthentication会有记住我功能,会在后续分析,过滤器链到此就结束了,直接返回给了前台

  1. 如果我们不是前后端分离项目,那么登入成功或者失败需要返回页面,错误页面默认情况下就是登入页面,只是在返回的状态和信息提示是登入错误,但是登入成功,我们需要返回被拦截的页面,那Security是如何在登入成功之后返回之前被拦截的页面呢.
    在访问路径的时候,判断路径是否需要拦截,拦截之后会存放到DefaultSavedRequest的URLRequest中,当检验成功之中在重定向到URLRequest路径.
    下面是自定已成功处理器,当检验成功就会执行下面的onAuthenticationonSuccess
@Component
public class MySavedRequestAwareAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //条件根据项目实际需要,替代true即可,可以在配置文件利用@Value获取变量替代
        if(false){
            //返回json字符串
        response.setContentType ("application/json;charset=UTF-8");
        response.getWriter ().write ("登入成功");}
        else{
            //如果访问/hello被拦截,登入成功后重定向到/hello
            super.onAuthenticationSuccess (request, response, authentication);
        }
    }
}

image.png

至于路径的是如何根据自定义规则被拦截,后续分分析.

相关文章

网友评论

      本文标题:SpringSecurity认证流程UsernamePasswo

      本文链接:https://www.haomeiwen.com/subject/svkfoktx.html