一、事情是这样的
有一天,看到这个代码的时候:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserService userService;
@Autowired
public UserDetailsServiceImpl(UserService userService) {
this.userService = userService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (username.isBlank()) {
throw new UsernameNotFoundException("用户名不能为空!");
}
User user = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username).eq(User::getDeleted, false));
if (Objects.nonNull(user)) {
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), Collections.emptyList());
} else {
throw new UsernameNotFoundException("当前用户不存在!");
}
}
}
突然反应过来,这些中文提示信息,好像从来没有在我登录失败的时候出现过啊?仔细一想,是没有出现过。那去试试呗,调用登录接口,如果用户名密码不同时写对,都出现以下结果:
{
"success": false,
"code": -1,
"message": "用户名或密码错误",
"data": null
}
不管错的是用户名,还是密码,都一样的提示:用户名或密码错误。果然是不出现的啊。
它的英文名叫:Bad credentials。日文叫做:ユーザ名かパスワードが正しくありません
那这可不行啊,虽然这个提示没啥毛病,也并不是刚需,但是我代码都写了,那你就得给我显示出来。
二、然后我就动手了
那就debug嘛,加个断点,按F7,F8呗。谁不会啊。直到来到这个方法:org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate
它是这样的:
然后注意到137行,做了一个判断:
if(!this.hideUserNotFoundExceptions)
看这变量名也知道这玩意干了什么,然后接下来跳过138行,直接执行到140行,抛出异常:
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
好啊,没想到你是这样的异常。
点一下那个hideUserNotFoundExceptions,原来它在这儿出生的
org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#hideUserNotFoundExceptions
那把这玩意重新设置一下呗,很easy啊。这是个抽象类,那我们看看它的子类实现嘛:
好!就是你了,这个什么DaoAuthenticationProvider。大哥亲自帮你在spring里面注册一个帐号:
@Bean
public AuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(encoder());
daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
return daoAuthenticationProvider;
}
除了把hideUserNotFoundExceptions这个属性设置成false,记得把咱们的UserDetailService实现和PasswordEncode也注入进去。
好,测试一下,先试试错误的用户名:
进来了,完美。
{
"success": false,
"code": -1,
"message": "当前用户不存在!",
"data": null
}
再试试用户名正确,密码错误的情况:
{
"success": false,
"code": -1,
"message": "Bad credentials",
"data": null
}
啊……这就??????怎么就变成英文了呀。虽然我知道什么意思,这个也并不是刚需。但是我是刚才都中文的,那现在也不能变成英文。
我本可以忍受黑暗,如果我不曾见过光明。
三、战况胶着,心也焦灼
那继续呗,还是F7,F8。
很明显,从这里进去:
78行抛异常,再明显不过了。 看看这个方法是什么玩意:org.springframework.context.support.MessageSourceAccessor#getMessage(java.lang.String, java.lang.String)
进去以后,从1378行到1397行
注意这个messageSource,它是Empty MessageSource,但并不null
那就再继续进1378行的getMessage方法,直到
org.springframework.context.support.DelegatingMessageSource#getMessage(java.lang.String, java.lang.Object[], java.lang.String, java.util.Locale)
这个方法的63行
再进去:
在这个类中,就可以看,返回了defaultMessage:Bad credentials
信息来源我们找到,但是要怎么改变它这个message呢?先想一想,之前,我们没加DaoAuthenticationProvider的时候,不就是中文么,为什么现在变英文了呢?好好的提示,怎么说变就变呢?
啊,这不是道德的沦丧,也不是人性的扭曲,它只是让引起我们的debug。
好,那先干掉我们之前配置的DaoAuthenticationProvider,恢复中文提示的时候,再看一遍:
接下来的一切都顺水推舟、水到渠成、顺理成章,就返回中文了。
但是仔细一想,虽然它们都在不停的执行各种getMessage方法,但是返回结果却不相同,这要说不是java特性里面的……里面的……封装和继承,真的很难说得过去啊。
那就确定子类身份,准备挖祖……翻族谱呗。
对比着看一遍,返回英文的时候, 执行的类的chain是这样的,从左到右依次:
返回中文的时候,是这样的:
那等什么,看看这个getMessage方法在哪个类定义的,这个类肯定是AbstractApplicationContext和AbstractMessageSource的公共父类。找到了MessageSource:
这里还是在回返中文时,我们debug看到的类:ResourceBundleMessageSource
而且里面有一个重要的属性,就是这个basenames:
然后找到这个basenames属性,就照着它的setBasenames方法的usages点,总一个地方设置了这玩意, 然后你就看到了这个:
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration
名字很易懂,自动配置。
那解决办法已经呼之欲出了,虽然我们没有写这些个文件配置信息,但是我们依葫芦画瓢,把这玩意设置一下呗,很easy啊。不就是MessageSource嘛,大爷亲自帮你注册一个!刚好这个org.springframework.context.support.ResourceBundleMessageSource
类就可以直接new一个,在你的配置里面注册一个bean,返回一个ResourceBundleMessageSource,并且设置好一些属性。(此处用的ReloadableResourceBundleMessageSource实现类,相比于 ResourceBundleMessageSource,它可以不重启项目,自动刷新配置文件)
@Bean
public ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:org/springframework/security/messages_zh_CN");
// messageSource.setBasename("classpath:org/springframework/security/messages_ja");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setUseCodeAsDefaultMessage(true);
messageSource.setFallbackToSystemLocale(false);
return messageSource;
}
主要就是这个basename,它怎么来的呢?
在debug的时候,看到绿色那个字了吗,那个就是它的值,它怎么来的?就找它的set方法呗,这不在这儿写得死死的吗,你重启时就加载了。
再顺便找一找这配置文件在哪,顺着源码包的org.springframework.security.messages
这个路径找下去,你就发现惊喜了,我们需要的basenames有着落了。不行怎么办,前面加“classpath:”,或者把文件copy到resource目录下,都可以试试嘛。
但是需要说明的是,虽然这里写了basename的值为“org.springframework.security.messages”,我们也确实找到了这个文件,但这个文件里面的提示全是英文的,而默认情况下(就本文最开始的情况)的提示是中文的,说明其实加载的并不是这个配置文件。
而事实上,加载配置文件的方法是java.util.ResourceBundle#getBundleImpl,它有三个重载方法,老大调老二,老二调老三,老三去findBundle,findBundle去loadBundle,loadBundle去new Bundle,老三负责找出符合条件的Bundle。这个条件就是我们的Locale,在LocaleContextHolder里面有默认中文。这是一个循环的过程,在加载资源文件时,根据basename和Locale拼接出需要加载的配置文件名进行加载:java.util.ResourceBundle.Control#toBundleName。然后最后老三和这个默认的中文的Locale对比,最终得到并返回一个中文提示的bundle。
重启项目吧,好了,世界安静了:
wtf?是的,它起不来了。为什么?
世界就是这么巧合,也许你的MessageSource刚好配置在WebSecurityConfigurerAdapter里面,它的方法名刚好叫messageSource,又或者Bean设置了name刚好叫messageSource,大抵就是这样的:
从结论来看:它重名了 。
那怎么办,改个名呗,换成getMessageSource,再启动,可以了!然后,一看最终效果:
{
"success": false,
"code": -1,
"message": "Bad credentials",
"data": null
}
达咩哒~
这怎么办呢,很好办的,就跟去网吧一样,游戏崩了找网管,网管说重启,你重启了,不行,网管又说,换台机器。那这里也一样。重启不行,WebSecurityConfigurerAdapter里也不行,那就换个地方写嘛,刚才不是有个MessageSourceAutoConfiguration吗,我自己写一个配置类叫MessageSourceManualConfiguration(笑),在里面写这个(名字很重要):
@Bean
public ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
// messageSource.setBasename("classpath:org/springframework/security/messages_zh_CN");
messageSource.setBasename("classpath:org/springframework/security/messages_ja");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setUseCodeAsDefaultMessage(true);
messageSource.setFallbackToSystemLocale(false);
return messageSource;
}
完全可以。
刚才我们配置了DaoAuthenticationProvider,现在配置了ReloadableResourceBundleMessageSource,这下才是真的安静了。
四、那是我心心念念的放不下
上面messageSource重名的问题,是什么原因呢?老规矩,在哪里报错,就在哪里debug:
一看这个factoryMethod,这个什么WebMvcConfiguratioSupport的字眼实在是抹布洗四基佬,
不由得想起它:
然后这样:
@SpringBootApplication(exclude = {WebMvcAutoConfiguration.class})
事实证明这样也是可以的,刚才我们遇到的巧合,也能成功了。
五、一直很安静
其实可以发现DaoAuthenticationProvider里面有这么一个方法:
daoAuthenticationProvider.setMessageSource(messageSource);
可以加,但没必要,实际上我们注册了ReloadableResourceBundleMessageSource,在装配的时候发现这个messageSource是空的,会默认装配我们注册的这个ReloadableResourceBundleMessageSource。
就像你并不需要在下面这个方法:
com.ustellar.hotmoon.config.SecurityConfig#configure(org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder)
加入这行代码:
auth.authenticationProvider(daoAuthenticationProvider());