Spring Security 源码解读 :认证总览

news2024/12/23 23:32:06

Spring Security 提供如下几种认证机制:

  • Username & Password
  • OAuth2.0 Login
  • SAML 2.0 Login
  • Remember Me
  • JAAS Authentication
  • Pre-authentication Scenarios
  • X509 Authentication

这里使用Spring Boot 2.7.4版本,对应Spring Security 5.7.3版本

Servlet Authentication Architecture

首先明确两个概念:

  • Principle : This interface represents the abstract notion of a principal, which can be used to represent any entity, such as an individual, a corporation, and a login id. 简单来说 可以认为是 唯一确定用户的 一个 userId ,但这个Principle是一个接口,具体参考 java.security.Principal
  • Credential : 通常就是一个密码,但他不是接口,而是通过接口Authentication#getCredentials来获取,返回一个Object类型。

Spring Security提供了以下几个核心类:

  • SecurityContextHolder : Spring Security用来保存被认证用户的详细信息(默认使用ThreadLocal保存);
  • SecurityContext : 从 SecurityContextHolder 获取得到,该接口提供被认证用户的 Authentication信息;
  • Authentication : 代表"认证",其中包含PrincipleCredential,通过AuthenticationManager#authenticate来认证一个Authentication,该方法接受一个未认证的Authentication并返回一个认证后的Authentication
  • GrantedAuthority : 认证后的Principle包含的权限
  • AuthenticationManager : 实施认证行为的接口,该类为函数式接口,只有一个方法: Authentication authenticate(Authentication authentication) throws AuthenticationException;
  • ProviderManager : the most common implementation of AuthenticationManager.
  • AuthenticationProvider : 提供认证方式的接口,组合在ProviderManager中。

在这里插入图片描述

SecurityContextHolder

先来看一个使用SecurityContextHolderSecurityContext完成认证的案例:

SecurityContext context = SecurityContextHolder.createEmptyContext();
// 手动生成一个Authentication,实际一般是通过数据库查出来生成的
Authentication authentication =
    new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);

SecurityContextHolder.setContext(context);

默认 情况下 SecurityContext存储使用ThreadLocal方式,这个就是ThreadLocal的一个用处,避免函数间参数传递的复杂性,只要处于一个线程,就可以直接获取而不需要通过函数参数返回值来传递。

注意:在使用ThreadLocal时,由于键是其this本身,是一个弱引用,而值只能是强引用,所以ThreadLocal不用时需要手动clear。而Spring Security中,在 FilterChainProxy中完成这一清除工作,如下:

public class FilterChainProxy extends GenericFilterBean {
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		...
		try {
			...
			// 执行Spring Security 的 SecurityFilterChain
			doFilterInternal(request, response, chain);
		}
		catch (Exception ex) {
			...
		}
		finally {
			// ***************
			// 清除ThreadLocal
			// ***************
			SecurityContextHolder.clearContext();
			request.removeAttribute(FILTER_APPLIED);
		}
	}


}

但有些情形是用不了ThreadLocal的,For example, a Swing client might want all threads in a Java Virtual Machine to use the same security context 。针对其他情况,SecurityContextHolder提供了4种模式,当然也可以自定义:

  1. MODE_THREADLOCAL
  2. MODE_GLOBAL
  3. MODE_INHERITABLETHREADLOCAL
  4. MODE_PRE_INITIALIZED

可以通过2中方式去修改模式:

  1. set a system property
  2. a static method on SecurityContextHolder
public class SecurityContextHolder {
	// 存储SecurityContext的模式,默认 ThreadLocal存储
	public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
	public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
	public static final String MODE_GLOBAL = "MODE_GLOBAL";

	private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";

	public static final String SYSTEM_PROPERTY = "spring.security.strategy";
	// 通过系统参数指定 存储模式
	private static String strategyName = System.getProperty(SYSTEM_PROPERTY);

	// 实际存储SecurityContext的接口(类)
	private static SecurityContextHolderStrategy strategy;

	private static int initializeCount = 0;

	static {
		// 初始化
		initialize();
	}

	private static void initialize() {
		initializeStrategy();
		initializeCount++;
	}

	private static void initializeStrategy() {
		...
		if (!StringUtils.hasText(strategyName)) {
			// Set default 默认 ThreadLocal
			strategyName = MODE_THREADLOCAL;
		}
		if (strategyName.equals(MODE_THREADLOCAL)) {
			strategy = new ThreadLocalSecurityContextHolderStrategy();
			return;
		}
		...
	}
	// 修改Strategy
	public static void setStrategyName(String strategyName) {
		SecurityContextHolder.strategyName = strategyName;
		initialize();
	}
	// 自定义Strategy
	public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) {
		Assert.notNull(strategy, "securityContextHolderStrategy cannot be null");
		SecurityContextHolder.strategyName = MODE_PRE_INITIALIZED;
		SecurityContextHolder.strategy = strategy;
		initialize();
	}
	
	public static SecurityContext getContext() {
		return strategy.getContext();
	}
	public static void setContext(SecurityContext context) {
		strategy.setContext(context);
	}
	public static void clearContext() {
		strategy.clearContext();
	}

可以看到,SecurityContextHolder实际上是一个门面,具体的Context存储在SecurityContextHolderStrategy中,来看看该接口默认的实现 ThreadLocalSecurityContextHolderStrategy :

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
	// ThreadLocal
	private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

	@Override
	public void clearContext() {
		contextHolder.remove();
	}

	@Override
	public SecurityContext getContext() {
		SecurityContext ctx = contextHolder.get();
		if (ctx == null) {
			ctx = createEmptyContext();
			contextHolder.set(ctx);
		}
		return ctx;
	}

	@Override
	public void setContext(SecurityContext context) {
		Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
		contextHolder.set(context);
	}

	@Override
	public SecurityContext createEmptyContext() {
		return new SecurityContextImpl();
	}

}

AuthenticationManager

public interface AuthenticationManager {
	// 传入一个待认证的Authentication
	// 返回 a fully authenticated object including credentials
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

AuthenticationManager的最常用实现类 ProviderManagerProviderManager调用多个AuthenticationProvider来认证传入的Authentication,只要有一个认证成功即可返回(返回nonNull),否则会抛出异常,AuthenticationManager默认支持三种异常:

  • DisabledException : 用户账号被disabled
  • LockedException : 用户账号被locked
  • BadCredentialsException : credentials错误(密码错误)

/**
 * ProviderManager中的List<AuthenticationProvider>会按顺序认证,知道有一个返回非空。
 * 如果后面的 AuthenticationProvider返回非空认证结果,前面抛出的异常统统清除;如果后面还有异常,以第一个异常为准
 * 
 * 该类中有一个 parent 的字段,类型也为AuthenticationManager,
 * 如果该类中的List<AuthenticationProvider>都不能认证,会调用parent认证,这个不常用。
 * 
 * 事件发布:
 * ProviderManager中认证事件发布委托给 AuthenticationEventPublisher 实现,默认是空实现。
 * parent 的 ProviderManager中不要实现 Publisher,否则会重复发布。
 **/

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
	// 事件发布,默认空实现
	private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
	// 实际认证的AuthenticationProvider
	private List<AuthenticationProvider> providers = Collections.emptyList();
	// 父级认证Manager
	private AuthenticationManager parent;

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			...
			result = provider.authenticate(authentication);
			if (result != null) {
				copyDetails(authentication, result);
				break;
			}
			...
		}
		// 该类中AuthenticationManager都判断完了,结果还是空,调用parent开始认证
		if (result == null && this.parent != null) {
			// Allow the parent to try.
			parentResult = this.parent.authenticate(authentication);
		}
		...
		
	// AbstractAuthenticationToken就是Authentication接口的实现类,模板模式
	// 常用的UsernamePasswordAuthenticationToken和OAuth2AuthenticationToken都extends这个Abstract类
	private void copyDetails(Authentication source, Authentication dest) {
		if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
			AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
			token.setDetails(source.getDetails());
		}
	}
}

可以看到 ProviderManager implements AuthenticationManager将认证工作进一步分配给 AuthenticationProvider,而这个AuthenticationProvider会根据支持的认证方式来认证,所以这个接口除了认证方法还有一个是否支持认证的判断方法:

public interface AuthenticationProvider {

	Authentication authenticate(Authentication authentication) throws AuthenticationException;

	boolean supports(Class<?> authentication);

AuthenticationProvider常见的实现类有:

  • DaoAuthenticationProvider : An AuthenticationProvider implementation that retrieves user details from a UserDetailsService. 用于UsernamePassword认证方式
  • OAuth2AuthorizationCodeAuthenticationProvider : 在授权服务器上认证authorization_code,拿着code换accessToken
  • OAuth2LoginAuthenticationProvider : 在授权服务器上认证authorization_code,拿着code换accessToken,此外,还会拿着accessToken换取UserInfo,属于上面的加强版。
  • OidcAuthorizationCodeAuthenticationProvider : 同上,不过在OAuth2.0基础上加了OpenID Connect协议。

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

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

相关文章

LeetCode题目笔记——1588. 所有奇数长度子数组的和

文章目录题目描述题目难度——简单方法一&#xff1a;暴力代码/C代码/Python方法二&#xff1a;前缀和代码/C代码/Python总结题目描述 给你一个正整数数组 arr &#xff0c;请你计算所有可能的奇数长度子数组的和。 子数组 定义为原数组中的一个连续子序列。 请你返回 arr 中…

MySql性能优化(六)索引监控

文章目录索引监控Handler_read_firstHandler_read_keyHandler_read_lastHandler_read_nextHandler_read_prevHandler_read_rndHandler_read_rnd_next索引监控 SHOW STATUS LIKE Handler_read%解释一下各个参数的含义 Handler_read_first 通过index获取数据的次数 Handler_r…

在cmd中遍历局域网内的IP命令解析

简单的方法 1&#xff0c;直接通过浏览器访问路由器&#xff0c;通过路由器的页面查看。2&#xff0c;网络中很多扫描网络的软件&#xff0c;3&#xff0c;自己使用cmd命令查看 有时候自己也觉得&#xff0c;有简单的方式还用这麻烦的干嘛。但遇到不知道路由的登录密码呢&…

Djiango零基础-快速了解基本框架笔记-附案例

初识Djiango 1. 安装djiango pip install django4.1 -i https://mirrors.aliyun.com/pypi/simple/C:\python38- python.exe- Scripts- pip.exe- djiango-admin.exe 【工具&#xff0c;创建djiango项目】- Lib- 内置模块- site-packages- openpyxl- python-docx- flask- djia…

IPV6实验(2.3)

目标&#xff1a; 一、首先将r2、r3、r4这个公网先弄通 [r2]int gi 0/0/0 [r2-GigabitEthernet0/0/0]ip add 23.1.1.1 24 [r3]int gi 0/0/0 [r3-GigabitEthernet0/0/0]ip add 23.1.1.2 24 [r3-GigabitEthernet0/0/0]int gi 0/0/1 [r3-GigabitEthernet0/0/1]ip add 34.1.1.1 2…

YOLO的学习

如何评价Alexey Bochkovskiy团队提出的YoloV7&#xff1f; - 知乎 1, Selective Search&#xff0c;RCNN和FasterRCNN 机器视觉(CV) 超简指南 选择性搜索 Selective Search_哔哩哔哩_bilibili 【精读RCNN】03选择性搜索&#xff0c;selective search_哔哩哔哩_bilibili …

win10系统安装

系统安装 文章目录系统安装1.工具下载2.制作启动盘3. win 10镜像下载4.进入PE系统1.工具下载 需要准备一个至少16 GB的U盘&#xff0c;工具下载链接 U盘&#xff1a;https://share.weiyun.com/aHhPh16e 迅雷&#xff1a;https://dl.xunlei.com/ win 10 镜像链接&#xff1a…

大咖说·计算讲谈社|当我们在谈目标时,究竟在谈什么?

本讲内容&#xff0c;节选自阿里巴巴研究员吴翰清&#xff08;道哥&#xff09;面向团队的内部讲话&#xff0c;经删减整理后&#xff0c;作为【计算讲谈社】第十六讲公开分享。 讲师介绍 吴翰清&#xff08;道哥&#xff09;&#xff1a;阿里巴巴研究员&#xff0c;阿里巴巴、…

33复杂美,上链不复杂

“链上复杂美&#xff0c;上链不复杂。” 33复杂美座落在美丽的西子湖畔&#xff1a;杭州&#xff0c;并在上海、南京、宁波、海南皆有设立分部。公司员工超过100人&#xff0c;70%为技术人员&#xff0c;吸引了来自甲骨文、阿里等优秀人才加盟。复杂美为浙江省区块链技术应用协…

windows 编译telegram桌面客户端

目的 主要是为了研究一下人家的软件架构。 步骤 前置条件 梯子至少10G硬盘空间 安装第三方应用 请更新至最新版。 GITCmakePython3visual studio 2022Qt Visual Studio Tools: Open Extensions -> Manage Extensions Go to Online tab Search for Qt Install Qt Visu…

系统学习Python——2D绘图库Matplotlib:绘图函数matplotlib.pyplot.plot

分类目录&#xff1a;《系统学习Python》总目录 matplotlib.pyplot是Matplotlib的基于状态的接口。它提供了一种隐式的、类似MATLAB的绘图方式。它还会在您的屏幕上打开图形&#xff0c;并充当图形GUI管理器。 语法 matplotlib.pyplot.plot(*args, scalexTrue, scaleyTrue , …

威联通NAS共享文件夹挂载到linux服务器下

威联通虚机中centos挂载共享文件夹设置方法 1、登录到QTS。 2、控制台–>Win/Mac/NFS选项&#xff0c;在Linux NFS中开启NFS v3或NFSv4服务。 3、设置文件夹权限&#xff1a; 控制台–>共享文件夹中&#xff0c;找到需要共享的文件夹&#xff0c;编辑文件夹权限。 选…

Golang数据竟态

本文以一个简单事例的多种解决方案作为引子&#xff0c;用结构体Demo来总结各种并发读写的情况 一个数据竟态的case package mainimport ("fmt""testing""time" )func Test(t *testing.T) {fmt.Print("getNum(): ")for i : 0; i <…

「Python|场景案例」如何将多个视频合并成多个子画面并排的单个视频?

本文主要介绍如何将多个视频画面合并到一个视频中&#xff0c;使得合成后的视频画面是原视频的并排画面。 文章目录场景描述准备工作处理步骤源代码处理效果展示场景描述 在某些音视频剪辑的场景下我们希望一个视频画面显示多个子画面&#xff0c;比如&#xff1a; 乐器演奏视…

【MySQL】数据库概述

文章目录1、数据库1.1 数据库作用1.2 数据库的相关概念1.3 数据库与数据库管理系统的关系2、MySQL3、RDBMS与非RDBMS3.1 关系型数据库(RDBMS)3.2 非关系型数据库(非RDBMS)3.2.1 键值型数据库3.2.2 文档型数据库3.2.3 搜索引擎数据库3.2.4 列式数据库3.2.5 图形数据库4、关系型数…

基于微信小程序的短视频管理系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端框架&#xff1a;VUE 数据库&#xff1a;MySQL5.7 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录 一、项目简介 二…

推荐一个跨平台支持Word, Excel, CSV, Email等30多种格式的操作库

更多开源项目请查看&#xff1a;一个专注推荐.Net开源项目的榜单 在我们日常项目开发中&#xff0c;经常需要解析操作文档&#xff0c;比如Office文档、Email文件、PDF、Xml、图片、Mp3等音频文件&#xff0c;操作Office、PDF文件我们需要用到IFilter&#xff0c;操作文本、图片…

【C++】引用与指针

专栏放在【C知识总结】&#xff0c;会持续更新&#xff0c;期待支持&#x1f339;引用引用的概念在C中&#xff0c;引用的本质其实就是给一个已经存在的变量”起别名“。也就是说&#xff0c;引用与它所引用的对象共用一块空间。&#xff08;同一块空间的多个名字&#xff09;就…

【搭建 mybatis 开发环境】

搭建 mybatis 开发环境 环境准备 创建数据库 创建maven 工程 pom文件&#xff1a;添加打包方式为jar 以及 mybatis 和 mysql 依赖 环境搭建 编写实体类 User.java&#xff1a;属性&#xff0c;生成get 和 set 方法 以及 toString方法 创建 IUserDao 接口&#xff0c;操作…

java进阶—一篇文章搞懂set 集合 及其底层实现

上节我们知道了List 下的两大 子类 ArrayList 跟 linkedList ArrayList 数组结构 查询快&#xff0c;增删慢 LinkedList 链表结构 查询慢&#xff0c;增删快 来看看我们今天的主角: Set Set 是 不可重复的&#xff0c;其底下也有两大子接口&#xff1a; HashSet&#xff1…