Spring Security学习(七)——父子AuthenticationManager(ProviderManager)

前言

《Spring Security学习(六)——配置多个Provider》有个很奇怪的现象,如果我们不添加DaoAuthenticationProvider到HttpSecurity中,似乎也能够达到类似的效果。那我们为什么要多此一举呢?从文章的效果来看确实是多此一举,但其实这里面暗藏玄机。也引出了本文的父子AuthenticationManager(ProviderManager)的话题。

探究父子AuthenticationManager(ProviderManager)体系

我们在Spring Security源码的ProviderManager(在org.springframework.security.authentication包中)中,在authenticate方法的以下位置断点调试:

 然后查看断点的变量:

目前程序准备处理MyProvider的authenticate方法,目前providers列表中有三个provider,分别是我们添加的MyProvider和DaoAuthenticationProvider,还有一个AnonymousAuthenticationProvider是Spring Security在HttpSecurity初始化时加进去的(参考HttpSecurityConfiguration的httpSecurity方法)。

重点来了,我们还看到parent,里面有个DaoAuthenticationProvider。parent的ProviderManager和里面的DaoAuthenticationProvider又是什么时候加进去的?这个源码上有点复杂,我也不准备大段大段的粘出来(大概率读者一下子很难看懂),提示一下读者,在HttpSecurityConfiguration的httpSecurity方法的这一行:

在蓝色标出的位置设置的父ProviderManager。这个父ProviderManager是AuthenticationConfiguration配置中创建InitializeUserDetailsBeanManagerConfigurer到Spring容器中,然后在Spring Security初始化时调用InitializeUserDetailsBeanManagerConfigurer的configue方法获取并设置ProviderManager,在父ProviderManager中设置DaoAuthenticationProvider也是类似的原理。

读者可以尝试删掉《Spring Securi习(六)——配置多个Provider》WebSecurityConfig中添加DaoAuthenticationProvider到HttpSecurity的代码,再断点调试一下。

父AuthenticationManager的作用

父子AuthenticationManager的机制是这样的:先对子AuthenticationManager中的AuthenticationProvider列表进行逐个匹配,若都无法匹配,则会对父AuthenticationManager中的AuthenticationProvider列表进行逐个匹配。

下面两张图是来自Spring Security官网文档的:

通过第二张图的多个子ProviderManager,我认为父AuthenticationManager的作用就是给多个子ProviderManager一个公共的匹配方式。

多个ProviderManager又是用在什么场景呢?根据Spring Security官网描述,是存在多个SecurityFilterChain,并且存在不同的登陆认证机制时使用。

自定义父ProviderManager

《Spring Security学习(六)——配置多个Provider》中直接调用http.authenticationProvider是把Provider加入子ProviderManager中。如果想加入到父ProviderManager中要怎么做呢?

查看过源码后,往父ProviderManager加Provider是比较复杂(当然也不是做不到),所以本次我们的目标是自定义父ProviderManager,加入需要的Provider,然后覆盖原父ProviderManager。

自定义一个新的Provider,从redis中取出账号密码进行比对,新建MyRedisProvider:

@Data
public class MyRedisProvider extends AbstractUserDetailsAuthenticationProvider{private UserDetailsServiceImpl userDetailsServiceImpl;private PasswordEncoder passwordEncoder;private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";private volatile String userNotFoundEncodedPassword;@Overrideprotected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {prepareTimingAttackProtection();try {UserDetails loadedUser = userDetailsServiceImpl.loadUserByRedis(username);if (loadedUser == null) {throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");}return loadedUser;}catch (UsernameNotFoundException ex) {mitigateAgainstTimingAttack(authentication);throw ex;}catch (InternalAuthenticationServiceException ex) {throw ex;}catch (Exception ex) {throw new InternalAuthenticationServiceException(ex.getMessage(), ex);}}@Overrideprotected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {if (authentication.getCredentials() == null) {this.logger.debug("Failed to authenticate since no credentials provided");throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));}String presentedPassword = authentication.getCredentials().toString();if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {this.logger.debug("Failed to authenticate since password does not match stored value");throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));}}private void prepareTimingAttackProtection() {if (this.userNotFoundEncodedPassword == null) {this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);}}private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {if (authentication.getCredentials() != null) {String presentedPassword = authentication.getCredentials().toString();this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);}}
}

和之前的MyProvider相比,仅仅是更改了第17行userDetailsServiceImpl.loadUserByRedis,从redis读取用户信息。当然在UserDetailsServiceImpl中我们也要加上loadUserByRedis方法:

@Component
public class UserDetailsServiceImpl implements UserDetailsService, InitializingBean{@Autowiredprivate SysUserService userService;@Autowiredprivate RedisTemplate redisTemplate;private static Map<String, SysUserEntity> userMap = new HashMap<String, SysUserEntity>();@Overridepublic void afterPropertiesSet() throws Exception {SysUserEntity memorySysUser = new SysUserEntity();memorySysUser.setUsername("test");memorySysUser.setPassword("test###");userMap.put("test", memorySysUser);SysUserEntity redisSysUser = new SysUserEntity();redisSysUser.setUsername("admin");redisSysUser.setPassword("admin123###");redisTemplate.opsForValue().set("admin", redisSysUser);}@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<SysUserEntity>();queryWrapper.eq("username", username);queryWrapper.last("limit 1");SysUserEntity user = userService.getOne(queryWrapper);if(user == null) {throw new UsernameNotFoundException("username not found");}return (new LoginUser(user));}public UserDetails loadUserByMemory(String username) throws UsernameNotFoundException {if(StrUtil.isNotEmpty(username)) {SysUserEntity user = userMap.get(username);if(user == null) {throw new UsernameNotFoundException("username not found");}return (new LoginUser(user));}return null;}public UserDetails loadUserByRedis(String username) throws UsernameNotFoundException {if(StrUtil.isNotEmpty(username)) {SysUserEntity user = (SysUserEntity)redisTemplate.opsForValue().get(username);if(user == null) {throw new UsernameNotFoundException("username not found");}return (new LoginUser(user));}return null;}
}

上述代码除了增加loadUserByRedis方法,为了在初始化时设置数据,还实现了InitializingBean接口的afterPropertiesSet方法,当然这只是测试使用。

为了使用redis,我们在pom.xml中引入相关依赖:

		<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency>

另外要在application.yml文件增加redis配置:

spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedriverClassName: com.mysql.cj.jdbc.Driverdruid:url: jdbc:mysql://127.0.0.1:3306/security_test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: thymeleaf:prefix: classpath:/templates/redis:host: 127.0.0.1port: 6379password:lettuce:pool:max-active: 500max-idle: 300max-wait: 1000min-idle: 0database: 8

增加一个配置类RedisConfig:

@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory){RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<String, Serializable>();redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.setConnectionFactory(connectionFactory);return redisTemplate;}
}

这样就可以使用RedisTemplate处理序列化的内容。

最后修改一下WebSecurityConfig:

@EnableWebSecurity
public class WebSecurityConfig{@Beanpublic MyPasswordEncoder PasswordEncoder() {return new MyPasswordEncoder();}@Beanpublic AuthenticationManager createParentAuthenticationManager() {List<AuthenticationProvider> providerList = new ArrayList<AuthenticationProvider>();MyRedisProvider myRedisProvider = new MyRedisProvider();myRedisProvider.setPasswordEncoder(PasswordEncoder());UserDetailsServiceImpl UserDetailsServiceImpl = SpringUtils.getBean(UserDetailsServiceImpl.class);myRedisProvider.setUserDetailsServiceImpl(UserDetailsServiceImpl);providerList.add(myRedisProvider);AuthenticationManager parentAuthenticationManager = new ProviderManager(providerList);return parentAuthenticationManager;}@Beanpublic SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()).formLogin(Customizer.withDefaults());MyProvider myProvider = new MyProvider();myProvider.setPasswordEncoder(PasswordEncoder());UserDetailsServiceImpl UserDetailsServiceImpl = SpringUtils.getBean(UserDetailsServiceImpl.class);myProvider.setUserDetailsServiceImpl(UserDetailsServiceImpl);http.authenticationProvider(myProvider);DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();daoAuthenticationProvider.setPasswordEncoder(PasswordEncoder());daoAuthenticationProvider.setUserDetailsService(UserDetailsServiceImpl);http.authenticationProvider(daoAuthenticationProvider);AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);AuthenticationManager parentAuthenticationManager = applicationContext.getBean(AuthenticationManager.class);authenticationManagerBuilder.parentAuthenticationManager(parentAuthenticationManager);return http.build();}
}

9-19行创建自定义的myRedisProvider,配置好加密器、userDetailsService、然后放到新建的ProviderManager中,通过@Bean注解注入到Spring容器中。39-42行从Spring Security上下文中获取9-19行注入Spring容器的ProviderManager并设置为父AuthenticationManager。 

注:之前看过有的文章说直接通过@Bean注入容器就可以了。但是我自己调试过不行,查看了源码父AuthenticationManager默认是new创建的,并没有从容器中获取的逻辑。如果读者有相关的逻辑和实现方式也请进行指正。

之后启动应用,访问/hello路径,然后尝试输入jake/123、test/test、admin/admin123,应该都能通过。

小结

本文主要讲述了父子AuthenticationManager的机制,并且实现了如何自定义父ProviderManager。其实对于一般的应用是没必要这么搞的,如Spring Security官网所说,主要用在多种不同认证体系下。所以本文主要目的还是学习其内部机制为主。

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

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

相关文章

2024最新互联网大厂面试题,(java,python,vue)

最近又赶上跳槽的高峰期&#xff0c;好多粉丝&#xff0c;都问我要有没有最新面试题&#xff0c;索性&#xff0c;我就把我看过的和我面试中的真题&#xff0c;及答案都整理好&#xff0c;整理了《第3版&#xff1a;互联网大厂面试题》并分类150份 PDF&#xff0c;累计 7701页&…

This dependency was not found解决方法

问题如上(前端代码)&#xff0c;我是引用js文件出的问题&#xff0c;无法找到api/userManage模块。 解决&#xff1a;没感觉哪有问题&#xff0c;把后面加了个/&#xff0c;就解决了&#xff0c;代表src目录&#xff0c;应该是目录和目录之间应该有/作为分割&#xff1a;

【AUTOSAR】--02 AUTOSAR网络管理相关参数

这是AUTOSAR网络管理梳理的第二篇文章&#xff0c;主要讲解AUTOSAR网络管理的相关参数。第一篇链接【01 AUTOSAR网络管理基础】。​ 相关参数有很多&#xff0c;我挑了一些相对重要的参数&#xff0c;分三部分进行讲解&#xff1a; 第一部分&#xff1a;比较常用&#xff0c…

Excel 面试题及答案(2)

一、VLOOKUP+IF案例: A1 :根据左侧数据源,按姓名匹配《职级》,仅限用函数,不能做任何辅助A2 :根据左侧数据源,按姓名匹配《部门》,仅限用函数,不能做任何辅助A3 :根据右侧考核规则,匹配《绩效比例》,用函数完成(可适当做辅助的单元格区域) =VLOOKUP(F8,IF({1,0},…

二刷代码随想录算法训练营第四天 |24. 两两交换链表中的节点、19.删除链表的倒数第N个节点 面试题 、02.07. 链表相交 、142.环形链表II

目录 一、24. 两两交换链表中的节点 二、19. 删除链表的倒数第 N 个结点 三、面试题 02.07. 链表相交 四、142. 环形链表 II 一、24. 两两交换链表中的节点 题目链接&#xff1a;力扣 文章讲解&#xff1a;代码随想录 视频讲解&#xff1a; 帮你把链表细节学清楚&#xff…

渗透工具——kali中cewl简介

一、什么是cewl cewl是一个ruby应用&#xff0c;爬行指定url的指定深度。也可以跟一个外部链接&#xff0c;结果会返回一个单词列表&#xff0c;这个列表可以扔到wpscan等密码爆破工具里进行密码破解。 cewl工具爬取目标网站信息&#xff0c;生成相对应的字典 二、cewl的简单使…

Linux常见指令(2)

目录 1、tar指令 &#xff01; 2、bc指令 3、uname 4、重要热键 5、关机 1、tar指令 &#xff01; 功能&#xff1a;压缩/解压缩文件或目录,类似zip 我们先来看一下我们的文件即目录&#xff0c;接下来我们输入指令&#xff1a; tar -czf test.tgz test 压缩 -c &#xf…

操作系统-复试笔记

http://t.csdnimg.cn/PJLWh 操作系统基础 什么是操作系统&#xff1f; 操作系统&#xff08;Operating System&#xff0c;简称 OS&#xff09;是管理计算机硬件与软件资源的程序&#xff0c;是计算机的基石。操作系统本质上是一个运行在计算机上的软件程序 &#xff0c;用于…

只需三步即可更改centos7系统语言,centos7系统语言更换,centos7系统中文互换

只需三步即可更改centos7系统语言,centos7系统语言更换,centos7系统中文互换 操作系统&#xff1a;centOS7.8 64位 ssh登录工具:FinalShell FinalShell可以点此下载 先查看系统的默认语言 locale #zh_CN 中文如何验证是中文&#xff0c;可以使用umtui来验证 umtui是一款…

Vue3路由组件练习

Vue3 路由组件练习 演示效果代码分析 安装 vue-router创建路由文件创建路由实例使用 router-link 组件导航 代码实现 index.js 文件App 文件 1. 演示效果 2. 代码分析 2.1. 安装 vue-router 命令&#xff1a;npm i vue-router 应用插件&#xff1a;Vue.use(VueRouter) 2.2…

C# 中 SQLite 查询数据库表中字段(列)是否存在的方法

查询SQLite数据库表中字段&#xff08;列&#xff09;存在的方法 使用SQL语句为&#xff1a;PRAGMA table_info([DeviceTrees]); 其中“DeviceTrees”为数据库表的名称。 使用SQLite Expert Professional工具&#xff0c;查看该语句是否起作用&#xff0c;这里使用的版本是…

神经网络系列---分类度量

文章目录 分类度量混淆矩阵&#xff08;Confusion Matrix&#xff09;&#xff1a;二分类问题二分类代码多分类问题多分类宏平均法:多分类代码多分类微平均法&#xff1a; 准确率&#xff08;Accuracy&#xff09;&#xff1a;精确率&#xff08;Precision&#xff09;&#xf…

[Linux]文件基础-如何管理文件

回顾C语言之 - 文件如何被写入 fopen fwrite fread fclose fseek … 这一系列函数都是C语言中对文件进行的操作&#xff1a; int main() {FILE* fpfopen("text","w");char str[20]"write into text";fputs(str,fp);fclose(fp);return 0; }而上…

Linux安装jdktomcatMySQl一战完成

一、jdk安装具体步骤 1、查询是否有jdk java -version 2、进入opt目录 cd /opt/ 连接服务器工具 进入opt目录&#xff0c;把压缩文件上传 查询是否查询成功 进入解压到的目录 cd /usr/local/创建新文件夹 mkdir java 再回到opt目录进行解压 cd /opt 解压到刚刚创建的文…

Golang Redis:构建高效和可扩展的应用程序

利用Redis的闪电般的数据存储和Golang的无缝集成解锁协同效应 在当前的应用程序开发中&#xff0c;高效的数据存储和检索的必要性已经变得至关重要。Redis&#xff0c;作为一个闪电般快速的开源内存数据结构存储方案&#xff0c;为各种应用场景提供了可靠的解决方案。在这份完…

c语言笔记typedef与输出输入

一typedef 1.1概述 &#xff1a;C 语言提供了 typedef 关键字&#xff0c;可以使用它来为类型取一个新的名字 示例&#xff1a;我们可以看到这里 我们将unsigned char 新起一个名字为 BYTE 后面我们定义此类型的变量的时候就可以使用BYTE typedef unsigned char BYTE;BYTE …

echarts series中的data属性添加动态数据后不显示问题,一处儿异步细节问题

当从后端获取到数据后&#xff0c;发现饼图并没有顺利加载数据出来&#xff0c;使用console.log()测试先后执行顺序&#xff0c;会发现饼图的方法会比请求先执行 此时就可以把饼图的方法放入到请求执行结束后 以下为修改前&#xff1a; 修改后&#xff1a; 一处儿异步的细节问…

【计算机网络】网络基础知识

一. 网络发展史 独立模式&#xff08;单机模式&#xff09;&#xff1a;计算机之间相互独立&#xff0c;各自拥有独立的数据。 网络互连&#xff1a;将多台计算机连接在一起&#xff0c;完成数据共享。 随着时代的发展&#xff0c;越来越需要计算机之间进行互相通信&#…

相机选型介绍

摄影测量中&#xff0c;相机是非常重要的角色&#xff0c;合适的相机产出合适的图像&#xff0c;得到合适的重建精度&#xff0c;这是相机的重要性。 您也许第一反应是&#xff0c;摄影测量所需的理想相机&#xff0c;是有着超高分辨率的相机&#xff0c;但事实可能并非如此&a…

【数据结构-字符串 五】【字符串转换】字符串转为整数

废话不多说&#xff0c;喊一句号子鼓励自己&#xff1a;程序员永不失业&#xff0c;程序员走向架构&#xff01;本篇Blog的主题是【字符串转换】&#xff0c;使用【字符串】这个基本的数据结构来实现&#xff0c;这个高频题的站点是&#xff1a;CodeTop&#xff0c;筛选条件为&…