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

news2024/9/23 19:17:38

前言

《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;

	@Override
	protected 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);
		}
	}

	@Override
	protected 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{

	@Autowired
	private SysUserService userService;
	
	@Autowired
	private RedisTemplate redisTemplate;
	
	private static Map<String, SysUserEntity> userMap = new HashMap<String, SysUserEntity>();
	
	@Override
	public 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);
	}
	
	@Override
	public 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.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
        url: jdbc:mysql://127.0.0.1:3306/security_test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: 
  thymeleaf:
    prefix: classpath:/templates/
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    lettuce:
      pool:
        max-active: 500
        max-idle: 300
        max-wait: 1000
        min-idle: 0
    database: 8

增加一个配置类RedisConfig:

@Configuration
public class RedisConfig {

	@Bean
	public 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{
	
	@Bean
	public MyPasswordEncoder PasswordEncoder() {
		return new MyPasswordEncoder();
	}
	
	@Bean
	public 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;
	}

	@Bean
	public 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.coloradmin.cn/o/1467819.html

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

相关文章

2024年 Openai的API相关全部概论汇总(通用版)

2024年 Openai的API相关全部概论汇总&#xff08;通用版&#xff09; 文章目录 2024年 Openai的API相关全部概论汇总&#xff08;通用版&#xff09;一、前言1、python快速开始 二、Openai 平台以及相关项目1、Openai的API管理平台2、ChatGPT项目推荐&#xff08;1&#xff09;…

ONLYOFFICE桌⾯应⽤程序v8.0:功能丰富,⽀持多平台

文章目录 可填写的 PDF 表单RTL支持电子表格中的新增功能其他改进和新增功能与 Moodle 集成用密码保护 PDF 文件快速创建文档本地界面主题总结 继 ONLYOFFICE 文档 v8.0 的发布后&#xff0c;很高兴&#xff0c;因为适用于 Linux、Windows 和 macOS 的 ONLYOFFICE 桌面应用程序…

ZYNQ W25Q256FVEI flash启动烧写系统

烧写ZYNQ petalinux从flash启动&#xff0c;W25Q256FVEI是FLASH型号&#xff0c;发现BOO.BIN能启动&#xff0c;报错&#xff1a; Wrong Image Format for bootm command ERROR: cant get kernel image! 其中烧写配置image.ub偏移地址的时候&#xff0c;image.ub.bin偏移地址…

- 工程实践 - 《QPS百万级的有状态服务实践》05 - 持久化存储

本文属于专栏《构建工业级QPS百万级服务》 继续上篇《QPS百万级的有状态服务实践》04 - 服务一致性。目前我们的系统如图1。现在我们虽然已经尽量把相同用户的请求转发到相同的机器&#xff0c;并且在客户端做了适配。但是因为成本&#xff0c;更极端的情况下&#xff0c;服务依…

【Ubuntu】使用WSL安装Ubuntu

WSL 适用于 Linux 的 Windows 子系统 (WSL) 是 Windows 的一项功能&#xff0c;可用于在 Windows 计算机上运行 Linux 环境&#xff0c;而无需单独的虚拟机或双引导。 WSL 旨在为希望同时使用 Windows 和 Linux 的开发人员提供无缝高效的体验。安装 Linux 发行版时&#xff0c…

猫头虎分享已解决Bug || Error: Target container is not a DOM element (React) ‍

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

Kafka入门二——SpringBoot连接Kafka示例

实现 1.引入maven依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache…

Sqli-labs靶场第8关详解[Sqli-labs-less-8]

Sqli-labs-Less-8 前言&#xff1a; SQL注入的三个条件&#xff1a; ①参数可控&#xff1b;&#xff08;从参数输入就知道参数可控&#xff09; ②参数过滤不彻底导致恶意代码被执行&#xff1b;&#xff08;需要在测试过程中判断&#xff09; ③参数带入数据库执行。&#…

从源码学习单例模式

单例模式 单例模式是一种设计模式&#xff0c;常用于确保一个类只有一个实例&#xff0c;并提供一个全局访问点。这意味着无论在程序的哪个地方&#xff0c;只能创建一个该类的实例&#xff0c;而不会出现多个相同实例的情况。 在单例模式中&#xff0c;常用的实现方式包括懒汉…

Spring Boot与HikariCP:性能卓越的数据库连接池

点击下载《Spring Boot与HikariCP&#xff1a;性能卓越的数据库连接池》 1. 前言 本文将详细介绍Spring Boot中如何使用HikariCP作为数据库连接池&#xff0c;包括其工作原理、优势分析、配置步骤以及代码示例。通过本文&#xff0c;读者将能够轻松集成HikariCP到Spring Boot…

深度学习中数据的转换

原始&#xff08;文本、音频、图像、视频、传感器等&#xff09;数据被转化成结构化且适合机器学习算法或深度学习模型使用的格式。 原始数据转化为结构化且适合机器学习和深度学习模型使用的格式&#xff0c;通常需要经历以下类型的预处理和转换&#xff1a; 文本数据&#xf…

Java语言实现五子棋

目录 内容 题目 解题 代码 实现 内容 题目 五子棋 使用二维数组,实现五子棋功能. 1.使用二维数组存储五子棋棋盘 如下图 2.在控制台通过Scanner输入黑白棋坐标(例如:1,2 2,1格式 表示二维数组坐标),使用实心五角星和空心五角星表示黑白棋子. 如下图: 输入后重新输出…

Redis信创平替之TongRDS(东方通),麒麟系统安装步骤

我的系统: 银河麒麟桌面系统V10(SP1)兆芯版 1.先进入东方通申请使用 2.客服会发送一个TongRDS包与center.lic给你(我这里只拿到.tar.gz文件,没有网上的什么安装版) 3.上传全部文件到目录中 4.服务节点安装,并启动 tar -zxvf TongRDS-2.2.1.2_P3.Node.tar.gz cd pmemdb/bin/…

chatGPT 使用随想

一年前 chatGPT 刚出的时候&#xff0c;我就火速注册试用了。 因为自己就是 AI 行业的&#xff0c;所以想看看国际上最牛的 AI 到底发展到什么程度了. 自从一年前 chatGPT 火出圈之后&#xff0c;国际上的 AI 就一直被 OpenAI 这家公司引领潮流&#xff0c;一直到现在&#x…

探讨javascript中运算符优先级

如果阅读有疑问的话&#xff0c;欢迎评论或私信&#xff01;&#xff01; 本人会很热心的阐述自己的想法&#xff01;谢谢&#xff01;&#xff01;&#xff01; 文章目录 深入理解JavaScript运算符优先级运算符优先级概述示例演示示例1&#xff1a;加法和乘法运算符的优先级示…

VBA即用型代码手册:立即保护所有工作表Code及插入多工作表Code

我给VBA下的定义&#xff1a;VBA是个人小型自动化处理的有效工具。可以大大提高自己的劳动效率&#xff0c;而且可以提高数据的准确性。我这里专注VBA,将我多年的经验汇集在VBA系列九套教程中。 作为我的学员要利用我的积木编程思想&#xff0c;积木编程最重要的是积木如何搭建…

【Java程序设计】【C00277】基于Springboot的招生管理系统(有论文)

基于Springboot的招生管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的招生管理系统 本系统分为系统功能模块、管理员功能模块以及学生功能模块。 系统功能模块&#xff1a;在系统首页可以查看首页、专业…

UE蓝图 函数调用(CallFunction)节点和源码

系列文章目录 UE蓝图 Get节点和源码 UE蓝图 Set节点和源码 UE蓝图 Cast节点和源码 UE蓝图 分支(Branch)节点和源码 UE蓝图 入口(FunctionEntry)节点和源码 UE蓝图 返回结果(FunctionResult)节点和源码 UE蓝图 函数调用(CallFunction)节点和源码 文章目录 系列文章目录一、Call…

[LWC] Components Communication

目录 Overview ​Summary Sample Code 1. Parent -> Child - Public Setter / Property / Function a. Public Property b. Public getters and setters c. Public Methods 2. Child -> Parent - Custom Event 3. Unrelated Components - LMS (Lightning Message…

LeetCode206: 反转链表.

题目描述 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 解题方法 假设链表为 1→2→3→∅&#xff0c;我们想要把它改成∅←1←2←3。在遍历链表时&#xff0c;将当前节点的 next指针改为指向前一个节点。由于节点没有引用其前一…