SpringBoot 集成 Spring Security(9)——解决 UserNotFoundException 不抛出问题

一、前言

《SpringBoot 集成 Spring Security》系列文章,原本只是我自己学习后写的笔记,没想到受到大家的欢迎,能够对大家带来帮助,让我感到十分高兴。但说起来我也只是初学者,这一系列文章中可能也存在错误,本文是为了解决 UserNotFoundException 这个异常无法抛出而写出。

这个问题大致是这样的,我们知道 Spring Security 的验证处理是由某个 Provider 处理的,在 Provider 中通过对应的 UserDetailsService 的 loadUserByUsername() 来决定如何加载数据库中的用户信息。

《SpringBoot集成Spring Security(3)——异常处理》 代码为例,采用默认的用户名密码登陆方式,我们在 CustomUserDetailsService 类中,有这么一行代码:

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
	...
    // 判断用户是否存在
    if(user == null) {
        throw new UsernameNotFoundException("用户名不存在");
    }

	...
}

但是实际运行后你会发现,当用户不存在时,只会抛出 BadCredentialsException,而不是 UsernameNotFoundException,百度下后发现这个问题的人不在少数。在本文中,我将介绍为什么会无法抛出 UsernameNotFoundException,以及如何解决这个问题。

二、导致的原因是什么?

首先说明,出现这种情况只有在你使用默认的用户名密码登陆方式,且没有自定义 Provider 的情况下,才会发生!如果你有自定义 Provider,仍然出现这个问题,说明代码写的有问题,这一点在后面我会说。

由提供的源码地址中,第三章和第四章两篇文章是一个项目。分别是 springboot_security03springboot_security03_filter。由于前者是自定义 Provider 实现,因此理论上不会出现这个问题,所以我们以后者代码为例。

运行 springboot_security03_filter 项目,发现当用户不存在时,的确抛出的是 BadCredentialsException。我们给抛出异常那行代码加断点,进行调试。

@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
	...
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        ...
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");  // TODO 该行断点
        }
        ...
    }
}

给 org.springframework.security.authentication.ProviderManager,Line 174 加上断点,该行是 provider 调用 UserDetailsService 位置。

运行后,首先进入该断点行中,根据断点信息已知 providers 一共只有1个,当前的 providers 是 DaoAuthenticationProvider。

DaoAuthenticationProvider 是 Spring Security 默认的用户名密码登陆的处理 provider,符合预期。跳到下一断点处,此时进入 UserDetailsService,由于用户不存在,跳转到异常抛出处。

下面采用单步调试,如图所示,抛出异常后在 DaoAuthenticationProviderretrieveUser() 方法中被捕获。随后执行了 mitigateAgainstTimingAttack() 方法,虽然我没看出这方法有啥实际作用。但这不重要,最后将该异常又抛出去了。

被抛到了 DaoAuthenticationProvider 的父类 AbstractUserDetailsAuthenticationProvider 的 authenticate() 方法中。如图所示,在 catch 到 UsernameNotFoundException 后,有个关键的 hideUserNotFoundExceptions 变量。当 hideUserNotFoundExceptions 为true 时,在这个地方被重新包装成了 BadCredentialsException 抛出去。

检查后发现 true 为其默认值,这就是导致 UserNotFoundException 无法抛出的原因。

三、如何解决?

解决的办法也很简单,我们只要想办法把这个变量默认值改掉就好了。因此我们需要手动注入 DaoAuthenticationProvider,在注入时候把值改了。WebSecurityConfig 类改动如下:

首先自己注入下 DaoAuthenticationProvider:

@Bean
public DaoAuthenticationProvider authenticationProvider() {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setHideUserNotFoundExceptions(false);
    provider.setUserDetailsService(userDetailsService);
    provider.setPasswordEncoder(passwordEncoder());
    return provider;
}

通过调用 setHideUserNotFoundExceptions 改变其默认值,这样就 OK 啦!同时这里需要指定加密方式和 UserDetailsService,因此原本默认的全局配置 config() 方法就可以不要了,删除方法:

//    @Override
//    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        auth.userDetailsService(userDetailsService).passwordEncoder(new PasswordEncoder() {
//            @Override
//            public String encode(CharSequence charSequence) {
//                return charSequence.toString();
//            }
//
//            @Override
//            public boolean matches(CharSequence charSequence, String s) {
//                return s.equals(charSequence.toString());
//            }
//        });
//    }

这里将加密方式也注入 bean,方便调用:

 @Bean
 public PasswordEncoder passwordEncoder() {
     return new PasswordEncoder() {
         @Override
         public String encode(CharSequence charSequence) {
             return charSequence.toString();
         }

         @Override
         public boolean matches(CharSequence charSequence, String s) {
             return s.equals(charSequence.toString());
         }
     };
 }

重新运行程序,已经没毛病了:

四、勘误

这一小节的题目为“勘误”,勘的就是我前面几篇文章,以及博客示例程序中的错误。错误的发现是当我运行 springboot_security03 项目时,依然出现了这个问题。而那个项目采用的是自定义 provider,如果你已经明白这个错误的缘由后,就会知道这个错误的根本原因是 DaoAuthenticationProvider 的父类 AbstractUserDetailsAuthenticationProvider 中存在变量来控制是否将异常进行包装。

那么我自己定义 provide,跟这 DaoAuthenticationProvider 屁关系都没有,怎么也会出现这个错误呢,显然代码是有毛病的。

因此我运行 springboot_security03 后,也在 ProviderManager Line 174 行断点调试后,发现 providers 竟然是两个,DaoAuthenticationProvider 也在列。

此时我还没发现问题,我继续单步调试,自己定义的 provider 中的确把 UsernameNotFoundException 跑出去了,我正高兴了,突然发现它竟然又跳入了下一次循环,此时 provider 是 DaoAuthenticationProvider,且它通过了provider.supports() 校验,开始进入 正式执行流程了。

我暗道不好,根据前文我们知道 DaoAuthenticationProvider 默认最后把 UsernameNotFoundException 包装成了 BadCredentialsException。当 providers 循环遍历结束后,取了 lastException,并把它抛出去。

由于是先执行 CustomAuthenticationProvider 后执行 DaoAuthenticationProvider,故最终自定义的 provider 的 UsernameNotFoundException,被 DaoAuthenticationProvider 的 BadCredentialsException 给覆盖了。

发现问题了,下面开始解决问题,大致有以下三种方案。

Plan A: 我们在 provider 循环中让 CustomAuthenticationProvider 和 DaoAuthenticationProvider 掉个顺序不就好了?

咳咳,不得不说真是一个十分糟主意,虽然我没咋研究 providers 的顺序是咋生成的,暂且认为是字典序吧,我难道自定义 providrs 还得考虑首字母命名顺序吗? PASS!

Plan B: 根据《SpringBoot集成Spring Security(7)——认证流程》,我们知道如果我们要自定义一种登陆方式,那么 xxxProvider、xxxUserDetailsService、xxxToken,这三个应该是一体的。

provider 决定了调用哪个 userDetailsService,support() 方法决定了这个 provider 支持的 token。但是我在这个项目中偷了懒,我只自定义了 provider 和 userDetailsService,却没有定义专属的 token,而是使用 UsernamePasswordAuthenticationToken。

那么 UsernamePasswordAuthenticationToken 将会同时被我的 provider 以及 DaoAuthenticationProvider 所支持。那么 PlanB 就是定义一个专属的 token,跟《SpringBoot 集成 Spring Security(8)——短信验证码登录》一样。

这的确是一个好主意,而且我认为这也是 Spring Security 在非 DEMO 项目中正确的使用方式,provider + userDetailsService + token 三者捆绑。

然而这毕竟只是一个 demo,而且还是第三章的 demo,考虑到入手难度,不想引入太多的类。PASS!

Plan 3: 换个思路,这个 DaoAuthenticationProvider 是咋加进去,如果它不在 providers 循环中,不就没问题了?

修改 WebSecurityConfig 类如下图,去除了 auth .userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()) 配置,这会导致将 DaoAuthenticationProvider 加入到 providers 集合中。

我参考 https://blog.csdn.net/wzl19870309/article/details/70314085 这篇文章,找到了解决方案。

  • 12
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 19
    评论
这是一个基于Spring Boot、Spring Security、JWT和OAuth2的示例项目,实现了用户注册、登录、注销、刷新令牌、访问受保护资源等功能。 ## 技术栈 - Spring Boot 2.5.4 - Spring Security 5.5.1 - Spring Data JPA 2.5.4 - MySQL 8.0.26 - JWT 0.11.2 - OAuth2 2.5.4 - Lombok 1.18.20 ## 数据库配置 在MySQL数据库中新建一个名为`springboot_security_jwt_oauth2`的数据库,执行以下SQL语句创建用户表: ```sql CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(255) NOT NULL COMMENT '用户名', `password` varchar(255) NOT NULL COMMENT '密码', `enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用', `create_time` datetime NOT NULL COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; ``` ## 项目结构 ``` ├── src/main/java │ └── com │ └── example │ └── demo │ ├── DemoApplication.java │ ├── config │ │ ├── JwtConfig.java │ │ ├── MyPasswordEncoder.java │ │ └── SecurityConfig.java │ ├── controller │ │ ├── LoginController.java │ │ └── UserController.java │ ├── dao │ │ ├── UserRepository.java │ │ └── UserRoleRepository.java │ ├── entity │ │ ├── User.java │ │ └── UserRole.java │ ├── exception │ │ ├── JwtAuthenticationException.java │ │ └── UserNotFoundException.java │ ├── service │ │ ├── AuthService.java │ │ ├── UserService.java │ │ └── impl │ │ ├── AuthServiceImpl.java │ │ └── UserServiceImpl.java │ ├── util │ │ ├── JwtTokenUtil.java │ │ └── JwtUserDetailsService.java │ └── web │ ├── JwtAuthenticationEntryPoint.java │ ├── JwtAuthenticationFilter.java │ ├── JwtAuthorizationFilter.java │ ├── RestResponse.java │ └── UserNotFoundExceptionHandler.java └── src/main/resources ├── application.properties ├── static └── templates ``` - `config`:Spring Security和JWT的配置类 - `controller`:控制器类,处理请求和响应 - `dao`:数据访问层,使用Spring Data JPA实现 - `entity`:实体类 - `exception`:异常类 - `service`:服务层接口和实现类 - `util`:工具类,包括JWT生成和解析、用户认证等 - `web`:Web相关类,包括异常处理、JWT过滤器等 ## API文档 ### 用户注册 - URL:`/api/register` - Method:POST - Request: ```json { "username": "test", "password": "123456" } ``` - Response: ```json { "code": 200, "message": "注册成功", "data": { "id": 1, "username": "test", "password": "$2a$10$8uFJ3zZB.Sd7K3YB2K3Y/OfVhF4oJXeS3j0R2A3RG1c2UJWuXkSdC", "enabled": true, "createTime": "2021-10-01T08:16:28.000+00:00" } } ``` ### 用户登录 - URL:`/api/login` - Method:POST - Request: ```json { "username": "test", "password": "123456" } ``` - Response: ```json { "code": 200, "message": "登录成功", "data": { "accessToken": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0Iiwicm9sZXMiOiJST0xFX1VTRVIiLCJleHAiOjE2MzI5NjQwODh9.5Syf8x3CZaLl0yHrXyXjJ4Qz4jJnVR3S4yIDg6GQ6puknFkJ9QWgJzJ5pB0tZzHfrGz2K1VJvJkHrOjLUQJWzA", "tokenType": "Bearer", "expiresIn": 3600 } } ``` ### 用户注销 - URL:`/api/logout` - Method:POST - Request Header: ``` Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0Iiwicm9sZXMiOiJST0xFX1VTRVIiLCJleHAiOjE2MzI5NjQwODh9.5Syf8x3CZaLl0yHrXyXjJ4Qz4jJnVR3S4yIDg6GQ6puknFkJ9QWgJzJ5pB0tZzHfrGz2K1VJvJkHrOjLUQJWzA ``` - Response: ```json { "code": 200, "message": "注销成功" } ``` ### 刷新令牌 - URL:`/api/refresh` - Method:POST - Request Header: ``` Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0Iiwicm9sZXMiOiJST0xFX1VTRVIiLCJleHAiOjE2MzI5NjQwODh9.5Syf8x3CZaLl0yHrXyXjJ4Qz4jJnVR3S4yIDg6GQ6puknFkJ9QWgJzJ5pB0tZzHfrGz2K1VJvJkHrOjLUQJWzA ``` - Response: ```json { "code": 200, "message": "刷新令牌成功", "data": { "accessToken": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0Iiwicm9sZXMiOiJST0xFX1VTRVIiLCJleHAiOjE2MzI5NjQxMzQsImlhdCI6MTYzMjk2MDUzNH0.2hWq8dLJ7s9G6MqQ8Gg7kNvGzeOaJQFb4eBZ9RcB6N8lP3kglz8W_KXMh8r4oJZkzy5HOVZrB5YSEKNxZyY5lg", "tokenType": "Bearer", "expiresIn": 3600 } } ``` ### 获取当前用户信息 - URL:`/api/user/info` - Method:GET - Request Header: ``` Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0Iiwicm9sZXMiOiJST0xFX1VTRVIiLCJleHAiOjE2MzI5NjQxMzQsImlhdCI6MTYzMjk2MDUzNH0.2hWq8dLJ7s9G6MqQ8Gg7kNvGzeOaJQFb4eBZ9RcB6N8lP3kglz8W_KXMh8r4oJZkzy5HOVZrB5YSEKNxZyY5lg ``` - Response: ```json { "code": 200, "message": "获取用户信息成功", "data": { "id": 1, "username": "test", "password": null, "enabled": true, "createTime": "2021-10-01T08:16:28.000+00:00", "authorities": [ { "authority": "ROLE_USER" } ] } } ``` ### 获取所有用户信息 - URL:`/api/user/all` - Method:GET - Response: ```json { "code": 200, "message": "获取所有用户信息成功", "data": [ { "id": 1, "username": "test", "password": null, "enabled": true, "createTime": "2021-10-01T08:16:28.000+00:00", "authorities": [ { "authority": "ROLE_USER" } ] } ] } ``` ## 完整代码 完整代码请参考[GitHub](https://github.com/zhongshijun/springboot-security-jwt-oauth2)。
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值