《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 这篇文章,找到了解决方案。

一、前言《SpringBoot 集成 Spring Security》系列文章,原本只是我自己学习后写的笔记,没想到受到大家的欢迎,能够对大家带来帮助,让我感到十分高兴。但说起来我也只是初学者,这一系列文章中可能也存在错误,本文是为了解决 UserNotFoundException 这个异常无法抛出而写出。这个问题大致是这样的,我们知道 Spring Security 的验证处理是由某个 Pr...
HomeTaskWeek3 实现Comparable []排序(Comparable []元素)方法。 它的任务是订购对象的数组,这些对象的类实现Comparable接口。 该方法必须返回该数组的副本,而原始数组必须保持完整。 给出一些对不同对象进行排序的示例。 测试String,StringBuffer和StringBuilder串联方法的性能。 使用System.currentTimeMillis()衡量执行​​时间 实现用户auth(字符串登录名,字符串密码)会抛出AuthException方法。 User类包含两个字段:字符串登录名和字符串密码。 该方法应在“数据存储”中通过登录名和密码搜索用户,然后将其返回。 如果找不到用户,则应引发UserNotFoundException。 如果用户名或密码为空-WrongCredentialsException。 如果通过登录找到用户,
这个教程是我在往项目中一点一点添加 Spring Security的过程的一个笔记,也是我学习 Spring Security的一个过程。 在解决这个问题之前要先说一点authentication-provider默认加载的是DaoAuthenticationProvider类。 完成了上一章的内容后在测试的时候发现在UserDetailsService中抛出的UsernameNotF
以下配置基于spring boot版本1.4.2.RELEASE,默认引入的spring security版本为4.1.3.RELEASE,页面模板采用thymeleaf。 在MyUserDetailsService实现了UserDetailsService接口以后,在重写的loadUserByUsername方法里验证用户名不存在时,我们会抛出一个UsernameNotFoundExceptio...
虽然这并非一个典型需求,但是把这个问题解决了,有助于加深大家对于 Spring Security 的理解。 因此,松哥打算撸一篇文章和大家稍微聊聊这个话题。 1. 问题再现 可能有小伙伴还不明白这个问题,因此我先稍微解释一下。 当我们登录失败的时候,可能用户名写错,也可能密码写错,但是出于安全考虑,服务端一般不会明确提示是用户名写错了还是密码写 @Service public class UserDetailsServiceImpl implements UserDetailsService { private final UserService userService; @Autowired public UserDetailsServiceImpl(UserService userService) { this.userService = userS
最近项目中在使用Spring Security的token;在做退出和验证时,出现UserNotFoundException抛出问题; 当用户不存在时,只会抛出BadCredentialsException,而不是UsernameNotFoundException; 首先debug断点跟踪 进入该断点行中,根据断点信息已知 providers 一共只有1个,当前的 providers 是 DaoAuthenticationProvider protected final U..
UserDetailsService代码如下,当根据用户名查询不到用户信息时,我们抛出UsernameNotFoundException异常,按预想,只需要捕获该异常,便可在用户名错误时给用户提示 然而实际运行时,无论用户名不存在还是密码错误,均抛出BadCredentialsException异常,无法知道登录失败的具体原因,经过debug发现,正是在这个地方做了判断,当hideUserNotFoundExceptions为false时,抛出UserNotFoundException异常,而true时.
通过查看源码可得知: 1. 前言 抽象类中AbstractUserDetailsAuthenticationProvider 接口抛出异常AuthenticationException 下面源码注释这么描述 * @throws AuthenticationException if the credentials c...
这是一个基于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)。