kewen-framework-auth框架说明
1. 框架简介
kewen-framework-auth 是一个基于注解实现的权限控制和验证的Java后端多模块的框架,通过引入对应的模块功能快速引入一整套权限系统开发业务功能,避免了业务上重新再设计权限体系。
在普通的项目中,权限往往是项目的基础功能,业务的运行离不开权限的配合使用,因此每个系统都去搭建自己的一套权限体系,而且每个业务中都需要维护一套权限的系列表,业务中的各个地方还需要在服务中进行一系列的校验。这样做的结果就是花了很多时间来解决权限的问题,而且每个业务自行维护的表无法进行公共的抽象,将出现了很多的相同逻辑的冗余代码。
目前大部分的权限体系其实都是基于RABC思想(用户-部门-角色-岗位)来设计,通过对业务与用户组织信息建立对应的映射关系来实现权限控制,其实有非常大的共有特点,而且由于权限本身对于一个项目来说的话就是公共的部分,因此本项目从常用的RABC权限体系入手,通过设计来实现统一的权限管理。
本框架主要是用Java+Maven+Spring+SpringMVC+SpringBoot+SpringSecurity+MybatisPlus+Mysql
来搭建的,主要基于Spring5和SpringBoot2搭建。
2. 特性
注解驱动菜单权限和数据权限
只通过注解的形式则可以完成菜单权限的验证和数据权限的验证及范围查询
基于用户-角色-部门的默认实现
实现了用户-角色-部门的rabc用户体系的实现
整合SpringSecurity加强安全
引入SpringSecurity加强安全
- 修改SpringSecurity的原生登录,改为json登录
- 解决SpringSecurity异常的重定向/error问题,统一通过springmvc异常处理方式
- 添加
@SecurityIgnore
白名单及配置文件白名单方式,其余全部拦截
菜单权限注解启动时添加新的api路径入库
启动时将需要验证的菜单入库到数据库表中,启动之后配置对应的菜单权限即可,无需逐一配置API信息。
预留诸多扩展满足差异化的实现
预留出扩展接口,方便自行实现定制化需求
3. 核心概念
3.1. 权限说明
菜单权限:访问某个菜单需要的权限。框架中涉及到有前端访问路由的菜单路由权限和请求后端接口的菜单API请求权限。通过对菜单的控制完成粗粒度的鉴权,达到可见/不可见的效果。
数据权限:某条数据对应的权限。对于每一条数据,其都应该对应有一个数据权限来对其控制,有对应权限的就应该可以执行对应的操作,没有权限的就应该在查询列表中不可见,同时对应请求也应当不能执行。
数据权限-操作类型 数据的权限应该还需要进一步的划分,一条数据对应不同的操作应该有不同的权限。如:会议室可以编辑,也可以预约,这两个操作的基础数据都是会议室,但是编辑应当由管理员来执行,而预约应当大部分人都可以执行的,它肯定不应该只能管理员可以预约。
3.2. 核心注解
主要涉及到4个核心注解,1个菜单注解+3个数据注解,分别对应不同的功能实现
@AuthMenu
:菜单权限注解,判断登录人是否能访问注解对应的API接口。@AuthDataRange
: 数据范围查询注解,加上此注解会在查询数据列表时自动筛选登录人可见的数据,不可见的数据不展示。@AuthDataOperation
:数据操作注解,加上此注解会在请求时校验是否对单条数据有操作权限,避免通过接口进行越权攻击,一般业务配合@AuthDataRange
使用。@AuthDataAuthEdit
: 数据编辑注解,加上此注解会直接把请求中的权限编辑到权限表中,此注解依赖菜单的权限校验。
3.3. 核心调用类
AnnotationAuthHandler<ID>
注解核心控制器,注解对应的所有实现由此Handler完成。其中,泛型ID是指业务权限中的data_id类型String Integer Long
中的一种AuthDataAdaptor<ID>
业务调用适配器,可以不使用注解@AuthDataAuthEdit
来编辑权限了,更灵活。其中,泛型ID是指业务权限中的data_id类型String Integer Long
中的一种
4. 快速开始
1.下载本框架工程然后mvn install
到本地maven仓库。
2.工程引入,通过继承auth-parent
,引入auth-spring-boot-starter
模块即可
1 | <parent> |
4.1. 启动示例工程
框架配置了一个示例工程,受mysql开源协议影响,示例工程已经移动至kewen-framework-auth-sample下,可以查看其README.md文档
此外,框架还搭配了我的另外一个前端模板项目kewen-web-admin,可以fork到自己工程中也可以复制下来,然后启动。
4.2. 引入到自己的工程
框架自带了一套基于RABC(User-Dept-Role)的权限实现,同时配置了安全模块,可以直接引入依赖即可完成,下面说一下快速启动的步骤
- 初始化数据库
- 引入依赖
- 添加配置
- 添加可选配置
- 启动
引入依赖:引入相关的依赖
需要把auth-starter-security-web
和auth-starter-rabc
都引进,前者有安全和登录相关的,后台有基于RABC的权限实现
1 | <dependencies> |
初始化数据库:和利用sample模块启动一致,找到工程目录./script/sql/auth.sql
的脚本执行即可
配置:同样,自行配置基本的数据库配置
1 | server.port=8081 |
4.3. 应用可选配置
然后应用中有部分可选配置,可以参考kewen-framework-auth-sample
启动示例工程选择使用ExceptionAdviceHandler
主要用于返回异常,方便在报错时可以准确处理异常SampleResponseBodyResultResolver
,认证成功返回的结构处理,可以匹配成和自己项目中一致的统一返回,不配置则使用默认的
另外,示例工程还包含了代码生成相关的类,可以忽略不管,新的表需要一键生成MybatisPlus代码时也可以是使用,后续来完善这部分功能
4.4. 启动
完成了以上配置,启动普通的springboot项目即可。
快速开始的内容中包含了完全实现了功能的最小配置,引入即可使用。一个自带的权限管理功能引入完成。
5. 项目结构
1 | kewen-framework-auth |
6. 技术选型
框架:本框架主要使用了Spring+SpringMVC+SpringBoot+SpringSecurity+MybatisPlus
相关框架完成基础框架的搭建
构建工具:采用Maven作为构建工具,。
数据库:采用Mysql作为后端数据库,但是框架本身是支持其他类型的数据库的,因此也可以自行选择数据库(需要修改初始化的sql脚本,可能还需要修改自定义Mapper中的sql)
7. 注解使用
前面我们说道,框架主要由4个注解实现,@AuthMenu
:菜单权限注解,判断登录人是否能访问注解对应的API接口。@AuthDataRange
: 数据范围查询注解,加上此注解会在查询数据列表时自动筛选登录人可见的数据,不可见的数据不展示。@AuthDataOperation
:数据操作注解,加上此注解会在请求时校验是否对单条数据有操作权限,避免通过接口进行越权攻击,一般业务配合@AuthDataRange使用。@AuthDataAuthEdit
: 数据编辑注解,加上此注解会直接把请求中的权限编辑到权限表中,此注解依赖菜单的权限校验。
7.1. @AuthMenu
注解如下:
1 | public AuthMenu { |
参数:
- name():API菜单的名称,在数据库存储API菜单的时候作为描述使用,方便在数据库层面定位到对应功能
使用方法:
在Controller对应的方法上添加
@AuthMenu
即可,如:1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestAuthAnnotationController {
/**
* 测试菜单控制
*/
public Result testCheckMenu() {
return Result.success("成功通过注解AuthMenu完成控制");
}
}此时
/test/checkMenu
路径就会校验菜单权限。
这里需要说明的是及时类上没有注解,也会默认生成虚拟API菜单,虚拟菜单的说明接下。在Controller类上使用
在类上使用表示类中所有的带有@ReqeustMapping的都会加入API菜单校验,但是权限不一定都一样。加入数据库的时候会以类的路径为基础生成一个虚拟API菜单,作为内部方法的父级,可以自由的选择是以父类为权限基准还是子类,如:1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestAuthMenuClassController {
public String hello(String name) {
return "hello " + name;
}
public String hello2() {
return "hello2 ";
}
}此时
/testAnnoClassMenuController/hello
、/testAnnoClassMenuController/hello2
均会加入API菜单校验
7.2. @AuthDataRange
注解如下:
1 | /** |
参数:
- businessFunction():必填,业务功能,用于区分权限的所属,唯一标识一套数据属于哪一个功能
- tableAlias(): 可选,表别名,用于多表联查时的别名,
- dataIdColumn(): 主表的ID字段名,默认id
- operate():操作,对同一业务功能的细化操作,如 会议室编辑、预约
- matchMethod(): 用In或exists匹配
基于当前权限范围查询注解,在查询数据的时候关联数据的权限表以及用户权限体系,检查用户是否拥有数据配置的权限
在mapper拦截 ,用于数据权限列表查询
例如 拼接语句:
1 | select * from ${business_table} |
已经实现了在where、in子查询、join子查询、withas子查询的匹配。
基本包含了常见的语句的权限匹配
使用方法:
在对应的Controller上的方法上添加注解@AuthDataRange,填上必填项businessFunction即可
业务中正常查询数据,数据库层面会自动添加上范围条件查询
1 |
|
支持类型示例:
in 子查询in
1 | SELECT * FROM meeting_room t1 |
exists exists
1 | SELECT * FROM meeting_room t1 |
in和exists都实现了,后面只提in相关的
with as with as
1 | WITH t_a AS ( |
join join查询
1 | SELECT * FROM meeting_room t1 |
7.3. @AuthDataOperation
注解如下:
1 | /** |
参数:
- businessFunction(): 业务功能
- operate(): 操作
实现了此注解会根据传入的模块ID和操作以及业务ID自动判定是否有执行此操作的权限,数据库层面如下
1 | select business_id from sys_auth_data |
使用方法:
在方法上添加相应的注解即可,此处不一定是Controller,Service也是可以的,但是要保证能正常开启切面(如不能是内部调用)。
1 | /** |
7.4. @AuthDataAuthEdit
注解如下:
1 | public AuthDataAuthEdit { |
参数:
- businessFunction():必填,业务功能,用于区分权限的所属,唯一标识一套数据属于哪一个功能
- operate():操作,对同一业务功能的细化操作,如 会议室编辑、预约
- before():在具体业务前执行还是之后执行
执行修改业务权限逻辑,记得在这之前要先执行url权限验证,自行控制权限,这里只是封装编辑逻辑
加上注解直接就开始修改业务的权限了,用了这个注解就不用再写权限逻辑,只需要完成写权限的后续逻辑即可
注意事务的保证,此注解只是单纯的封装编辑逻辑,并未对事务进行处理,需要调用方保证
使用方法:
1 | /** |
也可以用AuthDataAdaptor#editDataAuths()
来代替
7.5. AuthDataAdaptor
数据权限适配器
使用此可以不使用注解@AuthDataAuthEdit
1 | public class AuthDataAdaptor<ID> { |
8. 示例案例
示例案例新建工程kewen-framework-auth-sample
中,可以参考此工程来引入及配置
9. 高级配置扩展
框架实现了基于RABC的权限体系,同时也预留了很多扩展点,这其实也是一个框架应该具有的基本要求。
这里将从基础到复杂进行配置说明
9.1. 安全登录相关
框架引入了SpringSecurity安全框架,大部分安全功能都是在SpringSecurity上进行扩展
9.1.1. 登录相关的参数配置
登录重写了SpringSecurity的表单登录逻辑,因为前后端分离项目本身就是使用json交互的,所以改成了json交互,
可以自定义登录的API请求地址、username参数、password参数、token头参数、当前登录人接口、最大session数量、是否可以挤下线
1 | kewen-framework: |
以上是基于yml配置的默认的值,不修改默认为以上的地址。
9.1.2. 认证成功处理器
在认证成功后,默认会提供一个成功返回的处理器DefaultSecurityAuthenticationSuccessHandler
,将成功认证的数据经过转换写入输出流中。
我们这里可以自定义一个成功返回解析器,也可以自定义成功处理器。一般只需要定义解析器就可以了。
自定义成功返回解析器
需要实现ResponseBodyResultResolver
,覆写resolver
方法,要求返回最终写流的数据格式,一般这里用来匹配后端统一定义的返回格式,否则默认返回用户信息和默认退出信息不太符合统一返回格式的要求。
可以参照SampleResponseBodyResultResolver
来进行配置1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/**
* 认证成功用户转化类,处理用户的额外信息
* @author kewen
* @since 2024-07-04
*/
public interface ResponseBodyResultResolver {
/**
* 处理返回格式
* @param request 请求
* @param response 响应,注意不要开关流
* @param data 准备返回的数据,可以为空
* @return 准备写流的数据
*/
Object resolver(HttpServletRequest request, HttpServletResponse response, ; Object data)
}自定义成功返回处理器
如果自定义返回处理器,则需要定义登录成功的返回和退出登录成功的返回1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/**
* 认证成功处理器
* @author kewen
* @since 2024-07-04
*/
public interface SecurityAuthenticationSuccessHandler extends AuthenticationSuccessHandler , LogoutSuccessHandler {
/**
* 认证成功的数据处理,要求写流返回
*/
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException;
/**
* 退出登录成功的数据处理,要求写流返回
*/
void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException;
}
9.1.3. 认证失败、授权失败处理器
认证失败的处理相对而言就简单了,框架封装了SpringMVC的异常解析器,只需要按照SpringMVC的异常处理@ControllerAdvice
+ @ExceptionHandler
就可以
比如配置一下异常增强,
1 |
|
也可以参考 kewen-framework-auth-sample下的ExceptionAdviceHandler
9.2. 注解权限相关
注解定义了主体的逻辑,除了既定的配置外,不支持扩展,能扩展的主要是在它的处理器中以及处理器的实现逻辑
9.2.1. 权限处理器AnnotationAuthHandler
AnnotationAuthHandler
是权限校验和配置的实现类,它主要包含四个注解对应的校验方法。
框架默认在auth-rabc中对其有一个RABC框架的实现,如果不需要的话也可以自己定义实现
但是这里需要注意,如果自定义了AnnotationAuthHandler
那么后续auth-rabc
和auth-starter-rabc
就不再应该引入了,因为auth-rabc
的目的就是实现默认的AnnotationAuthHandler
实现默认的需要实现4个方法,如下:
1 | /** |
因此,如果用RABC权限体系的话一般不建议实现此接口,如有自定义的可以修改RabcAnnotationAuthHandler
内部的一些逻辑
9.2.2. AbstractAuthDatraAnnotationAuthHandler
扩展
AbstractAuthDatraAnnotationAuthHandler 本身实现了数据权限的相关操作功能,使用的是jdbcTemplate操作数据库。
此类的配置主要是权限数据表的定义可以自己指定,并且全局统一
9.2.3. RabcAnnotationAuthHandler
内部的扩展
内部扩展主要是在SysAuthMenuComposite
和SysAuthDataComposite
两个。
SysAuthMenuComposite
承载了菜单相关的逻辑,默认实现是基于内存的菜单管理关系,这里可以扩展将其修改为基于redis的等。后续看情况单独抽离一个存储容器让其可以自定义
9.3. RABC默认权限结构体的扩展
RABC默认目前只实现了基于部门-用户-角色的三个维度的权限,且没有完成上下级的相关(后续拟加入上下级和复杂组合的权限实现)
如果目前不满足也可以自行扩展基础类
比如,我们新增加一个岗位维度的
- 新建一个岗位类
Position
继承AbstractIdNameFlagAuthEntity
,实现基本的基于id、name的权限体
- 新建一个岗位类
1 | public class Position extends AbstractIdNameFlagAuthEntity{ |
- 新建一个权限集合体继承
SimpleAuthObject
,重写方法addAnotherBashAuth
和setAnotherBaseAuth
,使之可以添加相关的功能
- 新建一个权限集合体继承
1 | public class PositionSimpleAuthObject extends SimpleAuthObject { |
- 实现
SysUserComposite
或继承SysUserCompositeImpl
,重写通过用户加载权限的方法loadByUsername()
,要替换UserAuthObject
中的authObject
字段
这部分可以参考SysUserCompositeImpl
来实现
- 实现
1 |
|
- 编辑权限入参的实体加入
PositionSimpleAuthObject
以配置权限体
- 编辑权限入参的实体加入
1 |
|
10. 权限粒度说明
- 数据创建、删除的权限应当由菜单权限控制,同时,需要有一个编辑接口专门编辑主权限(即创建之后交于其他人管理的最大权限,其他人应当可以管理除了主权限之外的信息),这样实现了管理分离
- 菜单权限拥有对接口的最大控制,没有接口权限不能访问任何数据。建议在高级接口才使用菜单权限控制,其余的数据的操作接口,可以设置为所有人均拥有权限(避免两种权限搞混)
- 对于只有单一控制的数据如日程(创建之后数据基本就形成了,没有再复杂的逻辑),因为数据本身就之归属于某个人或一组人,因此菜单权限保证数据的增删改查,数据权限就仅剩下数据对应的范围。
- 对于有多重复杂控制的数据如会议室(创建了之后还需要以此为中心执行其他的业务逻辑),因为数据本身固定,但会提供给其他,因此菜单建议控制添加、删除、修改主权限,其余的交予数据权限来控制。