SpringSecurity原理剖析与权限系统设计
Spring Secutity和Apache Shiro是Java领域的两大主流开源安全框架,也是权限系统设计的主要技术选型。本文主要介绍Spring Secutity的实现原理,并基于Spring Secutity设计基于RBAC的权限系统。
一、技术选型
为何把Spring Secutity作为权限系统的技术选型,主要考虑了以下几个方面:
- 数据鉴权的能力:Spring Secutity支持数据鉴权,即细粒度权限控制。
- Spring生态基础:Spring Secutity可以和Spring生态无缝集成。
- 多样认证能力:Spring Secutity支持多样认证方式,如预认证方式可以与第三方认证系统集成。
Spring Security | Apache Shiro | |
---|---|---|
认证 | 支持多种认证方式(如密码、匿名、预认证) | 简单登录认证 |
鉴权 | 功能鉴权、数据鉴权 | 功能鉴权 |
多源适配 | Mem、JDBC、DAO、LDAP、 OpenID、OAuth等 |
LDAP、JDBC、Kerberos、 ActiveDirectory等 |
加密 | 支持多种加密方式 | 简单加密方式 |
运行环境 | 依赖Spring | 可独立运行 |
开放性 | 开源、Spring生态基础 | 开源 |
复杂度 | 复杂、较重 | 简单、灵活 |
二、核心架构
权限系统一般包含两大核心模块:认证(Authentication)和鉴权(Authorization)。
- 认证:认证模块负责验证用户身份的合法性,生成认证令牌,并保存到服务端会话中(如TLS)。
- 鉴权:鉴权模块负责从服务端会话内获取用户身份信息,与访问的资源进行权限比对。
官方给出的Spring Security的核心架构图如下:
核心架构解读:
- AuthenticationManager:负责认证管理,解析用户登录信息(封装在Authentication),读取用户、角色、权限信息进行认证,认证结果被回填到Authentication,保存在SecurityContext。
- AccessDecisionManager:负责鉴权投票表决,汇总投票器的结果,实现一票通过(默认)、多票通过、一票否决策略。
- SecurityInterceptor:负责权限拦截,包括Web URL拦截和方法调用拦截。通过ConfigAttributes获取资源的描述信息,借助于AccessDecisionManager进行鉴权拦截。
- SecurityContext:安全上下文,保存认证结果。提供了全局上下文、线程继承上下文、线程独立上下文(默认)三种策略。
- Authentication:认证信息,保存用户的身份标示、权限列表、证书、认证通过标记等信息。
- SecuredResource:被安全管控的资源,如Web URL、用户、角色、自定义领域对象等。
- ConfigAttributes:资源属性配置,描述安全管控资源的信息,为SecurityInterceptor提供拦截逻辑的输入。
三、设计原理
通过对源码的分析,我把Spring Security的核心领域模型设计整理如下:
全局抽象模型解读:
- 配置:AuthenticationConfiguration负责认证系统的全局配置,GlobalMethodSecurityConfiguration负责方法调用拦截的全局配置。
- 构建:AuthenticationConfiguration通过AuthenticationManagerBuilder构建认证管理器AuthenticationManager,GlobalMethodSecurityConfiguration会自动初始化AbstractSecurityInterceptor进行方法调用拦截。
- Web拦截:HttpSecurity对Web进行安全配置,内置了大量GenericFilterBean过滤器对URL进行拦截。负责认证的过滤器会通过AuthenticationManager进行认证,并将认证结果保存到SecurityContext。
- 方法拦截:Spring通过AOP技术(cglib/aspectj)对标记为@PreAuthorize、@PreFilter、@PostAuthorize、@PostFilter等注解的方法进行拦截,通过AbstractSecurityInterceptor调用AuthenticationManager进行身份认证(如果必要的话)。
- 认证:认证管理器AuthenticationManager内置了多种认证器AuthenticationProvider,只要其中一个认证通过,认证便成功。不同的AuthenticationProvider获取各自需要的信息(HTTP请求、数据库查询、远程服务等)进行认证,认证结果全部封装在Authentication。需要加载用户、角色、权限信息的认证器(如密码认证、预认证等)需要对接UserDetailsManager接口实现用户CRUD功能。
- 鉴权:权限拦截器AbstractSecurityInterceptor通过读取不同的SecurityMetadataSource加载需要被鉴权资源的描述信息ConfigAttribute,然后把认证信息Authentication、资源描述ConfigAttribute、资源对象本身传递给AccessDecisionManager进行表决。AccessDecisionManager内置了多个投票器AccessDecisionVoter,投票器会将鉴权信息中的ConfigAttribute转换为SpringEL的格式,通过表达式处理器SecurityExpressionHandler执行基于表达式的鉴权逻辑,鉴权逻辑会通过反射的方式转发到SecurityExpressionRoot的各个操作上去。
- 定制:通过WebSecurityConfigureAdapter可以定制HTTP安全配置HttpSecurity和认证管理器生成器AuthenticationManagerBuilder;通过AbstractPreAuthenticatedProcessingFilter可以定制预认证过滤器;通过UserDetailsManager和UserDetails接口可以对接自定义数据源;通过GrantedAuthority定制权限信息;通过PermissionEvaluator可以定制自定义领域模型的访问控制逻辑。
四、应用集成
理清Spring Security的定制点后,就可以在系统内部集成Spring Security了。
这里使用预认证的方式,以适配第三方认证系统。AbstractPreAuthenticatedProcessingFilter提供了预认证的扩展点,基于该抽象类实现一个自定义认证过滤器。
public class MyPreAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
// 从第三方系统获取用户ID
return userId;
}
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return "";
}
}
Spring Security会根据预认证过滤器getPreAuthenticatedPrincipal返回的用户ID信息,加载用户角色等初始信息。这里需要实现UserDetailsManager接口,提供用户信息管理器。
@Service
public class MyUserManager implements UserDetailsManager {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库加载用户信息
return user;
}
// 其他管理接口
}
UserDetails内包含了GrantedAuthority接口类型的权限信息抽象,一般可以基于它自定义角色和权限。Spring Security使用一种接口形式表达角色和权限,角色和权限的差别是角色的ID是以"ROLE_"为前缀。
public class MyRole implements GrantedAuthority {
private final String role;
@Override
public String getAuthority() {
return "ROLE_" + role;
}
}
public class MyAuthority implements GrantedAuthority {
private final String authority;
@Override
public String getAuthority() {
return authority;
}
}
接下来注册自定义认证过滤器和用户管理器,这里需要实现WebSecurityConfigurerAdapter进行Web安全配置。
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, mode = AdviceMode.PROXY)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsManager userDetailsManager;
@Bean
protected AuthenticationProvider createPreAuthProvider() {
// 注册用户管理器
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<>(userDetailsManager));
return provider;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 注册预认证过滤器
http.addFilter(new MyPreAuthFilter(authenticationManager()));
}
}
这样,最简单的Spring Security框架集成内系统内部已经完成了。在系统的任意服务接口上可以使用如下方式进行鉴权。
public interface MyService {
@PreAuthorize("hasAuthority('QUERY')")
Object getById(String id);
@PreAuthorize("hasRole('ADMIN')")
void deleteById(String id);
}
PreAuthorize注解表示调用前鉴权,Spring使用默认使用动态代理技术生成鉴权逻辑。注解内配置了SpringEL表达式来定制鉴权方式。上述代码中,hasAuthority会检查用户是否有QUERY权限,hasRole会检查用户是否有ADMIN角色。
使用动态代理的方式进行AOP,只允许在接口层面进行权限拦截,如果想在任意的方法上进行权限拦截,那么就需要借助于AspectJ的方式进行AOP。首先将注解EnableGlobalMethodSecurity的mode设置为AdviceMode.ASPECTJ,然后添加JVM启动参数,这样就可以在任意方法上使用Spring Security的注解了。
-javaagent:/path/to/org/aspectj/aspectjweaver/1.9.4/aspectjweaver-1.9.4.jar
以上还是只是以用户的身份信息(角色/权限)进行权限,灵活度有限,也发挥不了Spring Security的数据鉴权的能力。要使用数据鉴权,需要实现一个Spring Bean。
@Component
public class MyPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
// 自定义数据鉴权
return false;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
// 自定义数据鉴权
return false;
}
}
PermissionEvaluator会被自动注册到Spring Security框架,并允许在注解内使用如下方式进行鉴权。
@PreAuthorize("hasPermission(#id, 'QUERY')")
Object func1(String id) {
}
@PreAuthorize("hasPermission(#id, 'TABLE', 'QUERY')")
Object func2(String id) {
}
其中,func1的注解表示校验用户是否对id有QUERY权限,代码逻辑路由到MyPermissionEvaluator的第一个接口。func2的注解表示校验用户是否对TABLE类型的id有QUERY权限,代码逻辑路由到MyPermissionEvaluator的第二个接口。PermissionEvaluator提供了权限系统中数据鉴权的扩展点,稍后会描述如何利用该扩展点定制基于RBAC的权限系统。
五、权限系统
构建基于RBAC(Role Based Access Control)的权限系统,需要明确用户、角色、权限、资源这几个核心的概念类的含义和它们之间的关系。
- 资源:权限系统内需要安全控制的客体,一般是系统内的数据或功能。
- 权限:描述了资源上的操作抽象,一般是一种动作。
- 授权:是权限和资源的组合,表示对资源的某一个操作。
- 角色:描述了一组授权的集合,表示一类特殊概念的功能集。
- 用户:权限系统的主体,一般是当前系统的访问用户,用户可以拥有多种角色。
以下是我们设计的基于RABC的权限核心领域模型:
一般情况下,系统内需要权限管控的资源是无法用户自定义的,因为资源会耦合大量的业务逻辑,所以我们提供了自 资源工厂,通过配置化的方式构建业务模块所需的资源。而用户、角色、权限,以及授权记录都是可以通过相应的管理器进行查询更新。
另外,资源抽象允许表达资源的继承和组合关系,继而表达更复杂的资源模型,资源统一鉴权的流程为:
- 执行鉴权时,首先看资源是原子资源还是组合资源。
- 对于原子资源,先查询是否有授权记录,再查看角色预授权是否包含当前授权,存在一种便成功。
- 没有授权记录和角色预授权的原子资源,尝试用父资源(如果有的话)代替鉴权,否则鉴权失败。
- 对于组合资源,先进行资源展开,获取子资源列表。
- 遍历子资源列表,并依次对子资源进行鉴权,子资源鉴权结果汇总后,即组合资源鉴权结果。
综上,基于统一资源抽象和资源配置化构建,可以实现资源的统一构建,继而实现统一鉴权。
六、总结回顾
本文从Spring Security的架构和原理出发,描述了开源安全框架对于认证和鉴权模块的设计思路和细节。并提供了系统内集成Spring Security的方法,结合RBAC通用权限系统模型,讨论了统一资源构建和统一鉴权的设计和实现。如果你也需要设计一个新的权限系统,希望本文对你有所帮助。
SpringSecurity原理剖析与权限系统设计的更多相关文章
- 若依管理系统RuoYi-Vue(二):权限系统设计详解
若依Vue系统中的权限管理部分的功能都集中在了系统管理菜单模块中,如下图所示.其中权限部分主要涉及到了用户管理.角色管理.菜单管理.部门管理这四个部分. 一.若依Vue系统中的权限分类 根据观察,若依 ...
- ASP.NET Core 运行原理剖析2:Startup 和 Middleware(中间件)
ASP.NET Core 运行原理剖析2:Startup 和 Middleware(中间件) Startup Class 1.Startup Constructor(构造函数) 2.Configure ...
- ASP.NET Core 运行原理剖析1:初始化WebApp模版并运行
ASP.NET Core 运行原理剖析1:初始化WebApp模版并运行 核心框架 ASP.NET Core APP 创建与运行 总结 之前两篇文章简析.NET Core 以及与 .NET Framew ...
- 【Xamarin挖墙脚系列:Xamarin.IOS机制原理剖析】
原文:[Xamarin挖墙脚系列:Xamarin.IOS机制原理剖析] [注意:]团队里总是有人反映卸载Xamarin,清理不完全.之前写过如何完全卸载清理剩余的文件.今天写了Windows下的批命令 ...
- 写给 Android 应用工程师的 Binder 原理剖析
写给 Android 应用工程师的 Binder 原理剖析 一. 前言 这篇文章我酝酿了很久,参考了很多资料,读了很多源码,却依旧不敢下笔.生怕自己理解上还有偏差,对大家造成误解,贻笑大方.又怕自己理 ...
- NameNode和SecondaryNameNode工作原理剖析
NameNode和SecondaryNameNode工作原理剖析 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.NameNode中的元数据是存储在那里的? 1>.首先,我 ...
- NameNode与DataNode的工作原理剖析
NameNode与DataNode的工作原理剖析 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.HDFS写数据流程 >.客户端通过Distributed FileSyst ...
- CDN 工作原理剖析
CDN 工作原理剖析 CDN / Content Delivery Network / 内容分发网络 https://www.cloudflare.com/zh-cn/learning/cdn/wha ...
- ext文件系统机制原理剖析
本文转载自ext文件系统机制原理剖析 导语 将磁盘进行分区,分区是将磁盘按柱面进行物理上的划分.划分好分区后还要进行格式化,然后再挂载才能使用(不考虑其他方法).格式化分区的过程其实就是创建文件系统. ...
随机推荐
- 依赖倒置原则(DIP)、控制反转(IoC)、依赖注入(DI)(C#)
理解: 依赖倒置原则(DIP)主程序要依赖于抽象接口,不要依赖于具体实现.高层模块不应该依赖底层模块,两个都应该以来抽象.抽象不应该依赖细节,细节应该依赖抽象.(具体看我上一篇贴子) 依赖倒置原则是六 ...
- 将excel中某列数据中,含有指定字符串的记录取出,并生成用这个字符串命名的txt文件
Python 一大重要的功能,就是可处理大量数据,那分不开的即是使用Excel表格了,这里我做下学习之后的总结,望对我,及广大同仁们是一个帮助Python处理Excel数据需要用到2个库:xlwt 和 ...
- (一)LinkedList集合解析及手写集合
一.LinkedList集合特点 问题 结 论 LinkedList是否允许空 允许 LinkedList是否允许重复数据 允许 LinkedList是否有序 有序 LinkedList是否 ...
- CodeForces 375D Tree and Queries 莫队||DFS序
Tree and Queries 题意:有一颗以1号节点为根的树,每一个节点有一个自己的颜色,求出节点v的子数上颜色出现次数>=k的颜色种类. 题解:使用莫队处理这个问题,将树转变成DFS序区间 ...
- 中南林业科技大学第十一届程序设计大赛 有趣的二进制 牛客网 java大数的一些方法
链接:https://www.nowcoder.com/acm/contest/124/C来源:牛客网 题目描述 小新在学C语言的时候,邝老师告诉他double类型的数据在表示小数的时候,小数点后的有 ...
- 关于C语言\b \t \n及转义序列的理解
转义序列 说明 \b 后退一格(Backspace) \t 水平制表(Tab=4个空格) \v 垂直制表 \r 回车(Enter) \f 换页 \a 发出鸣响 \n 换行 \" 输出/输入双 ...
- 【Linux】一些常用命令(待整理)
一.关机重启命令 二.查询ip 三.查询杀死进程 四.CentOS7 关闭防火墙 五.vim常用 5.1 搜索 5.2 设置行号 剪切 替换 一.关机重启命令 shutdown -h 10 #计算机将 ...
- Python操作MongoDB文档数据库
1.Pymongo 安装 安装pymongo: pip install pymongo PyMongo是驱动程序,使python程序能够使用Mongodb数据库,使用python编写而成: 2.Pym ...
- Oracle sqlldr 在DOS窗口导入多列数据到数据库表
sqlldr 用法详见:https://www.cnblogs.com/rootq/archive/2009/03/01/1401061.html 测试内容: 1.创建数据库表: create tab ...
- CVE-2014-6271 Shellshock 破壳漏洞 复现
补坑. 什么是shellshock ShellShock是一个BashShell漏洞(据说不仅仅是Bash,其他shell也可能有这个漏洞). 一般情况来说,系统里面的Shell是有严格的权限控制的, ...