Spring Boot 3 整合 Mybatis-Plus 实现数据权限控制
Spring Boot 3 整合 Mybatis-Plus 实现数据权限控制MyBatis-Plus 数据权限插件插件原理Spring Boot 3 项目整合DataPermissionInterceptor 实现数据权限实现DataPermissionHandler项目示例平台简介内置功能后端开发前端开发效果展示
早期使用spring boot的时候写数据权限是通过使用自定义一个切面@Aspect配合自定义DataScope注解来实现。
DataScopeAspect 类
@Aspect
@Component
public class DataScopeAspect {
@Resource
private SysDeptService sysDeptService;
@Resource
private SysUserRoleService sysUserRoleService;
@Resource
private SysRoleDeptService sysRoleDeptService;
/**
* 配置织入点
*/
@Pointcut("@annotation(cn.harry.common.annotation.DataScope)")
public void dataScopePointCut() {
}
@Before("dataScopePointCut()")
public void doBefore(JoinPoint point) {
handleDataScope(point);
}
protected void handleDataScope(final JoinPoint joinPoint) {
Object params = joinPoint.getArgs()[0];
if (params instanceof Map) {
SysUser user = SecurityUtils.getSysUser();
// 如果不是超级管理员,则进行数据过滤
if (!SecurityUtils.isAdmin(user.getId())) {
// 根据用户ID 获取数据权限标识 如果数据权限包含全部数据 直接返回
List<Integer> dataScopes = sysUserRoleService
.listDataScopesByUserId(user.getId());
if (!CollectionUtils.isEmpty(dataScopes)) {
if (dataScopes.contains(DataScopeEnums.ALL.getKey())) {
return;
}
}
Map map = (Map) params;
map.put(CommonConstant.SQL_FILTER, this.getSQLFilter(user, joinPoint));
}
return;
}
throw new ApiException("数据权限接口,只能是Map类型参数,且不能为NULL");
}
/**
* 获取数据过滤的SQL
* @param user
* @param point
* @return
*/
private String getSQLFilter(SysUser user, JoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
DataScope dataFilter = signature.getMethod().getAnnotation(DataScope.class);
// 获取表的别名
String tableAlias = dataFilter.tableAlias();
if (StringUtils.isNotBlank(tableAlias)) {
tableAlias += ".";
}
// 部门ID列表
Set<Long> deptIdList = new HashSet<>();
StringBuilder sqlFilter = new StringBuilder();
// 1. 根据用户ID 获取角色列表
List<Long> roleIdList = sysUserRoleService.listRoleIdByUserId(user.getId());
if (!roleIdList.isEmpty()) {
// 2.用户角色对应的部门ID列表
List<Long> idList = sysRoleDeptService.queryDeptIdList(roleIdList);
deptIdList.addAll(idList);
}
// 用户子部门ID列表
if (dataFilter.subDept()) {
List<Long> subDeptIdList = sysDeptService.getSubDeptIdList(user.getDeptId());
deptIdList.addAll(subDeptIdList);
}
sqlFilter.append(" (");
if (!deptIdList.isEmpty()) {
sqlFilter.append(tableAlias).append(dataFilter.deptId()).append(" in(")
.append(StringUtils.join(deptIdList, ",")).append(")");
}
// 没有设置本部门数据权限,也能查询本部门数据 user.getDeptId()
if (dataFilter.user()) {
if (!deptIdList.isEmpty()) {
sqlFilter.append(" or ");
}
sqlFilter.append(tableAlias).append(dataFilter.deptId()).append("=")
.append(user.getDeptId());
}
sqlFilter.append(")");
if ("()".equals(sqlFilter.toString().trim())) {
return null;
}
return sqlFilter.toString();
}
}
DataScope 注解类
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {
/** 表的别名 */
String tableAlias() default "";
/** true:没有本部门数据权限,也能查询本人数据 */
boolean user() default true;
/** true:拥有子部门数据权限 */
boolean subDept() default false;
/** 部门ID */
String deptId() default "dept_id";
/** 用户ID */
String userId() default "user_id";
}
早期Spring Boot 项目中实现项目权限的写法,入参使用map,service层实现时使用wrapper.apply
拼接了sql语句,虽说也能实现数据权限的控制,但同时代码可读性很差.
在整合Spring Boot 3框架的时候,发现MyBatis-Plus 提供了一个数据权限插件,这样我就可以不用破坏它原本的结构,实现数据权限的管理。我们看一下MyBatis-Plus 数据权限插件。
MyBatis-Plus 数据权限插件
地址: https://baomidou.com/plugins/data-permission/
DataPermissionInterceptor 是 MyBatis-Plus 提供的一个插件,用于实现数据权限控制。它通过拦截执行的 SQL 语句,并动态拼接权限相关的 SQL 片段,来实现对用户数据访问的控制。
插件原理
DataPermissionInterceptor 的工作原理与租户插件类似,它会在 SQL 执行前拦截 SQL 语句,并根据用户权限动态添加权限相关的 SQL 片段。这样,只有用户有权限访问的数据才会被查询出来。
Spring Boot 3 项目整合DataPermissionInterceptor 实现数据权限
实现DataPermissionHandler
/**
* 数据权限控制器
*
* @author harry
* @公众号 Harry技术
*/
@Slf4j
public class MybatisPlusDataPermissionHandler implements DataPermissionHandler {
@Override
@SneakyThrows
public Expression getSqlSegment(Expression where, String mappedStatementId) {
log.info(" 数据权限控制器 :{}", mappedStatementId);
Class<?> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT)));
String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1);
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.getName().equals(methodName)) {
DataScope annotation = method.getAnnotation(DataScope.class);
// 如果没有注解或者是超级管理员,直接返回
if (annotation == null || SecurityUtils.isRoot()) {
return where;
}
return dataScopeFilter(annotation, where);
}
}
return where;
}
@SneakyThrows
private Expression dataScopeFilter(DataScope annotation, Expression where) {
String deptAlias = annotation.deptAlias();
String userAlias = annotation.userAlias();
String deptIdColumnName = annotation.deptIdColumnName();
String userIdColumnName = annotation.userIdColumnName();
String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName;
String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName;
// 获取当前用户的数据权限
Integer dataScope = SecurityUtils.getDataScope();
DataScopeEnums dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnums.class);
Long deptId, userId;
String appendSqlStr;
switch (dataScopeEnum) {
case ALL:
return where;
case DEPT:
deptId = SecurityUtils.getDeptId();
appendSqlStr = deptColumnName + StringPool.EQUALS + deptId;
break;
case SELF:
userId = SecurityUtils.getUserId();
appendSqlStr = userColumnName + StringPool.EQUALS + userId;
break;
// 默认部门及子部门数据权限
default:
deptId = SecurityUtils.getDeptId();
appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , ancestors ) )";
break;
}
if (StrUtil.isBlank(appendSqlStr)) {
return where;
}
Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr);
if (where == null) {
return appendExpression;
}
return new AndExpression(where, appendExpression);
}
}
再Mapper上使用自定义注解
@Override
@DataScope(userIdColumnName = "id")
List<SysUser> selectList(IPage<SysUser> page, @Param(Constants.WRAPPER) Wrapper<SysUser> queryWrapper);
userIdColumnName = "id"
表示我们将用户ID传递为id
定义权限枚举
/**
* 数据权限枚举
*
* @author harry
* @公众号 Harry技术
*/
@Getter
@AllArgsConstructor
public enum DataScopeEnums implements IBaseEnum<Integer> {
/**
* value 越小,数据权限范围越大
*/
ALL(0, "所有数据权限"),
DEPT_AND_SUB(1, "本部门及子部门数据"),
DEPT(2, "本部门数据权限"),
SELF(3, "本人数据权限"),
;
private final Integer value;
private final String label;
}
定义四类枚举值:根据不同的枚举类型,拼接sql语句
关于上面的数据的实现已经整合到Harry技术
项目中,你可以下载源码学习使用
项目示例
基于SpringBoot3+Vue3前后端分离的Java快速开发框架
平台简介
基于 JDK 17、Spring Boot 3、Spring Security 6、JWT、Redis、Mybatis-Plus、Knife4j等构建后端,基于Vue 3、Element-Plus 、TypeScript等构建前端的分离单体权限管理系统。
- 🚀 开发框架: 使用 Spring Boot 3 和 Vue 3,以及 Element-Plus 等主流技术栈,实时更新。
- 🔐 安全认证: 结合 Spring Security 和 JWT 提供安全、无状态、分布式友好的身份验证和授权机制。
- 🔑 权限管理: 基于 RBAC 模型,实现细粒度的权限控制,涵盖接口方法和按钮级别。
- 🛠️ 功能模块: 包括用户管理、角色管理、菜单管理、部门管理、字典管理等多个功能。
- 📘 接口文档: 自动生成接口文档,支持在线调试,提高开发效率。
内置功能
- 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
- 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
- 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
- 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
- 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
- 参数管理:对系统动态配置常用参数。
- 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
- 登录日志:系统登录日志记录查询包含登录异常。
- 系统接口:根据业务代码自动生成相关的api接口文档,引入swagger接口文档服务的工具(Knife4j)。
后端开发
Gitee仓库地址: https://gitee.com/harry-tech/harry.git
前端开发
- 本项目是前后端分离的,还需要部署前端,才能运行起来
Gitee仓库地址: https://gitee.com/harry-tech/harry-vue.git
效果展示
Watermark 水印
暗黑模式
觉着有帮助,给个Star再走呗 ~~~~
公众号搜“Harry技术”,关注我,带你看不一样的人间烟火!