User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Spring   ›   SpringSecurity   ›   正文
SpringSecurity

SpringSecurity—区分登录失败异常

2021-10-29 17:10:23
1604  0 0
参考目录 隐藏
1) 1.源码分析
2) 3. 登录流程
3) 4. 思路分析
4) 5. 具体实践

阅读完需:约 7 分钟

当我们登录失败的时候,可能用户名写错,也可能密码写错,但是出于安全考虑,服务端一般不会明确提示是用户名写错了还是密码写错了,而只会给出一个模糊的用户名或者密码写错了。

然而对于很多程序员而言,可能并不了解这样一些“潜规则”,可能会给用户一个明确的提示,明确提示是用户名写错了还是密码写错了。

为了避免这一情况,Spring Security 通过封装,隐藏了用户名不存在的异常,导致开发者在开发的时候,只能获取到 BadCredentialsException,这个异常既表示用户名不存在,也表示用户密码输入错误。Spring Security 这样做是为了确保我们的系统足够安全。

然而由于种种原因,有时候我们又希望能够分别获取到用户不存在的异常和密码输入错误的异常,这个时候就需要我们对 Spring Security 进行一些简单的定制了。

1.源码分析

首先我们要先找到问题发生的原因,发生的地方。

在 Spring Security 中,负责用户校验的工作的类有很多,涉及到的关键类 AbstractUserDetailsAuthenticationProvider。

这个类将负责用户名密码的校验工作,具体在 authenticate 方法里边,这个方法本来特别长,我这里只把和本文相关的代码列出来:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		try {
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (UsernameNotFoundException ex) {
			if (!this.hideUserNotFoundExceptions) {
				throw ex;
			}
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}
}

retrieveUser 方法就是根据用户登录输入的用户名去查找用户,如果没找到,就会抛出一个 UsernameNotFoundException,这个异常被 catch 之后,会首先判断是否要隐藏这个异常,如果不隐藏,则原异常原封不动抛出来,如果需要隐藏,则抛出一个新的 BadCredentialsException 异常,BadCredentialsException 异常从字面理解就是密码输入错误的异常。

所以问题的核心就变成了 hideUserNotFoundExceptions 变量了。

这是一个 Boolean 类型的属性,默认是 true,AbstractUserDetailsAuthenticationProvider 也为该属性提供了 set 方法:

public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
	this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
}

看起来修改 hideUserNotFoundExceptions 属性并不难!只要找到 AbstractUserDetailsAuthenticationProvider 的实例,然后调用相应的 set 方法就能修改了。

现在问题的核心变成了从哪里获取 AbstractUserDetailsAuthenticationProvider 的实例?

看名字就知道,AbstractUserDetailsAuthenticationProvider 是一个抽象类,所以它的实例其实就是它子类的实例,子类是谁?当然是负责用户密码校验工作的 DaoAuthenticationProvider。

这个知识点先记住,我们一会会用到。

3. 登录流程

为了弄明白这个问题,我们还需要搞懂 Spring Security 一个大致的认证流程,这个也非常重要。

首先大家知道,Spring Security 的认证工作主要是由 AuthenticationManager 来完成的,而 AuthenticationManager 则是一个接口,它的实现类是 ProviderManager。简而言之,Spring Security 中具体负责校验工作的是 ProviderManager#authenticate 方法。

但是校验工作并不是由 ProviderManager 直接完成的,ProviderManager 中管理了若干个 AuthenticationProvider,ProviderManager 会调用它所管理的 AuthenticationProvider 去完成校验工作,如下图:

另一方面,ProviderManager 又分为全局的和局部的。

当我们登录的时候,首先由局部的 ProviderManager 出场进行用户名密码的校验工作,如果校验成功,那么用户就登录成功了,如果校验失败,则会调用局部 ProviderManager 的 parent,也就是全局 ProviderManager 去完成校验工作,如果全局 ProviderManager 校验成功,就表示用户登录成功,如果全局 ProviderManager 校验失败,就表示用户登录失败,如下图:

OK,有了上面的知识储备,我们再来分析一下我们想要抛出 UsernameNotFoundException 该怎么做。

4. 思路分析

首先我们的用户校验工作在局部的 ProviderManager 中进行,局部的 ProviderManager 中管理了若干个 AuthenticationProvider,这若干个 AuthenticationProvider 中就有可能包含了我们所需要的 DaoAuthenticationProvider。那我们是否需要在这里调用 DaoAuthenticationProvider 的 setHideUserNotFoundExceptions 方法完成属性的修改呢?

不需要!为什么?

因为当用户登录的时候,首先去局部的 ProviderManager 中去校验,如果校验成功当然最好;如果校验失败,并不会立马抛出异常,而是去全局的 ProviderManager 中继续校验,这样即使我们在局部 ProviderManager 中抛出了 UsernameNotFoundException 也没用,因为最终这个异常能不能抛出来决定权在全局 ProviderManager 中(如果全局的 ProviderManager 所管理的 DaoAuthenticationProvider 没做任何特殊处理,那么局部 ProviderManager 中抛出来的 UsernameNotFoundException 异常最终还是会被隐藏)。

所以,我们要做的就是获取全局的 ProviderManager,进而获取到全局 ProviderManager 所管理的 DaoAuthenticationProvider,然后调用其 setHideUserNotFoundExceptions 方法修改相应属性值即可。

弄明白了原理,代码就简单了。

5. 具体实践

全局 ProviderManager 的修改在 WebSecurityConfigurerAdapter#configure(AuthenticationManagerBuilder) 类中,这里配置的 AuthenticationManagerBuilder 最终用来生成全局的 ProviderManager,所以我们的配置如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
        userDetailsService.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin").build());
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        auth.authenticationProvider(daoAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .failureHandler((request, response, exception) -> System.out.println(exception))
                .permitAll();

    }

}

这里的代码就简单了:

  1. 创建一个 DaoAuthenticationProvider 对象。
  2. 调用 DaoAuthenticationProvider 对象的 setHideUserNotFoundExceptions 方法,修改相应的属性值。
  3. 为 DaoAuthenticationProvider 配置用户数据源。
  4. 将 DaoAuthenticationProvider 设置给 auth 对象,auth 将用来生成全局的 ProviderManager。
  5. 在另一个 configure 方法中,我们就配置一下登录回调即可,登录失败的时候,打印异常信息看看。

接下来启动项目进行测试。输入一个错误的用户名,可以看到 IDEA 控制台会打印出如下信息:

UsernameNotFoundException 异常已经抛出来了

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

0 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 594185浏览

随机文章
SpringCloud—Gateway(四)Filter(过滤器)
5年前
Kotlin-反射—反射(二十九)
4年前
SpringMVC—Web九大组件之ViewResolver
3年前
SpringBoot—三种跨域场景总结
4年前
Spring—RestTemplate几种常见的请求方式
5年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 594185 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付