问题
自己实现的 UserDetailsService
中 loadUserByUsername()
方法中抛出了 UsernameNotFoundException
,在全局异常处理器中进行了捕获和处理,但是为什么没有用
自定义 UserDetailsService
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
SysUserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库中查
SysUser sysUser = userService.selectUserByUsername(username);
if (sysUser == null) {
log.info("登录用户:{} 不存在.", username);
// 抛出异常 UsernameNotFoundException,注意我的错误信息,之后看结果比较
throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
}
// LoginUser实现了UserDetails接口,这个不是重点,不用管
LoginUser loginUser = new LoginUser(sysUser, new ArrayList<>());
return loginUser;
}
}
配置类中进行配置
@EnableWebSecurity
// 开启方法上权限控制的注解支持
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自己的 校验逻辑
auth.userDetailsService(userDetailsService);
}
}
全局异常处理器
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 用户名不存在异常
*/
@ExceptionHandler(UsernameNotFoundException.class)
public R userNameNotFoundExceptionHandler(UsernameNotFoundException e) {
// 这里拿到的异常信息应该是上面跑出来的,也就是类似 ‘登录用户 xxx 不存在’
log.error(e.getMessage());
return R.error(e.getMessage());
}
实际测试结果
这个结果绝对不是我写的,哪里来的英文???

源码分析
在上一篇博客中,我们已经分析了 AuthenticationManager
实际上调用的是 DaoAuthentionProvider
的 authentication
方法,在这个方法里面实际调用的父类 AbstractUserDetailsAuthenticationProvider
的 authention
方法,我们再来看看这个类和这个方法。
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
// authentication
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// ....
try {
// 1. 这个 retrieveUser ,里面实际拿到了容器中的 UserDetailsService。也就是我们自己实现的,并调用了它的 loadUserByUsername 方法,在这里面我们的异常被抛出来了
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
=========================================================================================
// 看这里
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
// 还有这里
if (hideUserNotFoundExceptions) {
// 还有这里
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
=========================================================================================
else {
throw notFound;
}
}
好家伙,我把异常抛出来,你给我捕获了,抛一个新的????
当然这里有个判断条件就是 if (hideUserNotFoundExceptions)
从名字就能看出来,是否要隐藏 用户不存在异常,那么它的值能够能改,我点一下。
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
// 直接写死,就要隐藏,妙啊!
protected boolean hideUserNotFoundExceptions = true;
解决办法
这又没绑定配置文件,改肯定是改不了的,但也不是没有办法,我们知道这个异常肯定是在 AuthenticationManager
调用 authentication() 的过程中发生中,而这个方法我们一般是手动调用的,除非你只使用默认配置,但前后端分离,你绝对要写自己的逻辑,我们只要给这个调用过程加上一个 try catch
,捕获一下
BadCredentialsException
异常,再自定义处理不就好了吗?
比如我们自己实现 UsernamePasswordAuthenticationFilter
,这个过滤器是干嘛的相信大家都有了解,我就不多说了。
@Component
public class MyLoinFilter extends UsernamePasswordAuthenticationFilter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
String username = req.getParameter("username");
String password = req.getParameter("password");
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, password);
try {
// 进行校验逻辑
super.getAuthenticationManager().authenticate(auth);
} catch (AuthenticationException e) {
// 被隐藏起来的用户不存在异常
if (e instanceof BadCredentialsException) {
// 我们一般不给前端提示的那么清楚
throw new RuntimeException("用户名或密码错误!");
}
throw e;
}
chain.doFilter(req, res);
}
}
然后把它配置容器中,替换原来的 。
@autowired
MyLoginFilter myLoginFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAt(myLoginFilter, UsernamePasswordAuthenticationFilter.class);
再次测试
完美!

提问与解答
提问
有人可能会说,我在自己的 UserDeteilsService
中 抛出 UserNotExist
异常会被catch到,然后被隐藏。那我给它抛的大一点,我直接给它抛出一个Exception
你还能捕获到?也就是这么写
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
SysUserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库中查
SysUser sysUser = userService.selectUserByUsername(username);
if (sysUser == null) {
// 我直接抛出一个大的
throw new Exception("登录用户:" + username + " 不存在");
}
// ...
}
回答
实际上,它还是能够被隐藏起来的。回顾一下刚才的代码
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
// authentication
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// ....
try {
// 1. 这个 retrieveUser ,里面实际拿到了容器中的 UserDetailsService。
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}// 看这里
catch (UsernameNotFoundException notFound) {
// ...
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
}
我们刚才没有看 retrieveUser
这个方法,现在来看一下
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 确实是拿到了 UserDetailsService,调用它的 loadUserByUsername 方法,就会
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
=========================================================================================
// 异常捕获,如果是 UsernameNotFoundException 也就是我门自己跑出的
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
// 它还会抛出去,所以在外面被捕获到,然后隐藏,抛出了个新的
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
// 看这里,就算你在 UserDetailsService 抛出的是 Exception,也会被捕获
catch (Exception ex) {
// 熟悉的配方,熟悉的味道,又给你来了个新的,但是这次它并没有重新设置错误消息,所以说,这个办法其实也可以。
// InternalAuthenticationServiceException extends AuthenticationServiceException
// AuthenticationServiceException extends AuthenticationException
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
总结
虽然说抛出一个更大的异常也被隐藏为 AuthenticationException
了,但是这次并没有改变你自己的异常信息,所以这个方法其实也可以。
总结
所以说,AbstractUserDetailsAuthenticationProvider
类设置的那个属性 hideUserNotFoundExceptions
还真是和名字匹配,就是只隐藏 UserNotFoundException
,重新抛出异常,修改异常信息为 “Bad Credentials”,至于其他的异常,虽然也捕获了,抛出了新的,但是没有修改你的异常信息,所以不受影响,你只需要设置相应的捕获方法就好了。