- 问题描述
- 吐槽
- 问题解决
- 总结
使用Spring boot整合Spring security时,在自定义的UserDetailsService中,抛出UsernameNotFoundException(“用户不存在”)异常。但实际运行时,抛出了org.springframework.security.authentication.BadCredentialsException: Bad credentials。
吐槽 写个文章也不为啥,主要就是想着困扰了自己这么久,吐槽一下。当然,要是能帮到什么人,那就再好不过了。然后要是有听不惯的也可以直接喷,反正网络世界有键盘就行。然后本人并不是什么技术大佬,内容仅供参考。
遇到问题时先是去百度了一下,出来的几个问答基本也就是那么两个版本,内容可以说是一模一样好吧。毕竟这种大数据时代,咱也不知道谁爬的谁,反正互相“分享”就对了。
首先,问题的本质呢,就是为什么我抛出了UsernameNotFoundException异常,但是为什么后台报错是BadCredentialsException异常,并且异常信息是Bad credentials,并不是我自定义的文字信息。如果你觉得我说的对基本就是和我最开始一样,白给,这个思路就是一个坑,就是一个死胡同。
简述一下我自己遇到的场景,自己做一个前后端分离项目,前端无法获取错误信息,抛出了位置错误,查看后台报错,发现并没有按照我的预期抛出正确的异常,然后一直在纠结怎么才能使系统抛出UsernameNotFoundException(“用户不存在”)异常,并且让前端接受到错误信息。
但其实,问题并不出抛出的异常类型不正确,而是你的错误为什么没被捕获到,就算抛出的信息不正确,那前端也该是显示Bad credentials才对,而不该没有获取到异常信息。所以真正的问题其实是我们的自定义全局异常处理,并不能捕获到这个类抛出的异常,具体后面会讲到。
然后再吐槽一下,网上看到的两个解答版本。第一个版本是一个小天才,一路阅读源码,找到了将UsernameNotFoundException转化成BadCredentialsException抛出的那段代码判断,然后修改了Spring security的配置,使那个if判断为false,这样就可以抛出UsernameNotFoundException异常了。实测无效,但是还是挺佩服这个小天才的,源码读的挺累的吧。然后第二个版本是一个逆天哥,说既然UsernameNotFoundException无效,那我们自定义异常抛出不就行了吗。好吧我其实最开始自己也这么想过哈哈哈哈哈哈,但是实际上就是,我们抛出异常的目的,不是为了和日志一样,让后台看到正确的报错,而是要捕获到异常,给前端返回相对应的错误信息。所以这个版本虽然抛出的异常信息会正确,但实际没什么卵用。
以下摘自stack overflow。
Security layer comes before anything in the controllers and @ControllerAdvice. Hence @ControllerAdvice isn’t an option since UsernameNotFoundException which is a subclass of AuthenticationException is thrown during authenticaton, making your exception handlers in @ControllerAdvice unreachable.
You can only use @ControllerAdvice and ResponseEntityExceptionHandler if you are throwing UsernameNotFoundException inside controller or any others beans referenced from the controllers.
Here is my suggestion - that you implement AuthenticationFailureHandler and use it with AuthenticationFilter that you are using for your security configuration. Spring boot security comes with about 4 handler interfaces for security related issues
大概意思就是说,UsernameNotFoundException是在身份校验过程中被抛出的,也就是安全层,而安全层位于controller和@ControllerAdvice之前,所以我们自定义的全局异常处理器无法捕获这个异常。然后Spring boot针对安全层定义了四个处理接口,对这个异常的处理推荐使用AuthenticationFailureHandler。
当请求进来 会按照 filter -> interceptor -> controllerAdvice -> aspect -> controller的顺序调,用当controller返回异常 也会按照controller -> aspect -> controllerAdvice -> interceptor -> filter来依次抛出。所以我个人认为,其实你只要在controllerAdvice 之后捕获异常就行了,自定义拦截器和过滤器讲道理都可以,当然肯定没有用Spring boot的接口方便。
下面是处理器代码。
@Component
@Slf4j
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 这些对于操作的处理类可以根据不同异常进行不同处理
if (e instanceof UsernameNotFoundException){
log.info("【登录失败】"+e.getMessage());
RetResult.error(500,"用户名不存在");
}
if (e instanceof LockedException){
log.info("【登录失败】"+e.getMessage());
RetResult.error(500,"用户被冻结");
}
if (e instanceof BadCredentialsException){
log.info("【登录失败】"+e.getMessage());
RetResult.error(500,"用户名密码不正确");
}
RetResult.error(500,"登录失败");
}
}
然后将处理器配置到security中
//这里可以自己写@Bean,也可以和我一样依赖注入
@Autowired
private RestAuthenticationFailureHandler restAuthenticationFailureHandler;
//配置登录失败自定义处理类
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.cors();
http
.authorizeRequests()
.antMatchers("/user/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.failureHandler(restAuthenticationFailureHandler);
}
总结
会出现这个问题,大概也就是因为使用Spring boot整合Spring security时,网上随便找了个demo吧,有些可能做的十分简化或者干脆就不全(不建议使用CSDN上的教程)。一般比较全的demo的话是会自定义四个处理器的。



