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 {@Overridepublic 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就执行authenticateresult = provider.authenticate(authentication);if (result != null) {copyDetails(authentication, result);break;}}// 如果认证失败就尝试由父AuthenticationManager认证,这部代码省略了// 认证成功则返回结果// 处理异常-省略代码}
}

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

public abstract class AbstractUserDetailsAuthenticationProviderimplements AuthenticationProvider, InitializingBean, MessageSourceAware {@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {// 1. 获取用户名String username = determineUsername(authentication);// 2. 尝试从缓存中获取用户信息boolean cacheWasUsed = true;UserDetails user = this.userCache.getUserFromCache(username);if (user == null) {// 缓存中不存在,则加载用户:使用UserDetailsServiceuser = 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();}// 创建UsernamePasswordAuthenticationTokenreturn 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 GenericFilterBeanimplements 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 {@Overridepublic 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" propertysetDetails(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的迷茫与困惑

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

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

相关文章

ES的集群节点发现故障排除指南(1)

本文是ES官方文档关于集群节点发现与互联互通的问题排查指南内容。 英文原文&#xff08;官网&#xff09; 集群节点发现是首要任务 集群互连&#xff0c;重中之重&#xff01; 在大多数情况下&#xff0c;发现和选举过程会迅速完成&#xff0c;并且主节点会长时间保持当选状…

生骨肉冻干真的对猫身体好吗?排行榜靠前对猫身体好的生骨肉冻干推荐

随着科学养猫知识的广泛传播&#xff0c;生骨肉冻干喂养正逐渐受到养猫人士的青睐。生骨肉冻干真的对猫身体好吗&#xff1f;生骨肉冻干不仅符合猫咪的饮食天性&#xff0c;还能提供全面均衡的营养&#xff0c;有助于维护猫咪的口腔和消化系统健康。然而&#xff0c;许多猫主人…

Open CASCADE学习|最小二乘法拟合直线

最小二乘法&#xff0c;又称最小平方法&#xff0c;起源于十八世纪的大航海探索时期&#xff0c;发展于天文领域和航海领域。其历史可以追溯到法国科学家马里勒让德于1805年首次提出这一概念&#xff0c;而1809年&#xff0c;高斯在他的著作《天体运动论》中也提出了最小二乘法…

xilinx FPGA 除法器ip核(divider)的学习和仿真(Vivado)

在设计中&#xff0c;经常出现除法运算&#xff0c;实现方法&#xff1a; 1、移位操作 2、取模取余 3、调用除法器IP核 4、查找表 简单学习除法器IP。 网上很多IP翻译文档&#xff0c;不详细介绍&#xff0c;记录几个重要的点&#xff1a; 1、三种算法模式(不同模式所消耗的资…

【Linux 进程概念】

【Linux 进程概念】 冯诺依曼体系结构冯诺依曼结构简要解释&#xff1a;你用QQ和朋友聊天时数据的流动过程 操作系统(OperatorSystem)概念设计OS的目的定位操作系统的上下层都分别是什么如何理解“管理"总结 进程基本概念描述进程-PCBtask_ struct内容 组织进程查看进程通…

大模型围剿战:Kimi如何在免费与盈利之间找到平衡?

文 | 大力财经 在近期的互联网科技领域&#xff0c;一款名为Kimi的国产大型AI模型引起了广泛关注。随着Kimi的火爆&#xff0c;国内的大型科技公司纷纷开始关注并投入到长文本处理技术的竞争中。 阿里巴巴、360等知名企业纷纷宣布了他们的长文本处理能力&#xff0c;分别达到…

网络——套接字编程UDP

目录 端口号 源端口号和目的端口号 认识TCP协议和UDP协议 网络字节序 socket编程接口 socket常见接口 sockaddr结构 UDP socket bind recvfrom sendto 编写客户端 绑定INADDR_ANY 实现聊天功能 端口号 在这之前我们已经说过源IP地址和目的IP地址&#xff0c;还有…

微信小程序的页面制作---常用组件及其属性2

一、标签栏taBar 在全局配置文件app.json中添加taBar配置&#xff0c;可实现标签栏配置。标签栏最少2个&#xff0c;最多5个 &#xff08;1&#xff09;如何配置标签栏&#xff1f; 1》先建多个文件&#xff0c;&#xff08;以我的index&#xff0c;list&#xff0c;myform文…

最近很火的游戏,人渣/SCUM(更新V0.9.517.81845)

包含DLC • SCUM Deluxe • SCUM Deluxe 2 • SCUM Danny Trejo • SCUM Hair Deluxe 1 • SCUM Luis Moncada • SCUM Charms 1 • SCUM Weapon Skins 1 中文设置 OPTIONS-LANGUAGE-中文(简体) 特别提示 • 通过“\SCUM\Binaries\Win64\SCUM.exe”启动游戏 • 如果…

Microsoft Excel 快捷键 (keyboard shortcut - hotkey)

Microsoft Excel 快捷键 [keyboard shortcut - hotkey] References 表格内部换行快捷键 Alt Enter 快速将光标移到表末 Ctrl End 快速将光标移到表首 Ctrl Home References [1] Yongqiang Cheng, https://yongqiang.blog.csdn.net/

人工智能(Educoder)-- 搜索技术 -- 盲目式搜索

第1关&#xff1a;盲目搜索之宽度优先搜索算法 任务描述 本关任务&#xff1a;给定迷宫地图以及在迷宫中的起始位置&#xff0c;利用宽度优先搜索算法求解走出迷宫的最短路径长度&#xff0c;走出迷宫意味着达到迷宫地图的边界&#xff08;所有位置下标0开始&#xff09;。 …

水电站泄洪预警广播系统方案

一、行业背景 近年来由于危险河道管理措施不到位&#xff0c;调峰电站泄水风险长期存在&#xff0c;信息通报制度缺失以及民众安全警觉性不高等因素导致的水电站在泄洪时冲走下游河道游客以及人民财产的事故频发。 我司通过物联网、云计算、大数据、人工智能等技术手段&#x…

不能从所选图层建立3d模型--模大狮模型网

在Revit中&#xff0c;从所选图层直接创建3D模型并不是一个常规的操作方式。通常情况下&#xff0c;你需要手动创建文字或者图形&#xff0c;然后将其转换为3D模型。 如果你有一个平面上的文字或图形&#xff0c;想要将其转换为3D模型&#xff0c;你可以使用以下步骤&#xff1…

什么是虚拟线程?

1、典型回答 Java 中的虚拟线程&#xff0c;也叫做协程或“轻量级线程”&#xff0c;它诞生于JDK 19(预览 API)&#xff0c;正式发布于 JDK 21&#xff0c;它是一种在 Java 虚拟机(JVM)层面实现的逻辑线程&#xff0c;不直接和操作系统的物理线程一一对应&#xff0c;因此它可…

文献速递:基于SAM的医学图像分割---SAM-Med2D

Title 题目 SAM-Med2D 01 文献速递介绍 医学图像分割在通过识别和勾画各种组织、器官或感兴趣区域来分析医学图像中发挥着至关重要的作用。准确的分割可以帮助医生精确识别和定位病理区域&#xff0c;从而实现更准确的诊断和治疗。此外&#xff0c;对医学图像进行定量和定性…

Node.js之沙盒专题

​ Node.js一直是薄弱项&#xff0c;今天特意整理一下&#xff0c;基本上是各个大佬写的大杂烩&#xff0c;仅用于学习记录~~~ 1. child_process 首先介绍一下nodejs中用来执行系统命令的模块child_process。Nodejs通过使用child_process模块来生成多个子进程来处理其他事物…

简化业务流程,AppLink连接一定签

APPlink是什么 APPlink是RestCloud打造的一款简单易用的零代码自动化集成平台&#xff0c;为业务流程提供自动化的解决方案&#xff0c;将企业内部的核心系统以及第三方应用程序和云服务等进行集成。无论是开发人员还是业务人员&#xff0c;都可以使用APPlink轻松构建出高效、…

【触想智能】工业触摸一体机九大常见故障检测方法分享

工业触摸一体机目前在社会生产中应用非常广泛&#xff0c;比如智能化的生产车间、城市智慧安防监控中心都经常用到工业触摸一体机。 电子产品在使用中难免会出现一些故障&#xff0c;工业触摸一体机也不例外。那么我们在使用工业触摸一体机的时遇到问题怎么办呢?下面小编给大家…

第十九章 linux部署scrapyd

文章目录 1. linux部署python环境1. 部署python源文件环境2. 下载python3. 解压安装包4. 安装5. 配置环境变量6. 检查是否安装成功7. 准备python使用的包8. 安装scrapyd9. 配置scrapyd10. 开放6800端口 2. 部署gerapy1. 本机下载包2. 初始化3. 进入gerapy同步数据库4. 创建用户…

nginx启停操作

一、nginx启动 方式一&#xff1a; /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf方式二&#xff1a; systemctl start nginx 查看进程启动状态 ps -ef | grep nginx 上图表示nginx进程启动成功&#xff0c;进程号为30034为主进程(负责链接操作)&am…