首先认识一个类SecurityContextHolder,这个类可以存储登录的身份信息,通过下面的方法,将身份信息存入框架中。
SecurityContextHolder.getContext().setAuthentication(authentication);
其中authentication来源于身份验证,:
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
此处的username和password从前台传来。
登录过程
- 调用/login登录接口,并携带username和password。
- 登录接口被调用后,会调用业务层的login登录方法(自己写的)。在登录方法中会验证身份
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
- 验证身份通过后,即登录成功。
接下来我们仔细研究认证方法authenticationManager.authenticate()
通过debug,我们可以发现,authenticate()方法会调用ProviderManager的authenticate方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// ...代码
// 调用AbstractUserDetailsAuthenticationProvider的authenticate方法
result = provider.authenticate(authentication);
}
然后我们来看AbstractUserDetailsAuthenticationProvider中的authenticate方法。
它主要分两步, 第一步根据用户名从后端(数据库)检索该用户的信息。 第二步,将检索的信息中的密码和登录接口中的密码进行比对。具体步骤如下
1. 第一步,检索信息。
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
我们追踪这个方法,可以发现它会调用DaoAuthenticationProvider的retrieveUser方法
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) {
//...
}
验证密码时,会调用DaoAuthenticationProvider的additionalAuthenticationChecks方法。将前端返回的密码进行我们配置的加密方式进行加密,然后与后端从数据库中获取的密码进行比对,比对成功,则身份认证成功。
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则负责流程中具体的实现。