关于自己实现的 UserDetailsService 中 loadUserByUsername() 方法中抛出的异常被隐藏

问题

自己实现的 UserDetailsServiceloadUserByUsername() 方法中抛出了 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());
    }
实际测试结果

这个结果绝对不是我写的,哪里来的英文???

image-20210726132807933
源码分析

上一篇博客中,我们已经分析了 AuthenticationManager 实际上调用的是 DaoAuthentionProviderauthentication 方法,在这个方法里面实际调用的父类 AbstractUserDetailsAuthenticationProviderauthention方法,我们再来看看这个类和这个方法。

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);
再次测试

完美!

image-20210726135205875
提问与解答
提问

有人可能会说,我在自己的 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”,至于其他的异常,虽然也捕获了,抛出了新的,但是没有修改你的异常信息,所以不受影响,你只需要设置相应的捕获方法就好了。

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
UserDetailsService方法抛出异常时,可以在控制器捕获该异常,并根据异常类型跳转到指定的画面。 例如,假设在UserDetailsService方法抛出UsernameNotFoundException异常,可以在控制器添加一个异常处理器来捕获该异常,然后根据异常类型跳转到指定的画面,示例如下: ```java @ControllerAdvice public class ExceptionHandlerController { @ExceptionHandler(UsernameNotFoundException.class) public String handleUsernameNotFoundException() { return "login-error"; // 跳转到登录错误页面 } } ``` 在上述代码,使用@ControllerAdvice注解标记该类为全局异常处理器,在该类定义一个handleUsernameNotFoundException方法来处理UsernameNotFoundException异常,该方法返回一个字符串类型的值,表示要跳转的页面。在本例,返回的是"login-error",表示要跳转到登录错误页面。 需要注意的是,如果使用了Spring Security框架,可以在WebSecurityConfigurerAdapter配置类设置登录失败时的跳转页面,如下所示: ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // ...省略其他配置代码... @Override protected void configure(HttpSecurity http) throws Exception { http .formLogin() .loginPage("/login") .failureUrl("/login-error") // 设置登录失败时的跳转页面 .permitAll() .and() .authorizeRequests() .antMatchers("/", "/home").permitAll() .anyRequest().authenticated(); } } ``` 在上述代码,使用了formLogin()方法来配置表单登录,其设置了登录页面为"/login",登录失败时的跳转页面为"/login-error"。这样,在UserDetailsService方法抛出UsernameNotFoundException异常时,如果用户在登录页面输入的用户名不存在,就会跳转到"/login-error"页面。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值