Spring Security修改默认的Bad Credentials提示 及其 原理(Spring Boot 2.0)

时间:2024-04-09 07:54:02

 

Spring Security的各种提示内容来源于Spring的国际化资源文件类MessageSource,当我们想要修改默认提示时可能理所当然的会想到通过自己配置一个MessageSource的方式来实现,但是这种做法并不适用于Spring Security,原因如下,我们跟踪它的代码看看:

 

Spring Security的登录密码验证有一个默认的实现类叫做DaoAuthenticationProvider,如下:

Spring Security修改默认的Bad Credentials提示 及其 原理(Spring Boot 2.0)

 

它的登录验证方法是这个additionalAuthenticationChecks,如下:

Spring Security修改默认的Bad Credentials提示 及其 原理(Spring Boot 2.0)

 

可以看到当密码校验未通过时,它通过messages.getMessage()这个方法来获取提示信息,这个messages对象是一个MessageSourceAccessor,来源于它的一个抽象父类AbstractUserDetailsAuthenticationProvider,如下:

Spring Security修改默认的Bad Credentials提示 及其 原理(Spring Boot 2.0)

 

可以看到这个messages对象由SpringSecurityMessageSource.getAccessor()初始化 ,那么我们继续看看这个SpringSecurityMessageSource,如下:

Spring Security修改默认的Bad Credentials提示 及其 原理(Spring Boot 2.0)

 

到这步就很明了了, SpringSecurityMessageSource实际上继承自ResourceBundleMessageSource,它的getAccessor()是一个static方法。方法里new了一个自己,然后设置basename到了org.springframework.security.messages这个路径,这个路径实际上就是security-core包里面的一个路径。然后再new了一个新的MessageSourceAccessor。所以无论我们怎么配置我们自己的MessageSource都没有作用,因为Spring Security最后用到的这个MessageSourceAccessor是它自己new出来的,而不是Spring托管的

 

那么,我们要怎样去修改这个Spring Security默认的提示呢?这里我的一个思路是:

① 我们自己去写一个AuthenticationProvider去继承这个Spring Security的登录校验父类DaoAuthenticationProvider

② 自己去配置一个MessageSource

③ 把我们自己配置的MessageSource赋值给父类DaoAuthenticationProvider的messages对象

④ 把自己的AuthenticationProvider设置给Spring Security

 

下面我们来看看具体步骤:

先写个简单的AuthenticationProvider(这里我的叫LoginAuthenticationProvider)如下:

@Component
public class LoginAuthenticationProvider extends DaoAuthenticationProvider{
	
	@Autowired
	private JdbcUserDetailsService jdbcUserDetailsService;
	
	@Autowired
	private PasswordEncoder passwordEncoder;
	
	@Autowired
	private void setJdbcUserDetailsService() {
		setUserDetailsService(jdbcUserDetailsService);
	}
	
	@PostConstruct
	public void initProvider() {
		ReloadableResourceBundleMessageSource localMessageSource = new ReloadableResourceBundleMessageSource();
		localMessageSource.setBasenames("messages_zh_CN");
		messages = new MessageSourceAccessor(localMessageSource);
	}
	
	@Override
	protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			logger.debug("Authentication failed: no credentials provided");

			throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}

		String presentedPassword = authentication.getCredentials().toString();

		if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			logger.debug("Authentication failed: password does not match stored value");

			throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}
	
}

注意LoginAuthenticationProvider需要一个UserDetailsService,否则会启动失败,这里我用setter注入了一个我自己的UserDetailsService叫做JdbcUserDetailsService。记住一定要给自己的JdbcUserDetailsService打上@Component和@Order(0)注解,否则Spring在加载的时候有可能会先加载我们自己写的LoginAuthenticationProvider然后找不到JdbcUserDetailsService导致启动失败。

 

然后用@PostConstruct去写一个简单的初始化,在初始化的时候将一个我们自己配置的MessageSource(这里我使用的是ReloadableResourceBundleMessageSource)赋值给父类的messages对象。注意setBasenames()方法会自己查找src/main/resources下的.properties文件。这里我新建了一个文件叫messages_zh_CN.properties放在了src/main/resources下面,并且修改了里面的默认提示,如图:

Spring Security修改默认的Bad Credentials提示 及其 原理(Spring Boot 2.0)

最后在Spring Security的WebSecurityConfigurerAdapter里面配置我们自己的 LoginAuthenticationProvider即可,代码如下:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	
    @Autowired
    private LoginAuthenticationProvider loginAuthenticationProvider;
	
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .anyRequest().authenticated()
            .and().csrf().disable();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    	auth.authenticationProvider(loginAuthenticationProvider);
    	super.configure(auth);
    }
	
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
    	return super.authenticationManagerBean();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {   	
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

 

到这里我们已经可以实现自定义Spring Security的提示了。但是在实际测试中我还遇到了一个题外的小问题仍未解决,如果你有兴趣可以接着往下读并寻找答案。

 

回顾我们自己写的DaoAuthenticationProvider父类的子类LoginAuthenticationProvider如下:

@Component
public class LoginAuthenticationProvider extends DaoAuthenticationProvider{
	
	@Autowired
	private JdbcUserDetailsService jdbcUserDetailsService;
	
	@Autowired
	private PasswordEncoder passwordEncoder;
	
	@Autowired
	private void setJdbcUserDetailsService() {
		setUserDetailsService(jdbcUserDetailsService);
	}
	
	@PostConstruct
	public void initProvider() {
		ReloadableResourceBundleMessageSource localMessageSource = new ReloadableResourceBundleMessageSource();
		localMessageSource.setBasenames("messages_zh_CN");
		messages = new MessageSourceAccessor(localMessageSource);
	}
	
	@Override
	protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			logger.debug("Authentication failed: no credentials provided");

			throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}

		String presentedPassword = authentication.getCredentials().toString();

		if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			logger.debug("Authentication failed: password does not match stored value");

			throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}
	
}

可以发现我使用了@PostConstruct注解做初始化去给父类的messages对象赋予新的MessageSource。那么能不能用@Autowired的setter注入去实现这步呢,答案是可以的。代码如下:

@Component
public class LoginAuthenticationProvider extends DaoAuthenticationProvider{
	
	@Autowired
	private JdbcUserDetailsService jdbcUserDetailsService;
	
	@Autowired
	private PasswordEncoder passwordEncoder;
	
	@Autowired
	private void setJdbcUserDetailsService() {
		setUserDetailsService(jdbcUserDetailsService);
	}
	
	@Autowired
	public void setLocalMessageSource() {
		ReloadableResourceBundleMessageSource localMessageSource = new ReloadableResourceBundleMessageSource();
		localMessageSource.setBasenames("messages_zh_CN");
		messages = new MessageSourceAccessor(localMessageSource);
	}
	
	/*@PostConstruct
	public void initProvider() {
		ReloadableResourceBundleMessageSource localMessageSource = new ReloadableResourceBundleMessageSource();
		localMessageSource.setBasenames("messages_zh_CN");
		messages = new MessageSourceAccessor(localMessageSource);
	}*/
	
	@Override
	protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			logger.debug("Authentication failed: no credentials provided");

			throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}

		String presentedPassword = authentication.getCredentials().toString();

		if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			logger.debug("Authentication failed: password does not match stored value");

			throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}
	
}

 但是实际测试发现,Spring Security依然会使用默认的提示。这里我怀疑Spring Security依然使用的是父类的messages对象,所以我在子类LoginAuthenticationProvider中对父类的messages进行了覆盖,如下:

@Component
public class LoginAuthenticationProvider extends DaoAuthenticationProvider{
	
        //覆盖父类的messages
	private MessageSourceAccessor messages;
	
	@Autowired
	private JdbcUserDetailsService jdbcUserDetailsService;
	
	@Autowired
	private PasswordEncoder passwordEncoder;
	
	@Autowired
	private void setJdbcUserDetailsService() {
		setUserDetailsService(jdbcUserDetailsService);
	}
	
	@Autowired
	public void setLocalMessageSource() {
		ReloadableResourceBundleMessageSource localMessageSource = new ReloadableResourceBundleMessageSource();
		localMessageSource.setBasenames("messages_zh_CN");
		messages = new MessageSourceAccessor(localMessageSource);
	}
	
	/*@PostConstruct
	public void initProvider() {
		ReloadableResourceBundleMessageSource localMessageSource = new ReloadableResourceBundleMessageSource();
		localMessageSource.setBasenames("messages_zh_CN");
		messages = new MessageSourceAccessor(localMessageSource);
	}*/
	
	@Override
	protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			logger.debug("Authentication failed: no credentials provided");

			throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}

		String presentedPassword = authentication.getCredentials().toString();

		if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			logger.debug("Authentication failed: password does not match stored value");

			throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}
	
}

经测试,覆盖父类的messages之后Spring Security便会使用我们自己配置的MessageSource来提示我们自己的信息了。

 

这里我猜测是Spring在使用AuthenticationProvider时并没有完全加载Spring托管的LoginAuthenticationProvider,导致Spring使用了父类的messages对象,当然我这是瞎猜:(

 

如果你有兴趣可以试着自己去探索一下...

 

本文参考:

https://www.jianshu.com/p/955e30866121

https://www.jianshu.com/p/46a4355ad0a3?from=groupmessage