Spring Security之认证过滤器

前言

上回我们探讨了关于Spring Security,着实复杂。这次咱们聊的认证过滤器就先聊聊认证功能。涉及到多方协同的功能,咱分开聊。也给小伙伴喘口气,嘻嘻。此外也是因为只有登录认证了,才有后续的更多功能集成的可能。

认证过滤器

认证过滤器是Web应用中负责处理用户认证请求的。这意味着他要验证用户的身份,确认你是谁。只有确认用户身份后,才能授予对应的权限,才能在后续访问对应的受保护资源。因此,在我看来,认证过滤器实际上需要完成两个事情:

  • 确认用户身份。
  • 授权。例如,角色。

SpringSecurity没有“鉴权”这概念?

其实我认为前面说到的AuthorizationFilter应该叫鉴权过滤器,在识别用户身份后甄别用户是否具备访问权限。我刻意找了一下英文,Authentication可以认证、鉴定、身份验证的意思,Authorization可以是授权、批准的意思。第一眼,竟然有点懵逼。。没有上下文的情况下,Authentication可以是认证,也可以是鉴权。而Authorization可以是授权、也可以是鉴权(批准-访问)。

得,我一直以来的疑惑也找到了:为什么Spring Security里面没有“鉴权”相关概念,而只有认证、授权?如果放到Spring Security的语境中,Authentication就是认证的意思,而且还非常准确,因为他还有身份验证的意思。Authorization则是鉴权的含义,授权/批准你访问某个请求。如果非要区分开,则还应使用Identification作为认证这个概念最为合适。

实际上,SpringSecurity在获取用户信息的时候就已经把用户的完整信息包括权限也加载了,所以认证也包括了我们在聊纯概念的“授权”。而SpringSecurity的Authorization是“授权访问”,也就是“鉴权”。因此,可以说在SpringSecurity中只有“认证”和“鉴权”。因为他要求加载的用户信息是包括权限的,跟认证在一块了。别把“授权”概念跟RABC这些“授权方式”混为一谈了,鄙人就懵圈了好久。关于授权和认证不妨再回头看看之前的文章:Spring Security之认证与授权的概念

SpringSecurity支持认证方式

Spring Security支持多种认证方式:

认证方式过滤器SecurityConfigurer描述
基于Basic认证BasicAuthenticationFilterHttpBasicConfigurer原生支持,开箱即用
基于Digest认证DigestAuthenticationFilter无,需要自己引入原生支持,开箱即用
基于OAuth2认证-资源服务器BearerTokenAuthenticationFilterOAuth2ResourceServerConfigurer需要spring-boot-starter-oauth2-resource-server包
基于OAuth2认证-客户端OAuth2LoginAuthenticationFilterOAuth2LoginConfigurer需要spring-boot-starter-oauth2-resource-server包
基于OAuth2认证-客户端OAuth2AuthorizationCodeGrantFilterOAuth2ClientConfigurer需要spring-boot-starter-oauth2-resource-server包
基于CAS认证CasAuthenticationFilter本来是有Configurer的,4.0之后被弃用,再之后就移除了需要spring-security-cas包
基于第三方系统认证AbstractPreAuthenticatedProcessingFilter-用户在其他系统已经认证了,在当前系统通过RequestAttribute/Header/Cookie等等方式获取到用户名后直接再当前系统把用户信息读取出来。
基于用户名和密码认证UsernamePasswordAuthenticationFilterFormLoginConfigurer原生支持

PS: OAuth2比较复杂,有四种登录方式,还分客户端应用、用户、资源。后面有机会再细聊。

基于第三方系统认证的方式,也有几个原生的实现:
基于J2EE认证:J2eePreAuthenticatedProcessingFilter-JeeConfigurer
基于WebSphere: WebSpherePreAuthenticatedProcessingFilter
基于X509证书认证:X509AuthenticationFilter-X509Configurer
基于Header:RequestHeaderAuthenticationFilter
基于RequestAttribute:RequestAttributeAuthenticationFilter

以上就是Spring Security原生提供的支持、或者通过官方的jar包能够开箱即用的。

基于用户名和密码认证

好了,下面我们来重点关注一下基于用户名和密码的认证方式。首先我们来认识一些必要的核心组件:

组件作用备注
UserDetails提供用户信息,包括权限提供了默认实现:User
UserDetailsService用于加载装载用户信息在认证之前需要先根据用户名查询用户
AuthenticationManager负责完成认证逻辑实现类ProviderManager

前面两个比较容易理解,无非就是加载用户。而后者,对于ProviderManager而言,相较于我们之前基于方法进行权限配置的方式所使用AuthorizationManager来说,无异于单独开辟了一块新天地。

  • ProviderManager
    就名字而言,他就是基于AuthenticationProvider来完成。额,没错,他就是一个新的接口,也就是一个新的组件。而ProviderManager主要的作用就是,根据Authentication的类型,寻找匹配的AuthenticationProvider,然后调用匹配的AuthenticationProvider来完成认证。

核心代码:

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		Authentication result = null;
		Authentication parentResult = null;
		int size = this.providers.size();
		// 遍历所有的AuthenticationProvider 
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			// 找到第一个能处理的AuthenticationProvider就执行authenticate
			result = provider.authenticate(authentication);
			if (result != null) {
				copyDetails(authentication, result);
				break;
			}
		}
		// 如果认证失败就尝试由父AuthenticationManager认证,这部代码省略了
		// 认证成功则返回结果
		// 处理异常-省略代码
	}
}

关于这个AuthenticationProvider,我们来感受一下他的实现:
AuthenticationProvider
之所以会有这么多,是因为Authentication有很多,每个用来表示凭证的都需要不同的处理,然后才能进行认证。例如:JwtAuthenticationToken需要将jwtToken解析后就能得到当前用户已经认证的用户信息了(OAuth2)。这些实现有不少是这种类似于token的。不过我们的用户名和密码方式,则是更为复杂。

public abstract class AbstractUserDetailsAuthenticationProvider
		implements AuthenticationProvider, InitializingBean, MessageSourceAware {
		
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		// 1. 获取用户名
		String username = determineUsername(authentication);
		// 2. 尝试从缓存中获取用户信息
		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			// 缓存中不存在,则加载用户:使用UserDetailsService
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
		}
		// 3. 进行认证:验证用户状态、验证用户凭证(密码)
		try {
			// 验证用户状态
			this.preAuthenticationChecks.check(user);
			// 验证用户凭证(密码)
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException ex) {
			// 重试
		}
		// 验证成功后,检查凭证有效期
		this.postAuthenticationChecks.check(user);
		if (!cacheWasUsed) {
			// 缓存用户
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
		// 创建UsernamePasswordAuthenticationToken
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}	

	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
			UserDetails user) {
		// 构建认证信息(已验证凭证),里面包含当前用户信息和权限
		UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal,
				authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());
		return result;
	}
}

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

	@Override
	@SuppressWarnings("deprecation")
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			// 没有凭证
			throw new BadCredentialsException(this.messages
				.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
		String presentedPassword = authentication.getCredentials().toString();
		// 校验密码
		if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			throw new BadCredentialsException(this.messages
				.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}
}

现在我们知道用户是怎么完成认证的,还有很重要的一环:认证成功之后,用户信息怎么保存?我们都知道一般保存在Session中。

UsernamePasswordAuthenticationFilter

其核心实现在父类:

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		// 当前请求是否为认证请求
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		try {
			// 执行验证。这是个抽象方法,由子类实现。
			Authentication authenticationResult = attemptAuthentication(request, response);
			if (authenticationResult == null) {
				// 没有返回结果,表示子类还没有完成。正常情况走不到这里
				return;
			}
			// 执行session策略
			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
			// 认证成功,执行后续处理
			successfulAuthentication(request, response, chain, authenticationResult);
		}
		catch (InternalAuthenticationServiceException failed) {
			unsuccessfulAuthentication(request, response, failed);
		}
		catch (AuthenticationException ex) {
			unsuccessfulAuthentication(request, response, ex);
		}
	}
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
			Authentication authResult) throws IOException, ServletException {
		// 1. 将当前认证信息保存到SecurityContextHolder中,一般是ThreadLocal,以便后续处理直接使用。
		SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
		context.setAuthentication(authResult);
		this.securityContextHolderStrategy.setContext(context);
		// 2. 将SecurityContext保存起来,一般是session中。这样后续的每个请求都能从中恢复当前用户信息,实现可连续交互式会话。
		this.securityContextRepository.saveContext(context, request, response);
		// 记住我功能
		this.rememberMeServices.loginSuccess(request, response, authResult);
		// 发布认证成功事件
		if (this.eventPublisher != null) {
			this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
		}
		// 认证成功后的处理。与认证成功后需要重定向跳转有关。
		this.successHandler.onAuthenticationSuccess(request, response, authResult);
	}
}

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		// 是否为POST请求
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		// 获取用户名
		String username = obtainUsername(request);
		username = (username != null) ? username.trim() : "";
		// 获取密码
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
		// 构建尚未认证的token,此时没有权限
		UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
				password);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		// 通过ProviderManager认证成功后,也就能获取到数据库中保存的权限了。
		return this.getAuthenticationManager().authenticate(authRequest);
	}
}

认证成功涉及的组件

组件作用描述
SessionAuthenticationStrategySession认证(成功)策略在用户认证成功后,调整session;修改sessionId或者重建session
SecurityContextHolderStrategy为处理当前请求的线程提供SecurityContext一般是保存在ThreadLocal中
SecurityContextRepository为不同请求持久化SecurityContext用户认证成功后,主要是为了完成后续请求。因此需要将SecurityContext持久化。而恢复ThreadLocal中的SecurityContext,也需要从这里获取
RememberMeServices记住我Service在认证成功后,若开启记住我功能,需要生成RemenberMeToken。后面才能使用该Token进行认证而无需用户输入密码
AuthenticationSuccessHandler认证成功后的处理器这是对于用户而言的,认证成功后需要给用户呈现什么内容/页面

由于这些都与session管理有着不可分割的关系,因此,我们留待后续聊session管理的时候再说。

小结

  • 核心认证流程
    1. 从HttpServletRequest中获取到用户名和密码
    2. 交给ProviderManager进行认证。
      DaoAuthenticationProvider会通过UserDetailsService从数据库获取用户信息,然后验证入参的密码。
    3. 认证成功后,创建SecurityContext并保存到ThreadLocal中,同时将其保存到Session中。当然还有其他扩展功能,后面再细聊。

后记

本文中,我们探讨了Spring Security的认证过滤器,并从源码层面分析了UsernamePasswordAuthenticationFilter的原理和处理流程。但是我们并没有仔细探索认证成功之后的操作。因为这些涉及到Session管理,这就与另一个过滤器SessionManagementFilter有着密不可分的关系了。所以下次,我们就聊SessionManagementFilter。届时会仔细说说。

参照

01 认证、授权、鉴权和权限控制
Authentication
JAAS 认证
【揭秘SAML协议 — Java安全认证框架的核心基石】 从初识到精通,带你领略Saml协议的奥秘,告别SSO的迷茫与困惑

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

unity学习(69)——多人位置同步

简单的很&#xff0c;每个客户端向服务器发送位置信息&#xff0c;服务器再把这些位置信息发送给其他客户端。 1.客户端发送。 1.1在SocketModel脚本中添加一个新的类MoveDTO public class MoveDTO {public string Id{get; set;}public int Dir{get;set;}public Assets.Mode…

Leetcode第13题:罗马数转为十进制数

利用等价换算法将罗马数转为十进制数 class Solution:def romanToInt(self, s: str) -> int:roman_map{I:1,V:5,X:10,L:50,C:100,D:500,M:1000}before_val,countroman_map[s[0]],0for c in s:valroman_map[c]if val<before_val:countvalelse:countcount-val2*(val-befor…

echarts 柱形图如何让其中一个柱子的颜色跟其他柱子不同

如何让其中一个柱子的颜色跟其他柱子不同 series: [{data: [120,// 使用对象的形式&#xff0c; value代表当前值, itemStyle设置样式{value: 200,itemStyle: {color: #a90000}},150,80,70,110,130],type: bar}]设置单个柱子颜色&#xff1a; 柱形图单个柱子颜色: https://e…

c 语言 三元搜索 - 迭代与递归(Ternary Search)

计算机系统使用不同的方法来查找特定数据。有多种搜索算法&#xff0c;每种算法更适合特定情况。例如&#xff0c;二分搜索将信息分为两部分&#xff0c;而三元搜索则执行相同的操作&#xff0c;但分为三个相等的部分。值得注意的是&#xff0c;三元搜索仅对排序数据有效。在本…

video.js自定义预览组件-旋转、下载、画中画、放大缩小功能

使用video.js实现视频播放功能 效果图 - 这里以弹窗展示为例 注意&#xff1a;记得安装video.js插件&#xff01;&#xff01;&#xff01; 代码 父级使用&#xff1a; videoPreview.vue文件 <!-- 视频预览组件 --> <template><el-dialogid"previewFi…

【战略前沿】丹麦正在建造一台英伟达人工智能超级计算机

【原文】Denmark is building an Nvidia AI supercomputer 【作者】Linnea Ahlgren 它将于今年上线&#xff0c;并以新的量子计算软件为特色。 过去一年最大的赢家——芯片制造商英伟达&#xff08;Nvidia&#xff09;和制药制造商诺和诺德&#xff08;Novo Nordisk&#xff0…

【C语言】linux内核pci_alloc_irq_vectors

一、注释 代码中包含了几个关于PCI&#xff08;外围组件互联&#xff09;设备中断请求&#xff08;IRQ&#xff09;向量分配的函数&#xff0c;以及内联函数声明&#xff0c;下面是对这些函数的中文注释&#xff1a; static inline int pci_alloc_irq_vectors_affinity(struc…

曲线生成 | 图解Reeds-Shepp曲线生成原理(附ROS C++/Python/Matlab仿真)

目录 0 专栏介绍1 什么是Reeds-Shepp曲线&#xff1f;2 Reeds-Shepp曲线的运动模式3 Reeds-Shepp曲线算法原理3.1 坐标变换3.2 时间翻转(time-flip)3.3 反射变换(reflect)3.4 后向变换(backwards) 4 仿真实现4.1 ROS C实现4.2 Python实现4.3 Matlab实现 0 专栏介绍 &#x1f5…

【竞技宝】DOTA2:lou神带队速推 AR力克Zero晋级决赛

北京时间2024年3月24日,DOTA2梦幻联赛S23中国区预选赛正在进行之中,昨日进行了本次预选赛的胜者组决赛Zero对阵AR。本场比赛双方前两局战至1-1平,决胜局AR选出一套前期进攻性十足的阵容早早取得优势,最终AR鏖战三局力克Zero晋级决赛。以下是本场比赛的详细战报。 第一局: Zero…

第九篇【传奇开心果系列】Python自动化办公库技术点案例示例:深度解读Python处理PDF文件

传奇开心果博文系列 系列博文目录Python自动化办公库技术点案例示例系列 博文目录前言一、重要作用介绍二、Python库处理PDF文件基础操作和高级操作介绍&#xff08;一&#xff09;基础操作介绍&#xff08;二&#xff09;高级操作介绍 三、Python库处理PDF文件基础操作示例代码…

ESP8266制作WIFI音箱

首先是设备截图 使用的技术: 1、Esp8266播放网络音乐 2、自己搭建一个音乐播放服务,这样播放的内容就由自己而定了,将你的服务对接支付宝,就可以实现支付宝收款语音播报了 代码 esp8266代码 #include <Arduino.h>#ifdef ESP32#include <WiFi.h> #else#inc…

(AtCoder Beginner Contest 325) ---- D - Printing Machine -- 题解

目录 D - Printing Machine&#xff1a; 题目大意&#xff1a; 思路解析&#xff1a; 代码实现&#xff1a; D - Printing Machine&#xff1a; 题目大意&#xff1a; 思路解析&#xff1a; 打印一次后&#xff0c;需要充电一微秒后才能再次打印就可以看作每微妙只能打印一…

2024年3月GESP认证Python编程一级真题试卷

2024年3月GESP认证Python编程一级真题试卷 题目总数&#xff1a;27 总分数&#xff1a;100 选择题 第 1 题 单选题 小杨的父母最近刚刚给他买了一块华为手表&#xff0c;他说手表上跑的是鸿蒙&#xff0c;这个鸿蒙是&#xff1f;&#xff08; &#xff09;。 A.小程…

03. 【Android教程】Genymotion 的安装与使用

在上一章中我们在 Eclipse 当中创建了 AVD&#xff0c;由于性能差只适合测试小型 App。这里将推荐一款性能更佳的 Android 模拟器—— Genymotion。首先我们看看 Genymotion 好在哪里。 1. Genymotion 优势 Genymotion 相对于内置模拟器有如下优势&#xff1a; 运行速度快、画…

[数据结构]二叉树的建立与遍历(递归)

一、二叉树的遍历与建立 首先我们拥有如下二叉树: 要了解二叉树遍历,我们得先了解二叉树的三种遍历方式:前序遍历,中序遍历,后序遍历 1.前序遍历 前序遍历:根,左子树,右子树 遍历的结果就是:1 2 4 8 N N 9 N N 5 10 N N 11 N N 3 6 N N 7 N N 2.中序遍历 中序遍历:左子树…

爆增49.07%!2024国自然面上项目申报,再创新高

毕业推荐 SSCI&#xff08;ABS一星&#xff09; • 社科类&#xff0c;3.0-4.0&#xff0c;JCR2区&#xff0c;中科院3区 • 13天录用&#xff0c;28天见刊&#xff0c;13天检索 SCIE&#xff1a; • 计算机类&#xff0c;6.5-7.0&#xff0c;JCR1区&#xff0c;中科院2区…

大东方保险集团陈志远:洞察保险行业的重要性及未来三年发展前景

在当今社会,保险行业作为风险管理的重要工具,正日益凸显其不可或缺的地位。大东方保险集团陈志远近日在接受采访时,深入探讨了保险行业的重要性以及未来三年的发展前景。 一、保险行业的重要性 陈志远指出,保险行业在现代经济中扮演着举足轻重的角色。它不仅是社会稳定的“减震…

Spring自定义注解防重提交方案(参数形式Token令牌)

防重提交通常在需要防止用户重复提交表单或执行某些敏感操作时使用&#xff0c;以确保系统的数据一致性和安全性&#xff0c;本文章集结了通用场景下防重提交&#xff08;参数形式&Token令牌&#xff09;&#xff0c;采用Java的特性&#xff08;注解和AOP&#xff09;&…

后端如何返回404地址

当我们网站输入不存在的地址&#xff0c;经常会出现404的页面&#xff0c;这是如何做到的 1.添加配置 spring:mvc:view:prefix: /templates/suffix: .html 2.resources下添加templates目录&#xff0c;下面放404的网站 3.添加依赖&#xff0c;版本在主pom里面配置好了&#x…

Memcached非关系型数据库介绍

使用背景 Memcached 不是一个数据库&#xff0c;而是一个高性能的分布式内存对象缓存系统。它主要用于减轻数据库负载&#xff0c;提高动态Web应用的速度、可扩展性和性能。Memcached 的工作原理是将数据存储在内存中&#xff0c;以提供快速的数据访问。当应用程序需要访问数据…