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.rhkb.cn/news/285662.html

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

相关文章

巧用 20个 Linux 命令贴士与技巧,让你生产力瞬间翻倍?

在本文中&#xff0c;我将向您演示一些专业的Linux命令技巧&#xff0c;这些技巧将使您节省大量时间&#xff0c;在某些情况下还可以避免很多麻烦&#xff0c;而且它也将帮助您提高工作效率。 并不是说这些只是针对初学者的 Linux 技巧。即使有经验的Linux用户也有可能没有发现…

简单了解单例模式

什么是单例模式 对于一个类&#xff0c;只有一个实例化的对象&#xff0c;我们构建单例模式一般有两种&#xff1a;饿汉式和懒汉式 饿汉式 优点是无线程安全问题&#xff0c;类加载就创建对象缺点是占内存 class Singleton01{private static Singleton01 instance new Sing…

shell实现查询进程号并批量kill(脚本)

问题或需求描述 在shell中&#xff0c;如果你想通过命令行查询出一系列匹配某个关键词的进程&#xff0c;并使用xargs命令批量结束这些进程&#xff0c;可以按照以下步骤操作&#xff1a; # 查询并提取进程号 pgrep -f "关键词" | xargs kill# 或者&#xff0c;如果…

图书馆RFID(射频识别)数据模型压缩/解压缩算法实现小工具

点击下载《C# 实现图书馆射频识别数据模型压缩算法&#xff08;源代码pdf参考资料&#xff09;》 1. 前言 最近闲来无聊&#xff0c;看了一下《图书馆射频识别数据模型第1部分&#xff1a;数据元素的设置及应用规则》以及《图书馆射频识别数据模型第2部分&#xff1a;基于ISO…

如何联合Qt,VS,C++,来开发一个电脑版软件(简单有趣,详细)

本教程适合 新手VS+QT小白。目前更新到了可以写一个计算器【拉到文章末尾,可以看到界面】。 前置安装 VS2019 或2022 社区版(这个太简单,就不在这里写了!)建议参考之前写的文章: https://zhuanlan.zhihu.com/p/682531067 注册登陆Qt账户 Try Qt 下载Qt 登陆之后,…

iOS开发 - 转源码 - __weak问题解决

iOS开发 - 转源码 - __weak问题解决 在使用clang转换OC为C代码时&#xff0c;可能会遇到以下问题 cannot create __weak reference in file using manual reference 原因 __weak弱引用是需要runtime支持的&#xff0c;如果我们还只是使用静态编译&#xff0c;是无法正常转换的…

Jenkins的快速入门

文章目录 一、Jenkins是什么&#xff1f;二、Jenkins安装和持续集成环境配置1.持续集成流程说明2.Gitlab代码托管服务器安装Gitlab简介&#xff1a;Gitlab安装Gitlab的使用切换中文添加组创建用户将用户添加到组创建项目idea中代码上传Gitlab 3.Jenkins持续集成环境服务器安装J…

在MongoDB建模1对N关系的基本方法

“我在 SQL 和规范化数据库方面拥有丰富的经验&#xff0c;但我只是 MongoDB 的初学者。如何建立一对 N 关系模型&#xff1f;” 这是我从参加 MongoDB 分享日活动的用户那里得到的最常见问题之一。 我对这个问题没有简短的答案&#xff0c;因为方法不只有一种&#xff0c;还有…

【Python实战】——神经网络识别手写数字

&#x1f349;CSDN小墨&晓末:https://blog.csdn.net/jd1813346972 个人介绍: 研一&#xff5c;统计学&#xff5c;干货分享          擅长Python、Matlab、R等主流编程软件          累计十余项国家级比赛奖项&#xff0c;参与研究经费10w、40w级横向 文…

【C++从练气到飞升】05---运算符重载

&#x1f388;个人主页&#xff1a;库库的里昂 ✨收录专栏&#xff1a;C从练气到飞升 &#x1f389;鸟欲高飞先振翅&#xff0c;人求上进先读书。 目录 ⛳️推荐 一、运算符重载的引用 二、运算符重载 三、赋值运算符重载 1 .赋值运算符重载格式: 2 .赋值运算符只能重载成…

SAP 标准委外业务对已收货后对组件的后续调整简介

标准委外业务对已收货后对组件的后续调整 通常在委外的业务中经常会存在发给供应商的物料发现有少发&#xff0c;或者需要补发的情况&#xff0c;委外的业务都是基于采购订单收货的时候才对供应商库存进行扣减。委外成品在收货后产生543的移动类型原材料进行冲消。 但是我们在物…

【C语言】编译和链接----预处理详解【图文详解】

欢迎来CILMY23的博客喔&#xff0c;本篇为【C语言】文件操作揭秘&#xff1a;C语言中文件的顺序读写、随机读写、判断文件结束和文件缓冲区详细解析【图文详解】&#xff0c;感谢观看&#xff0c;支持的可以给个一键三连&#xff0c;点赞关注收藏。 前言 欢迎来到本篇博客&…

继承和多态(2)(多态部分)

提前讲的重要知识点 一个类在没有父类的情况下默认有一个父类为Object类。 而当在有父类情况下&#xff0c;如果你那父类没有父类&#xff0c;则其父类的父类默认为object类&#xff0c;所以即使一个类有父类&#xff0c;其内部还是有object类。 object类都是隐藏起来的&…

【机器学习】基于北方苍鹰算法优化的BP神经网络分类预测(NGO-BP)

目录 1.原理与思路2.设计与实现3.结果预测4.代码获取 1.原理与思路 【智能算法应用】智能算法优化BP神经网络思路【智能算法】北方苍鹰优化算法&#xff08;NGO)原理及实现 2.设计与实现 数据集&#xff1a; 数据集样本总数2000 多输入单输出&#xff1a;样本特征24&#x…

Vue3:网页项目中路由的设计和配置

为了避免我每次建项目配路由的时候都回去翻网课&#xff0c;打算整一博客 路由设计 不同网页的路由设计思路基本相同&#xff0c;分为一级路由和二级路由&#xff0c;基本设计思路如下图 以我之前做过的招新系统管理端为例&#xff0c;可设计出如下路由 路由配置 还是以招新系…

剖析美国政府视角下的ICT供应链安全

2018 年 11 月 15 日&#xff0c;美国国土安全部&#xff08;DHS&#xff09;宣布成立了信息和通信技术 (ICT) 供应链风险管理&#xff08;SCRM&#xff09;工作组&#xff0c;这个工作组是由美国多个政府部门、IT行业企业代表及通信行业企业代表联合成立的。该组织对外宣传的目…

Docker Command

小试牛刀 # 查看docker版本 docker -v docker --version # 查看帮助 docker --help # 永远的Hello World docker run hello-world镜像操作 查看本地已有的镜像 docker images -a :列出本地所有的镜像&#xff08;含中间映像层&#xff09; -q :只显示镜像ID --digests :显示…

C# 右键快捷菜单(上下文菜单)的两种实现方式

在C#中&#xff0c;ContextMenuStrip是一种用于创建右键菜单的控件。它提供了一种方便的方式来为特定的控件或窗体添加自定义的上下文菜单选项。有两种实现方式&#xff0c;如下&#xff1a; 一.通过ContextMenuStrip控件实现 1.从工具箱中拖一个ContextMenuStrip控件到窗体上…

Java面试题总结200道(四)

76、ApplicationContext 通常的实现是什么? FileSystemXmlApplicationContext &#xff1a;此容器从一个 XML 文件中加 载 beans 的定义&#xff0c;XML Bean 配置文件的全路径名必须提供给它的构造函数。ClassPathXmlApplicationContext&#xff1a;此容器也从一个 XML 文件…

python、execl数据分析(数据描述)

一 python 1.各函数 1.1python库的安装与导入 #pip install os#pip install matplotlib#pip install seaborn#pip install scikit-learn#pip install scipy#修 改 工 作 目 录import osos.getcwd () # 查看当前工作环境os.chdir( F :\my course\database ) # 修改工作环境o…