DEBUG,认识一下Spring security的认证过程

112 阅读3分钟

首先认识一个类SecurityContextHolder,这个类可以存储登录的身份信息,通过下面的方法,将身份信息存入框架中。

SecurityContextHolder.getContext().setAuthentication(authentication);

其中authentication来源于身份验证,:

 Authentication authentication = authenticationManager
 .authenticate(new UsernamePasswordAuthenticationToken(username, password));

此处的username和password从前台传来。

登录过程

  1. 调用/login登录接口,并携带username和password。
  2. 登录接口被调用后,会调用业务层的login登录方法(自己写的)。在登录方法中会验证身份
 Authentication authentication = authenticationManager
 .authenticate(new UsernamePasswordAuthenticationToken(username, password));
  1. 验证身份通过后,即登录成功。
接下来我们仔细研究认证方法authenticationManager.authenticate()

通过debug,我们可以发现,authenticate()方法会调用ProviderManagerauthenticate方法


public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    // ...代码
    
    // 调用AbstractUserDetailsAuthenticationProvider的authenticate方法
    result = provider.authenticate(authentication);

}

然后我们来看AbstractUserDetailsAuthenticationProvider中的authenticate方法。

它主要分两步, 第一步根据用户名从后端(数据库)检索该用户的信息。 第二步,将检索的信息中的密码和登录接口中的密码进行比对。具体步骤如下

1. 第一步,检索信息。
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);

我们追踪这个方法,可以发现它会调用DaoAuthenticationProviderretrieveUser方法

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    this.prepareTimingAttackProtection();

    try {
        // 注意这里loadUserByUsername
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
        } else {
            return loadedUser;
        }
    } catch (UsernameNotFoundException ex) {
        this.mitigateAgainstTimingAttack(authentication);
        throw ex;
    } catch (InternalAuthenticationServiceException ex) {
        throw ex;
    } catch (Exception ex) {
        throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
    }
}

注意到loadUserByUsername方法。这个方法就是需要我们自己实现的UserDetailsService接口的loadUserByUsername方法的。我们会在这里获取数据库中存储的用户信息。并返回UserDetail。 在配置SecurityConfig时,我们会将我们自己是先的UserDetailsServiceImpl类,配置到框架中,

@Bean
public AuthenticationManager authenticationManager() {
    DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
    // 配置实现的UserDetailsServiceImpl
    daoAuthenticationProvider.setUserDetailsService(userDetailsService);
    
    // 配置密码加密方式
    daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
    return new ProviderManager(daoAuthenticationProvider);
}

信息获取后,我们回到AbstractUserDetailsAuthenticationProvider中的authenticate方法。

2.现在进行第二步
try {
    // 检查用户信息
    this.preAuthenticationChecks.check(user);
    
    // 验证密码
    this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException ex) {
//...
}

验证密码时,会调用DaoAuthenticationProvideradditionalAuthenticationChecks方法。将前端返回的密码进行我们配置的加密方式进行加密,然后与后端从数据库中获取的密码进行比对,比对成功,则身份认证成功。

protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    if (authentication.getCredentials() == null) {
        this.logger.debug("Failed to authenticate since no credentials provided");
        throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    } else {
        String presentedPassword = authentication.getCredentials().toString();
        
        // 加密前端的密码,然后与后端密码进行比对,比对成功就不会抛出异常,表示认证通过,比对失败则会抛出异常,表示认证不通过。
        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"));
        }
    }
}

总结

最后我来总结一下流程。客户端向服务器发送请求后,请求会通过认证方法进行认证。认证是委托ProviderManager进行管理,ProviderManager中的AbstractUserDetailsAuthenticationProvider和DaoAuthenticationProvider进行实际的认证工作。其中,AbstractUserDetailsAuthenticationProvider负责认证的流程,先检索信息,然后检查,然后比对。DaoAuthenticationProvider则负责流程中具体的实现。