Spring Security学习(六)——配置多个Provider(存在两种认证规则)

news2024/9/22 22:29:28

前言

《Spring Security学习(五)——账号密码的存取》一文已经能满足一般应用的情况。但实际商业应用也会存在如下的情况:用户提交的账号密码,能在本地的保存的账号密码匹配上,或者能在远端服务认证中匹配上,就算认证通过。这种通常类似于单点登录,或者认证中心之类的。本文不去探索这种架构,只是单纯讨论如果存在两种认证规则下如何处理。

多种认证方式使用Spring Security时有好几种处理方式。根据不同的情况,架构师考虑不同的解决方案。本文先从比较简单的方案说起。

假设能匹配数据库保存的账号密码,或者能匹配内存中保存的账号密码两者之一,就算认证通过。附加条件:

  1. 这两种认证方式是通过同一方式提交用户密码;
  2. 这两种认证方式是一条链式的处理方式。比如先判断第一种方式是否通过认证,不通过则判断第二种。

上述情况,我们考虑使用多个provider去处理。

架构

前面的文章讲过ProviderManager是AuthenticationManager的实现。其架构图如下:

在《Spring Security学习(五)——账号密码的存取》 中,我们说过DaoAuthenticationProvider撬动UserDetailsService和PasswordEncoder的“杠杆”。DaoAuthenticationProvider则是AuthenticationProvider的默认实现。Spring Security通过AuthenticationProvider调用获取UserDetails,然后进行匹配,最后返回UsernamePasswordAuthenticationToken(Authentication接口的具体)。

对于Spring Security的当存在多个AuthenticationProvider的实现时,用户提交的账号密码满足其中任意一个AuthenticationProvider,返回正确的Authentication即可。

自定义AuthenticationProvider

我们做个简单的自定义AuthenticationProvider。自定义MyProvider类:

@Data
public class MyProvider 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.loadUserByMemory(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);
		}
	}
}

自定义Provider应该实现AuthenticationProvider接口,并实现authenticate方法。这里我们参考DaoAuthenticationProvider来写,重写retrieveUser方法调用我们自定义的userDetailsServiceImpl的loadUserByMemory方法从内存获取。additionalAuthenticationChecks方法的作用是对提交的密码和系统中保存的密码进行匹配,这里直接参考DaoAuthenticationProvider类的写法。prepareTimingAttackProtection和mitigateAgainstTimingAttack都是用来防御计时攻击的,非认证必要。

在《Spring Security学习(二)——使用数据库保存密码》中我们定义了SysUserServiceImpl,这里进行一些修改:

@Component
public class UserDetailsServiceImpl implements UserDetailsService{

	@Autowired
	private SysUserService userService;
	
	private static Map<String, SysUserEntity> userMap;
	
	static {
		userMap = new HashMap<String, SysUserEntity>();
		SysUserEntity sysUser = new SysUserEntity();
		sysUser.setUsername("test");
		sysUser.setPassword("test###");
		userMap.put("test", sysUser);
	}
	
	@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;
	}
}

首先我们增加一个loadUserByUsername方法,用于读取map中保存的测试账号。另外之前当我们查询找不到对象的时候,抛出了RuntimeException,按照Spring Security的设计思想来说是不对的,应该抛出UsernameNotFoundException让上层接住这个异常。抛出RuntimeException会直接终止后面provider的匹配,不符合Spring Security的设计理念。

自定义密码加密器还是使用之前文章的版本MyPasswordEncoder,为原密码后加3个#:

public class MyPasswordEncoder implements PasswordEncoder {

	@Override
	public String encode(CharSequence rawPassword) {
		return rawPassword + "###";
	}

	@Override
	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		if (encodedPassword.equals(rawPassword + "###")) {
			return true;
		}
		return false;
	}
}

另外由于自定义加密方式,所以数据库密码也要改一下:

最后修改一下WebSecurityConfig的配置,把自定义的MyProvider加进去:

@EnableWebSecurity
public class WebSecurityConfig{
	
	@Bean
	public MyPasswordEncoder PasswordEncoder() {
		return new MyPasswordEncoder();
	}

	@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);
		return http.build();
	}
}

16行新建MyProvider,17行设置自定义的加密器,18-19行设置UserDetailsServiceImpl。20行加到HttpSecurity中。22行设置DaoAuthenticationProvider到HttpSecurity中。

之后我们同样是访问/hello路径,然后在默认登录页分别尝试使用jake/123,test/test两个账号登陆。会发现通过两种认证方式都可以登陆成功。

小结

本文使用自定义Provider的方式让用户匹配多种认证方式。这个只是最简单的方式。从系列文章的本文章开始,就涉及很多需要了解Spring Security架构设计的情况,读者最好还是阅读源码去理解。否则生搬硬套很难达到实际开发想要达到的目的。本文主要涉及ProviderManager、DaoAuthenticationProvider、AbstractUserDetailsAuthenticationProvider的源码。读者最好阅读一下以了解本文实现的思路。

其实还有个小疑问,WebSecurityConfig似乎我们去掉DaoAuthenticationProvider的配置,一样能达到同样的效果,那我们的配置是不是有点多余了?其实这涉及另一个概念,留到下一篇文章再讲。

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

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

相关文章

171基于matlab的随机共振微弱信号检测

基于matlab的随机共振微弱信号检测&#xff0c;随机共振描述了过阻尼布朗粒子受周期性信号和随机噪声的共同作用下,在非线性双稳态系统中所发生的跃迁现象. 随机共振可用于弱信号的检测。程序已调通&#xff0c;可直接运行。

Linux用到的命令

1 压缩文件 tar -czf wonderful.tar.gz pm 这个命令的作用就是创建一个以.tar.gz结尾的包文件&#xff0c;然后调用gzip程序将当前目录下的pm文件夹压缩到这个以.tar.gz结尾的文件里面去

Colmap学习笔记(一):Pixelwise View Selection for Unstructured Multi-View Stereo论文阅读

1. 摘要 本文展示一套MVS系统&#xff0c;该系统利用非结构化的图片实现鲁棒且稠密的建模。本文的主要贡献是深度和法向量的联合估计&#xff0c;用光度和几何先验进行像素筛选&#xff0c;多视图几何一致项&#xff0c;该项同时进行精修和基于图片的深度和法向量的融合。在标…

C2-1.6 Dropout正则化——提高泛化能力

C2-1.6 Dropout正则化——提高泛化能力 1、参考书籍 2、什么是Dropout正则化 以图一为例&#xff1a; 假设在训练图一所示的神经网络&#xff0c;它存在过拟合&#xff08;模型过于复杂情况&#xff09;&#xff0c;dropout 会遍历网络的每一层&#xff08;每一层设置的阈值不…

StarRocks加速查询——低基数全局字典

前言 StarRocks-2.0引入了低基数全局字典&#xff0c;可以通过全局字典将字符串的相关操作转换成整型相关操作&#xff0c;极大提升了查询性能。StarRocks 2.0后的版本默认会开启低基数字典优化。 一、低基数字典 对于利用整型替代字符串进行处理&#xff0c;通常使用字典编码…

Docker之查看并获取最新Ubuntu镜像(十)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

在vue3中使用canvas实现雨滴效果

在vue3中使用canvas实现雨滴效果 这是封装的一个组件DotAndRain&#xff08; &#xff09; <script setup> import { ref, onMounted } from "vue"; import { onUnmounted } from "vue";let animationFrameId null;const el ref(null); let canv…

Java零基础 - 赋值运算符

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互相学习&#xff0c;一个人虽可以走的更快&#xff0c;但一群人可以走的更远。 我是一名后…

redis入门指南

文章目录 Redis概述Redis基本数据类型Redis与MySQL的区别以及使用场景如何保持双写一致性&#xff08;缓存一致性&#xff09;1. 延迟双删2. 分布式锁&#xff08;强一致性时使用&#xff09;3. 中间件 Redis持久化机制RDB&#xff08;redis database&#xff09;AOF&#xff0…

Python算法100例-1.10 数制转换

完整源代码项目地址&#xff0c;关注博主私信源代码后可获取 1.问题描述2.问题分析3.算法设计4.确定程序框架5.字符与数字进行转换6.其他数制转换成十进制7.十进制转换成其他数制8.完整的程序 1&#xff0e;问题描述 给定一个M进制的数x&#xff0c;实现对x向任意一个非M进制…

SwiftUI 集合视图(Grid)拖放交换 Cell 的极简实现

概览 自从 SwiftUI 横空出世那天起&#xff0c;小伙伴们都感受到了它惊人的简单与便捷。而在本课中&#xff0c;我们将会用一个小“栗子”更直观的让大家体验到它无与伦比简洁的描述性特质&#xff1a; 如上图所示&#xff0c;我们在 SwiftUI 中实现了 Grid 中拖放交换 Cell 的…

开开开开开,干

大家新年快乐&#xff0c;开工啦啦啦啦 其实每天很多人都会问&#xff1a; 有协同过滤的算法吗&#xff0c;有的&#xff0c;可以给你解释原理… 有的小伙伴只开了一部分逻辑&#xff0c;我要实现用户可以下单功能 但是细细考虑下单&#xff0c;需要现有用户&#xff0c;维护…

[Git] 配置Access Token 解决Github 认证弹窗

[Git] 配置Access Token 解决Github 认证弹窗 1. 前言2. 解决2.1 申请Personal Access Token2.2. 配置Token2.3. 授权激活Token 博主热门文章推荐&#xff1a; 1. 前言 最近从bitbucket切换到了Github Enterprise, 刚使用几次发现 每次操作 都有弹窗认证&#xff0c; 虽然手动点…

波奇学Linux:进程通信管道

进程通信 管道&#xff1a;基于文件级别的单向通信 创建父子进程&#xff0c;使得进程的struct file*fd_array[]的文件描述符指向同一个struct file文件&#xff0c;这个文件是内存级文件。 父进程关写端&#xff0c;子进程再关闭读端。实现单向通信 子进程写入&#xff0c;父进…

个人博客搭建

使用彩虹云主机百度云域名WordPress 下载WordPress https://cn.wordpress.org/ 购买主机 购买彩虹云主机&#xff0c;购买香港高防主机https://www.cccyun.net/ 购买之后点击 管理 进入后点 击前往控制面板 -> 一键登录控制面板 可进入控制面板。 选择文件管理 在线…

cuda加速:memory coalescing,Bank Conflicts

cuda加速&#xff1a;memory coalescing 1.memory coalescing2.Shared Memory Bank Conflicts参考文献 1.memory coalescing 参考【1】中给出的定义&#xff1a;一个warp中&#xff0c;thread 0到thread 31访问连续的内存空间&#xff0c;则这些线程的访问被合并为一次访问。 …

2.21学习总结

1.【模板】ST 表 2.Balanced Lineup G 3.景区导游 4.最近公共祖先&#xff08;LCA&#xff09; 倍增思想&#xff1a;主要用于LCA问题&#xff0c;RMQ问题。在进行 递推 时&#xff0c;如果 状态空间很大&#xff0c;通常的 线性递推 无法满足 时间 与 空间复杂度 的要求&…

BabylonJS 6.0文档 Deep Dive 动画(一):动画介绍

1. 动画介绍 无论动画如何实现&#xff0c;它都必须考虑所需的动作、时间、产生所需流动性所需的帧数以及序列中的关键点。这个介绍应该有助于理解Babylon.js是如何进行动画的&#xff0c;以及它们是如何实现的。 动画由一系列图像、帧生成&#xff0c;这些图像、帧一个接一个地…

做跨境电商,为什么要建独立站,2024年的机会在哪里?一次性讲清楚...

做跨境电商&#xff0c; 是选择依托第三方平台&#xff1f; 还是自建独立网站&#xff1f; 01 什么是自建独立站 最简单的说法&#xff1a;独立站对于我们跨境电商这个行业来说&#xff0c;就是那些不是主流平台的网站&#xff0c;是某个企业或者个人自己做和运营的搭建电商…

记录一次调用奇门api报错信息Invalid signature (签名无效)

奇门有msg对应错误的记录 我可能是 3.1 以下是还原场景 第一个方法 private static String getSellerItem() throws IOException {Map<String, String> params new HashMap<String, String>();// 公共参数params.put("page_index", "100"…