jdk11,springboot2.6.7环境下的UsernameNotFoundException和i18n配置

一、事情是这样的

有一天,看到这个代码的时候:

@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());

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值