Spring Security的登录密码验证有一个默认的实现类叫做DaoAuthenticationProvider,如下:
它的登录验证方法是这个additionalAuthenticationChecks,如下:
可以看到当密码校验未通过时,它通过messages.getMessage()这个方法来获取提示信息,这个messages对象是一个MessageSourceAccessor,来源于它的一个抽象父类AbstractUserDetailsAuthenticationProvider,如下:
可以看到这个messages对象由SpringSecurityMessageSource.getAccessor()初始化 ,那么我们继续看看这个SpringSecurityMessageSource,如下:
到这步就很明了了, 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的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();
}
}
回顾我们自己写的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