此文章基于Spring Security 6.0
1、当一个未认证用户请求一个不在白名单里的接口
2、服务端FilterSecurityInterceptor拒绝了这个未认证的请求,并抛出AccessDeniedException
3、根据配置的LoginUrl或者AuthenticationEntryPoint,服务端向客户端响应请求,重定向到登录界面
4、客户端请求登录界面
5、服务端返回登录界面
tips:如果是前后端分离项目,跟4,5没关系
当用户提交的请求路径匹配到post方法的/login(或者自定义)时UsernamePasswordAuthenticationFilter过滤器被触发
1、在UsernamePasswordAuthenticationFilter中组装一个authenticated为false的UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken中的属性
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,password);
public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
return new UsernamePasswordAuthenticationToken(principal, credentials);
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
2、从Spring获取一个AuthenticationManager实例,调用authenticate方法,验证用户名和密码是否正确,至于这个authenticate用到的是哪个,就看用户名密码存在哪里了(内存和数据库)
3、如果认证失败了清除SecurityContextHolder,调用RememberMeService的内容,如果没有配置RememberMeService,那么不调用,然后返回登录失败的信息
4、如果认证成功了,将存储session,保存认证信息,如果有配置RememberMeService,调用RememberMeService,ApplicationEventPublisher publishes an InteractiveAuthenticationSuccessEvent,最后调用AuthenticationSuccessHandler的方法,重定向到之前RequestCache里的URL。如果没有,就重定向到主页
在Spring Security 6.0中,AuthenticationManager默认实现类是ProviderManager,这个类的作用就是,选出基于你的配置,可以提供authenticate方法的类,如果不出意外的话,基本就是DaoAuthenticationProvider了,在这个类中,有两个关键方法
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
/**
* The plaintext password used to perform
* {@link PasswordEncoder#matches(CharSequence, String)} on when the user is not found
* to avoid SEC-2056.
*/
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private PasswordEncoder passwordEncoder;
/**
* The password used to perform {@link PasswordEncoder#matches(CharSequence, String)}
* on when the user is not found to avoid SEC-2056. This is necessary, because some
* {@link PasswordEncoder} implementations will short circuit if the password is not
* in a valid format.
*/
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
public DaoAuthenticationProvider() {
setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
@Override
@SuppressWarnings("deprecation")
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"));
}
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"));
}
}
@Override
protected void doAfterPropertiesSet() {
Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
}
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
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);
}
}
@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
private void prepareTimingAttackProtection() {
if (this.userNotFoundEncodedPassword == null) {
this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
}
}
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
}
}
/**
* Sets the PasswordEncoder instance to be used to encode and validate passwords. If
* not set, the password will be compared using
* {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}
* @param passwordEncoder must be an instance of one of the {@code PasswordEncoder}
* types.
*/
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
this.userNotFoundEncodedPassword = null;
}
protected PasswordEncoder getPasswordEncoder() {
return this.passwordEncoder;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected UserDetailsService getUserDetailsService() {
return this.userDetailsService;
}
public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
this.userDetailsPasswordService = userDetailsPasswordService;
}
}
UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)
这两个方法分别是从UserdetailsService中根据用户名取出一个UserDetail以及进行密码校验。
在密码校验通过后返回一个认证通过的Authentication,否则就抛出密码错误
在SpringBoot启动的时候,如果没有配置UserDetailsService的bean,那么就不会调用这个类(会在控制台打印密码,默认用户名user),如果配置了UserDetailsService但没有配置加密类的bean,就会报错(Spring Security 5.0是这样的,不知道6.0有没有默认的加密类,还没试过)