问答

security 的loadUserByUsername这个方法重写的问题

作者:admin 2021-06-21 我要评论

在使用security 做登录授权的时候,有一个接口UserDetailsService,通过实现该接口可以重写loadUserByUsername做用户校验,但是有个问题就是,当用户输入的账号...

在说正事之前,我要推荐一个福利:你还在原价购买阿里云、腾讯云、华为云服务器吗?那太亏啦!来这里,新购、升级、续费都打折,能够为您省60%的钱呢!2核4G企业级云服务器低至69元/年,点击进去看看吧>>>)

在使用security 做登录授权的时候,有一个接口UserDetailsService,通过实现该接口可以重写loadUserByUsername做用户校验,但是有个问题就是,当用户输入的账号不存在,怎么提示信息给前端,貌似只有输入用户密码错误框架才会返回:
{

"error": "invalid_grant",

"error_description": "用户名或密码错误"

}
如果

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         // 首先判断username是否存在
        // 通过传进来的 username 去数据库查找用户信息,这里模拟
//        User userInfo = userService.findUserByUserName(username);
     if(userInfo == null){
         return null;  // 返回null 直接报500错误,怎么才能返回"用户名或密码错误"的提示,
     }      
//        String userPasswordDb = userInfo.getPassword();
           //Integer userId  = userInfo.getId();
      // 线上环境应该通过用户名查询数据库获取用户加密后的密码
        String userPasswordDb = passwordEncoder.encode("123456");
        // 根据用户名查询出来的用户角色权限列表
        String permissions = "sys_user_edit,sys_user_del,sys_user_add";

        CpmsUser userDetails = new CpmsUser(userId,username,userPasswordDb,AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));

        return userDetails;
    }
###

首先,在不为security加入自定义过滤器的前提下,只能给出:用户名或密码错误 的提示,没有办法细化到:用户名错了 或是 密码错了的提示。

其次,如果只是想使loadUserByUsername返回类似用户名或密码错误的提示,我认同楼上的方法,直接抛出异常即可:

     User userInfo = userService.findUserByUserName(username);
     if(userInfo == null){
         throw new UsernameNotFoundException();
     }      

此时,前台将得到了一个401错误。表示:用户名或密码错误。


我简单说下为什么返回null会发生500,而抛出UsernameNotFoundException则会返回401.
AbstractUserDetailsAuthenticationProvider中有这么几行核心代码:

            try {
                // 获取当前登录用户,该方法将调用我们自定义的loadUserByUsername方法。
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException notFound) {
                if (hideUserNotFoundExceptions) {
                    // 由于hideUserNotFoundExceptions的默认值为true
                    // 所以若接收到UsernameNotFoundException则抛出该异常,则抛出BadCredentialsException的异常。
                    // 这正是会提示401的原因
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                }
                else {
                    throw notFound;
                }
            }

然后另一个核心文件:DaoAuthenticationProvider中有这么几行核心代码:

        try {
            // 调用我们的loadUserByUsername方法
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                // 这是关键,如果loadUserByUsername方法返回null,则将抛出InternalAuthenticationServiceException异常
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            // 该异常直接向上抛出InternalAuthenticationServiceException
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }

最后,在BasicAuthenticationFilter又有这么几行代码:

        try {
            // 省略部分代码

            if (authenticationIsRequired(username)) {
                // 以下语句完成认证:
                // 1. 未发生异常,说明认证成功。继续向下执行
                // 2. 发生BadCredentialsException异常,不能够catch接收。异常向上抛出,最终被Sring获取,返回401
                // 3. 发生InternalAuthenticationServiceException,该异常继承于AuthenticationException。将被catch接收。
                // catch中清空了用户登录信息后直接返回,(猜想,未跑demo)近而在后续的语句执行中发生了500异常。
                Authentication authResult = this.authenticationManager
                        .authenticate(authRequest);
                // 未发生异常,用户名密码认证成功
            }

        }
        catch (AuthenticationException failed) {
            // 清空登录信息后直接return
           
            return;
        }

最后再说一下为什么我们无法分辨出是:用户名不存在或密码错误。
DaoAuthenticationProvider有以下几行是验证密码是否正确的:

        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            logger.debug("Authentication failed: password does not match stored value");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }

如上所示:如果密码未匹配成功,同样会抛出BadCredentialsException异常。既然用户名不存在与密码错误同样抛出的是BadCredentialsException异常,而Spring Security处理BadCredentialsException的方法又是统一的,所以得到401实际上只能代表接收到了BadCredentialsException异常,而该异常是由用户名错误引起的,或是由密码错误引起的,就无从得知了。

总结:Spring Security是个相对复杂的工程,水平有限,相信也没能讲明白,取其精华去其糟粕吧。

###

throw new UsernameNotFoundException();

###

原因.不抛出springsecurity系列的异常,比如UsernameNotFoundException,BadCredentialsException,CredentialsException等异常。否则会在springsecurity里统一成“认证失败"的异常信息,就无法区分用户不存在,密码错误,还是用户状态不对的情况。
解决办法:
1.我抛的是空指针异常,上层捕获,返回前端用户不存在,然后密码错误是在DaoAuthenticationProvider的自定义子类方法中抛出new BadCredentialsException("密码不正确!")。
2.你当然可以抛出自定义一系列异常,也能达到目的。

版权声明:本文转载自网络,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。本站转载出于传播更多优秀技术知识之目的,如有侵权请联系QQ/微信:153890879删除

相关文章
  • security 的loadUserByUsername这个方

    security 的loadUserByUsername这个方

  • flutter 使用 dio 上传文件 找不到路径

    flutter 使用 dio 上传文件 找不到路径

  • make_pair为什么不支持传递左值参数?

    make_pair为什么不支持传递左值参数?

  • 内嵌汇编语言关于push的一个问题?

    内嵌汇编语言关于push的一个问题?

腾讯云代理商
海外云服务器