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

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

相关文章

AI:135-基于卷积神经网络的艺术品瑕疵检测与修复

🚀点击这里跳转到本专栏,可查阅专栏顶置最新的指南宝典~ 🎉🎊🎉 你的技术旅程将在这里启航! 从基础到实践,深入学习。无论你是初学者还是经验丰富的老手,对于本专栏案例和项目实践都有参考学习意义。 ✨✨✨ 每一个案例都附带关键代码,详细讲解供大家学习,希望…

Mybatis总结--传参二

#叫做占位符 Mybatis是封装的JDBC 增强版 内部还是用的jdbc 每遇到一个#号 这里就会变为&#xff1f;占位符 一个#{}就是对应一个问号 一个占位符 用这个对象执行sql语句没有sql注入的风险 八、多个参数-使用Param 当 Dao 接口方法有多个参数&#xff0c;需要通过名称使…

力扣随笔之颜色分类(中等75)

思路&#xff1a;定义两个指针划分left&#xff0c;right划分三个区域left左边是红色区域&#xff0c;right右边是蓝色区域&#xff0c;left和right之间是白色区域&#xff1b;定义一个遍历指针遍历整个数组&#xff0c;遇到红色与left所指位置数字交换&#xff0c;并将left自加…

Element table 实现表格行、列拖拽功能

安装包 npm install sortablejs --save <template><div class"draggable" style"padding: 20px"><el-table row-key"id" :data"tableData" style"width: 100%" border><el-table-columnv-for"(it…

趣学贝叶斯统计:贝叶斯定理和乐高积木

利用贝叶斯定理&#xff0c;可以将条件概率倒置。知道P(B|A)&#xff0c;就可以求出P(A|B)。例如&#xff0c;知道感冒时你打喷嚏的概率&#xff0c;就可以倒过来判断打喷嚏时你感冒的概率。这样&#xff0c;我们就用数据更新了自己对世界的信念。 目录 1. 运用乐高2. 通过数学…

SpringBoot和SpringCloud的区别,使用微服务的好处和缺点

SpringBoot是一个用于快速开发单个Spring应用程序的框架&#xff0c;通过提供默认配置和约定大于配置的方式&#xff0c;快速搭建基于Spring的应用。让程序员更专注于业务逻辑的编写&#xff0c;不需要过多关注配置细节。可以看成是一种快速搭建房子的工具包&#xff0c;不用从…

C语言:指针的进阶讲解

目录 1. 二级指针 1.1 二级指针是什么&#xff1f; 1.2 二级指针的作用 2. 一维数组和二维数组的本质 3. 指针数组 4. 数组指针 5. 函数指针 6. typedef的使用 7. 函数指针数组 7.1 转移表 1. 二级指针 如果了解了一级指针&#xff0c;那二级指针也是可以很好的理解…

【安卓逆向】app防止截屏分析与去除

本次分析的app name为&#xff1a;5paH5qGI54uX 这款应用打开之后里面的内容是不允许截图的&#xff0c;防止截图分析&#xff1a;Android应用防止截屏_landroid/view/window;->setflags 0x2000-CSDN博客 App防止恶意截屏功能的方法&#xff1a;iOS、Android和鸿蒙系统的实…

红日靶场3

靶场链接&#xff1a;漏洞详情 在虚拟机的网络编辑器中添加两个仅主机网卡 信息搜集 端口扫描 外网机处于网端192.168.1.0/24中&#xff0c;扫描外网IP端口&#xff0c;开放了80 22 3306端口 80端口http服务&#xff0c;可以尝试登录网页 3306端口mysql服务&#xff0c;可…

7-liunx服务器规范

目录 概况liunx日志liunx系统日志syslog函数openlog 可以改变syslog默认输出方式 &#xff0c;进一步结构化 用户信息进程间的关系会话ps命令查看进程关系 系统资源限制改变工作目录和根目录服务器程序后台话 概况 liunx服务器上有很多细节需要注意 &#xff0c;这些细节很重要…

nodejs+vue+ElementUi废品废弃资源回收系统

系统主要是以后台管理员管理为主。管理员需要先登录系统然后才可以使用本系统&#xff0c;管理员可以对系统用户管理、用户信息管理、回收站点管理、站点分类管理、站点分类管理、留言板管理、系统管理进行添加、查询、修改、删除&#xff0c;以保障废弃资源回收系统系统的正常…

瑞_23种设计模式_装饰者模式

文章目录 1 装饰者模式&#xff08;Decorator Pattern&#xff09;1.1 介绍1.2 概述1.3 装饰者模式的结构 2 案例一2.1 需求2.2 代码实现 3 案例二3.1 需求3.2 代码实现 4 JDK源码解析5 总结5.1 装饰者模式的优缺点5.2 装饰者模式的使用场景5.3 装饰者模式 VS 代理模式 &#x…

创作纪念日:记录我的成长与收获

机缘 一开始是在我深入学习前端知识的Vue.js框架遇到了一个问题&#xff0c;怎么都解决不了&#xff0c;心烦意乱地来csdn上找解决方法。开心的是真被我找到了&#xff0c;真的很感恩&#xff0c;也意识到在这个平台上分享自己的经验是多么有意义的事情&#xff0c;可能随便的…

Python爬虫-付费代理推荐和使用

付费代理的使用 相对免费代理来说&#xff0c;付费代理的稳定性更高。本节将介绍爬虫付费代理的相关使用过程。 1. 付费代理分类 付费代理分为两类&#xff1a; 一类提供接口获取海量代理&#xff0c;按天或者按量收费&#xff0c;如讯代理。 一类搭建了代理隧道&#xff0…

【前端素材】推荐优质后台管理系统Welly平台模板(附源码)

一、需求分析 后台管理系统&#xff08;或称作管理后台、管理系统、后台管理平台&#xff09;是一种专门用于管理网站、应用程序或系统后台运营的软件系统。它通常由一系列功能模块组成&#xff0c;为管理员提供了管理、监控和控制网站或应用程序的各个方面的工具和界面。以下…

WSL2配置Linux、Docker、VS Code、zsh、oh my zsh(附Docker开机自启设置)

0. 写在前面 本篇笔记来自于UP主麦兜搞IT的合集视频Windows10开发环境搭建中的部分内容 1. 安装WSL2 按照微软官方文档进行操作&#xff0c;当然也可以直接wsl --install 也可以按照 旧版手动安装的步骤 来进行操作 选择安装的是Ubuntu 20.04 LTS 注&#xff1a;WSL默认安装…

NATS学习笔记(一)

NATS是什么&#xff1f; NATS是一个开源的、轻量级、高性能的消息传递系统&#xff0c;它基于发布/订阅模式&#xff0c;由Apcera公司开发和维护。 NATS的功能 发布/订阅&#xff1a;NATS的核心是一个发布/订阅消息传递系统&#xff0c;允许消息生产者发布消息到特定的主题…

如何使用ArcGIS Pro生成等高线

无论在制图还是规划中&#xff0c;经常会使用到等高线&#xff0c;大多数情况下&#xff0c;从网上获取的高程数据都是DEM文件&#xff0c;我们可以通过ArcGIS Pro来生成等高线&#xff0c;这里为大家介绍一下生成方法&#xff0c;希望能对你有所帮助。 数据来源 教程所使用的…

解析OOM的三大场景,原因及实战解决方案

目录 一、什么是OOM 二、堆内存溢出&#xff08;Heap OOM&#xff09; 三、方法区内存溢出&#xff08;Metaspace OOM&#xff09; 四、栈内存溢出&#xff08;Stack OOM&#xff09; 一、什么是OOM OOM 是 Out Of Memory 的缩写&#xff0c;意思是内存耗尽。在计算机领域…

vue3+js 实现记住密码功能

常见的几种实现方式 1 基于spring security 的remember me 功能 ​​​​​​​ localStorage 除非主动清除localStorage 里的信息 &#xff0c;不然永远存在&#xff0c;关闭浏览器之后下次启动仍然存在 存放数据大小一般为5M 不与服务器进行交互通信 cookies 可以…