SpringSecurity 认证流程源码详细解读

news2024/9/23 13:30:37

一、SpringSecurity 本质探寻

SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这样说肯定非常枯燥,所以接下来还是在代码中看一看。

前期工作,需要在代码中引入 SpringSecurity 依赖,这里不再赘述,直接 debug ,如下图。

在这里插入图片描述

然后点击箭头指向的图标,输入 run.getBean(DefaultSecurityFilterChain.class),如下图所示。

在这里插入图片描述

这个 run其实就是 Spring 容器,这里面存放了 SpringBoot 自动配置加载的各种 Bean 实例,SpringBoot 自动配置过程的源码解读可以参考我的另一篇文章。

这里,我们需要获取 DefaultSecurityFilterChain.class,这正是 SpringSecurity 的过滤器链。

而我们要探讨的认证过程的主角则是 :

  1. UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求,也就是负责校验登录表单中的用户名和密码。

  2. ExceptionTranslationFilter:处理过滤器链中抛出的任何 AccessDeniedException和AuthenticationException 。

  3. FilterSecurityInterceptor:负责权限校验的过滤器。

其他的过滤器大家可以自己查阅相关资料学习。

二、SpringSecurity 认证流程源码解读

请添加图片描述

这张图基本涵盖了 SpringSecurity 整个认证过程,接下来我们将结合这张图,一步步进行分析。

在这里插入图片描述

首先,UsernamePasswordAuthenticationFilter 过滤器会获取用户输入的用户名和密码,然后封装成一个 UsernamePasswordAuthenticationToken 对象,然后会调用 authenticationManager.authenticate(authenticationToken),我们继续进入 authenticate() 方法。

在这里插入图片描述

点进入去之后,我们来到了 AuthenticationManager 接口,但这只是一个接口,显然不是我们要的,具体的实现代码应该在实现类中,所以我们继续选择 ProviderManager ,他是 AuthenticationManager 接口的一个实现类。

下面是 ProviderManager 实现类对 AuthenticationManager 接口的 authenticate() 方法的重写,乍一看代码还是很多的,不过大家不用担心,我们只需要看重点代码即可。

@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		int currentPosition = 0;
		int size = this.providers.size();
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
						provider.getClass().getSimpleName(), ++currentPosition, size));
			}
			try {
				// 委托 AbstractUserDetailsAuthenticationProvider 类的 authenticate() 方法进行认证
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
				prepareException(ex, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw ex;
			}
			catch (AuthenticationException ex) {
				lastException = ex;
			}
		}
		if (result == null && this.parent != null) {
			// Allow the parent to try.
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException ex) {
				parentException = ex;
				lastException = ex;
			}
		}
		if (result != null) {
			if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}
			// If the parent AuthenticationManager was attempted and successful then it
			// will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent
			// AuthenticationManager already published it
			if (parentResult == null) {
				this.eventPublisher.publishAuthenticationSuccess(result);
			}

			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).
		if (lastException == null) {
			lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
					new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
		}
		// If the parent AuthenticationManager was attempted and failed then it will
		// publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
		// parent AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}
		throw lastException;
	}

为了大家观看方便,这张图我就多引用几次。

我们把目光再次回到这张图,此时我么已经到了第二个阶段,即 ProviderManager 实现类的 authenticate() 方法实际上会调用 AbstractUserDetailsAuthenticationProvider 类的 authenticate() 方法进行认证。看到这里大家可能会想这不是套娃吗?没错,他就是在套娃!

请添加图片描述

在这里插入图片描述
如上图,ProviderManager 实现类重写的 authenticate() 方法实际上会调用 AbstractUserDetailsAuthenticationProvider 类的 authenticate() 方法进行认证,在上面的源码中我也进行了注释。

我们继续进入 AbstractUserDetailsAuthenticationProvider 类的 authenticate() 方法。点进去之后,我们发现我们进入了 AuthenticationProvider 接口,我们找到他的实现类——AbstractUserDetailsAuthenticationProvider。
在这里插入图片描述

AbstractUserDetailsAuthenticationProvider 类的 authenticate() 方法源码如下:

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
		String username = determineUsername(authentication);
		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException ex) {
				this.logger.debug("Failed to find user '" + username + "'");
				if (!this.hideUserNotFoundExceptions) {
					throw ex;
				}
				throw new BadCredentialsException(this.messages
						.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
			}
			Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
		}
		try {
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException ex) {
			if (!cacheWasUsed) {
				throw ex;
			}
			// There was a problem, so try again after checking
			// we're using latest data (i.e. not from the cache)
			cacheWasUsed = false;
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		this.postAuthenticationChecks.check(user);
		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

我们重点看一下 retrieveUser() 方法。
在这里插入图片描述

点进去,发现这是 AbstractUserDetailsAuthenticationProvider 接口中的方法,而且只有一个实现,那我们继续进入实现类实现的方法。

在这里插入图片描述
这个实现类正是 DaoAuthenticationProvider 类。
在这里插入图片描述

总的来说这个 retrieveUser() 方法只做了一件事,就是调用 UserDetailsService 接口的 loadUserByUsername() 方法,拿到用户信息。
在这里插入图片描述

在这里插入图片描述

这个注释真是太给力了,直白。

请添加图片描述

我们把目光再次回到这张图,此时我们已经来到 UserDetailsService 接口这里。这个接口只有 loadUserByUsername() 方法这一个方法,这个方法默认会从内存中回去用户信息,也就是下图中的 InMemoryUserDetailsManager 实现类中的方法。但是一般情况下我们的用户数据都是存放在数据库中,所以我们一般会实现 UserDetailsService 接口,然后重写 loadUserByUsername() 方法,从数据库中差寻用户信息。

在这里插入图片描述

loadUserByUsername() 方法会将查询到的用户信息封装到一个 UserDetails 对象,然后将 UserDetails 对象返回。

在这里插入图片描述
此时,我们又回到了 AbstractUserDetailsAuthenticationProvider类的 authenticate() 方法,这时我们已经从数据库拿到了用户信息(用户名、密码等)。

我们继续往下看,AbstractUserDetailsAuthenticationProvider类的 authenticate() 方法又调用了 additionalAuthenticationChecks() 方法,参数分别是从数据库中获取到的用户信息,以及根据用户在前端输入的用户信息封装成的 authentication 对象。
在这里插入图片描述

点进去发现这是 AbstractUserDetailsAuthenticationProvider 接口中的方法,而且只有一个实现,那我们继续进入实现类实现的方法。
在这里插入图片描述
这个实现类正是 DaoAuthenticationProvider 类。

在这里插入图片描述

总的来说,这个方法就做了一件事,将用户输入的密码和从数据库中查到的密码进行比较,如果一致就什么都不做,不一致就抛出异常。
在这里插入图片描述

密码的比较则是通过 PasswordEncoder 接口的 matches() 方法实现的。
在这里插入图片描述
所以,如果你想自定义加密和解密的方式只需要自己实现 PasswordEncoder 接口,然后重写 encode() 方法 和 matches() 方法。
在这里插入图片描述
当然,我们也可以在 SpringSecurity 配置类中指定加密方式。

在这里插入图片描述

密码校验完成之后,我们就来到了 AbstractUserDetailsAuthenticationProvider 类的 authenticate()方法的末尾。

在这里插入图片描述
进入 createSuccessAuthentication() 方法。

在这里插入图片描述

这个方法就是将 UserDetails 中的权限信息封装到 Authentication 对象中返回,方便后续权限校验,但是这个权限信息这里也可以为空,不一定非要在这里设置。

请添加图片描述

我们把目光再次回到这张图,这时候我们其实已经又回到了最开始的地方,ProviderManager 类 authenticate() 方法,没错我们终于跳出来了。

result = provider.authenticate(authentication) 这一行代码把我们折腾了这么就,终于出来了。

在这里插入图片描述

再次回顾 ProviderManager 类 authenticate() 方法的源码,可以发现 result = provider.authenticate(authentication) 这行代码之后都是围绕 result 这个放回的 Authentication 对象的一些校验工作,这都是一些细节,不影响大家对认证流程的理解。如果这些校验都通过,就会将 result 返回。

@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		int currentPosition = 0;
		int size = this.providers.size();
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
						provider.getClass().getSimpleName(), ++currentPosition, size));
			}
			try {
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
				prepareException(ex, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw ex;
			}
			catch (AuthenticationException ex) {
				lastException = ex;
			}
		}
		if (result == null && this.parent != null) {
			// Allow the parent to try.
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException ex) {
				parentException = ex;
				lastException = ex;
			}
		}
		if (result != null) {
			if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}
			// If the parent AuthenticationManager was attempted and successful then it
			// will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent
			// AuthenticationManager already published it
			if (parentResult == null) {
				this.eventPublisher.publishAuthenticationSuccess(result);
			}

			return result;
		}

在这里插入图片描述
所以,我们最终又回到了这里,authenticate 对象正是返回的 result。认证通过后,我们就可以继续我们写我们的其他业务代码,如封装响应信息返回给前端,告知用户认证成功。

好了,最后,感谢大家观看,如果有疑问或者文章有错误,可以在评论区@我。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/505416.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

服务(第十七篇)mysql的高级语句

mysql 6大常见的约束&#xff1a; 主键约束&#xff1a;primay key 主键字段不允许有重复的记录&#xff0c;不允许为null&#xff0c;一个表只能有一个主键 唯一性约束&#xff1a;unique key 唯一键字段不允许有重复的记录&#xff0c;但允许为null&#xff0c;一…

unordered系列容器的底层——哈希

目录 unordered系列容器的底层结构 哈希概念 哈希冲突 哈希函数 常见哈希函数 哈希冲突解决 闭散列 线性探测 哈希表什么情况下进行扩容&#xff1f;如何扩容&#xff1f; 二次探测 开散列 开散列概念 开散列增容 存储string类型的解决 闭散列的实现 开散列的实…

IS210AEBIH3BEC隔离器用于变压器等高压设备

IS210AEBIH3BEC隔离器用于变压器等高压设备 隔离器可以根据在电力系统中的位置进行分类 母线侧隔离器——隔离器直接连接到主母线线路侧隔离器 - 隔离器将放置在任何馈线的线路侧Transfer bus side isolator – isolator will be directly connected with the transfer bus S…

python自动化爬虫实战

python自动化爬虫实战 偶然的一次机会再次用到爬虫&#xff0c;借此机会记录一下爬虫的学习经历&#xff0c;方便后续复用。 需求&#xff1a;爬取网站数据并存入的csv文件中&#xff0c;总体分为两步 爬取网站数据存到到csv文件中 1、配置爬虫环境 1.1、下载自动化测试驱动 …

基于transformer的Seq2Seq机器翻译模型训练、预测教程

前言 机器翻译&#xff08;Machine Translation, MT&#xff09;是一类将某种语言&#xff08;源语言&#xff0c;source language&#xff09;的句子 x x x翻译成另一种语言&#xff08;目标语言&#xff0c;target language&#xff09;的句子 y y y 的任务。机器翻译的相关…

深度学习 - 46.DIN 深度兴趣网络

目录 一.引言 二.摘要 ABSTRACT 三.介绍 INTRODUCTION 1.CTR 在广告系统的作用 2.传统 MLP 存在的问题 3.DIN 的改进 四.近期工作 RELATEDWORK 1.传统推荐算法 2.用户行为抽取 五.背景 BACKGROUD 六.深度兴趣网络 DEEP INTEREST NETWORK 1.特征表示 Feature Repres…

ChatGPT的强化学习部分介绍——PPO算法实战LunarLander-v2

PPO算法 近线策略优化算法&#xff08;Proximal Policy Optimization Algorithms&#xff09; 即属于AC框架下的算法&#xff0c;在采样策略梯度算法训练方法的同时&#xff0c;重复利用历史采样的数据进行网络参数更新&#xff0c;提升了策略梯度方法的学习效率。 PPO重要的突…

Leecode101 ——对称二叉树

对称二叉树:Leecode 101 leecode 101 对称二叉树 根据题目描述&#xff0c;首先想清楚&#xff0c;对称二叉树要比较的是哪两个节点。对于二叉树是否对称&#xff0c;要比较的是根节点的左子树与根节点的右子树是不是相互翻转的&#xff0c;其实也就是比较两个树&#xff0c;…

A+CLUB活动预告 | 2023年5月

五月立夏已至&#xff0c;万物并秀。 ACLUB五月活动预告新鲜出炉&#xff0c;本月我们将会有2场峰会活动&#xff08;深圳&#xff09;、1场沙龙活动&#xff08;深圳&#xff09;和1场管理人支持计划&#xff08;上海&#xff09;&#xff0c;期待您的参与&#xff01; 注&a…

06- 算法解读 Fast R-CNN (目标检测)

要点&#xff1a; Fast R-CNN 属于 Two-stage detector 回归损失参考&#xff1a;https://www.cnblogs.com/wangguchangqing/p/12021638.html 二 Fast R-CNN算法 Fast R-CNN 是作者 Ross Girshick 继 R-CNN 后的又一力作。同样使用 VGG16 作为网络的 backbone &#xff0c; …

您应该使用WhatsApp电子商务的3个理由

随着WhatsApp进军电子商务领域并扩大其为企业提供的服务种类&#xff0c;它正迅速成为客户参与的完美工具和对话式商务的绝佳平台。使用WhatsApp作为主要渠道向客户销售商品和服务的前景产生了WhatsApp电子商务一词。 WhatsApp已被加冕为世界领先的消息传递平台。对于最终用户来…

二十三、SQL 数据分析实战(10个简单的SQL题目)

文章目录 题目1: 比赛名单整理题目2: 热门游戏排行题目3: 社区生鲜App覆盖分析题目4: 社区团购行为分析题目5: 统计字符出现次数题目6: 找出各类别商品销量最高的商品题目7: 找出每个部门薪资第二高的员工题目8: 游戏玩家登录情况分析题目9: 用户首单消费金额题目10: 参与优惠活…

这可能是你读过最透彻的TCC方案讲解|分布式事务系列(三)

本文从两个场景说起&#xff0c;详细描述了TCC的详细过程&#xff0c;以及对比2PC有什么区别&#xff0c;适用什么样的场景。 点击上方“后端开发技术”&#xff0c;选择“设为星标” &#xff0c;优质资源及时送达 在面试前复习 TCC 的时候你是不是这样做的&#xff1a;百度TC…

百度再掀智能手机风云:推出小度AI智能手机

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 太突然了!百度进军手机市场&#xff0c;据报百度也要进军手机市场了。5月底发布首款AI智能手机。目前这款新机处于发布前的最后准备阶段。 这款智能手机将采用百度的AI技术&#xff0c;预计会具备…

react如何渲染包含html标签元素的字符串

如何渲染包含html标签元素的字符串 最近有个搜索替换的需求&#xff0c;用户可以输入关键字信息来匹配出对应的数据&#xff0c;然后对其关键字进行标记显示&#xff0c;如下图所示&#xff1a; 实现上面的需求的思路就是前端去判断检索内容&#xff0c;将内容中对应的关键字…

又失眠了!

下班晚&#xff0c;洗漱之后&#xff0c;就这个点了&#xff0c;睡不着&#xff0c;爬上来和大家随意 BB 几句。 一个人成长的过程&#xff0c;也是自我认同感不断增强的过程&#xff0c;在这个过程中&#xff0c;一个稳定的精神内核不断夯实&#xff0c;你不会为谁的贬低而诚惶…

Linux性能监控与调优工具

Linux性能监控与调优工具 文章目录 Linux性能监控与调优工具1.使用top、 vmstat、 iostat、 sysctl等常用工具2.使用高级分析手段&#xff0c; 如OProfile、 gprof4.使用LTP进行压力测试5.使用Benchmark评估系统 除了保证程序的正确性以外&#xff0c; 在项目开发中往往还关心性…

体验讯飞星火认知大模型,据说中文能力超越ChatGPT

&#x1f4cb; 个人简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是阿牛&#xff0c;全栈领域优质创作者。&#x1f61c;&#x1f4dd; 个人主页&#xff1a;馆主阿牛&#x1f525;&#x1f389; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4d…

Nature | 生成式人工智能如何构建更好的抗体

疫情高峰期&#xff0c;研究人员竞相开发一些首批有效的COVID-19治疗方法&#xff1a;从已经康复的人的血液中分离出来的抗体分子。 现在&#xff0c;科学家已经证明&#xff0c;生成式人工智能&#xff08;AI&#xff09;可以通过一些繁琐的过程提供捷径&#xff0c;提出增强抗…

代码随想录算法训练营day29 | 491.递增子序列,46.全排列,47.全排列 II

代码随想录算法训练营day29 | 491.递增子序列&#xff0c;46.全排列&#xff0c;47.全排列 II 491.递增子序列解法一&#xff1a;回溯&#xff08;map进行数层去重&#xff09;解法二&#xff1a;回溯&#xff08;仅针对本题&#xff0c;不具有普适性&#xff09; 46.全排列解法…