Files
youlai-boot/src/main/java/com/youlai/boot/config/MyDataPermissionHandler.java

268 lines
10 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.youlai.boot.config;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
import com.youlai.boot.common.annotation.DataPermission;
import com.youlai.boot.common.enums.DataScopeEnum;
import com.youlai.boot.security.model.RoleDataScope;
import com.youlai.boot.security.model.SysUserDetails;
import com.youlai.boot.security.util.SecurityUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import java.lang.reflect.Method;
import java.util.List;
/**
* 数据权限控制器
* <p>
* 支持多角色数据权限合并(并集策略):
* - 如果任一角色是 ALL则跳过数据权限过滤
* - 否则用 OR 连接各角色的数据权限条件
* <p>
* 使用 JSQLParser 构建 SQL 条件,避免字符串拼接,提高代码安全性和可读性。
*
* @author zc
* @since 2021-12-10 13:28
*/
@Slf4j
public class MyDataPermissionHandler implements DataPermissionHandler {
private static final String DEPT_TABLE = "sys_dept";
private static final String DEPT_ID_COLUMN = "id";
private static final String DEPT_TREE_PATH_COLUMN = "tree_path";
/**
* 获取数据权限的sql片段
*
* @param where 查询条件
* @param mappedStatementId mapper接口方法的全路径
* @return sql片段
*/
@Override
@SneakyThrows
public Expression getSqlSegment(Expression where, String mappedStatementId) {
// 如果是未登录或者是定时任务执行的SQL或者是超级管理员直接返回
if (SecurityUtils.getUserId() == null || SecurityUtils.isRoot()) {
return where;
}
// 获取当前用户的数据权限列表
List<RoleDataScope> dataScopes = SecurityUtils.getUser()
.map(SysUserDetails::getDataScopes)
.orElse(List.of());
// 如果任一角色是 ALL则跳过数据权限过滤并集策略
if (hasAllDataScope(dataScopes)) {
return where;
}
// 如果没有数据权限,跳过过滤
if (CollectionUtil.isEmpty(dataScopes)) {
return where;
}
// 获取当前执行的接口类
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)) {
DataPermission annotation = method.getAnnotation(DataPermission.class);
// 判断当前执行的方法是否有权限注解,如果没有注解直接返回
if (annotation == null) {
return where;
}
// 使用并集策略过滤
return dataScopeFilterWithUnion(mappedStatementId, annotation, dataScopes, where);
}
}
return where;
}
/**
* 判断是否包含"全部数据"权限
*
* @param dataScopes 数据权限列表
* @return 是否有全部数据权限
*/
private boolean hasAllDataScope(List<RoleDataScope> dataScopes) {
if (CollectionUtil.isEmpty(dataScopes)) {
return false;
}
return dataScopes.stream()
.anyMatch(scope -> DataScopeEnum.ALL.getValue().equals(scope.getDataScope()));
}
/**
* 使用并集策略进行数据权限过滤
* <p>
* 多个角色的数据权限通过 OR 连接,实现并集效果
*
* @param annotation 数据权限注解
* @param dataScopes 数据权限列表
* @param where 原始查询条件
* @return 追加权限过滤后的查询条件
*/
@SneakyThrows
private Expression dataScopeFilterWithUnion(String mappedStatementId, DataPermission annotation, List<RoleDataScope> dataScopes, Expression where) {
String deptAlias = annotation.deptAlias();
String deptIdColumnName = annotation.deptIdColumnName();
String userAlias = annotation.userAlias();
String userIdColumnName = annotation.userIdColumnName();
// 构建各角色的数据权限条件,使用 OR 连接实现并集
Expression unionExpression = null;
for (RoleDataScope dataScope : dataScopes) {
Expression roleExpression = buildRoleDataScopeExpression(
deptAlias, deptIdColumnName, userAlias, userIdColumnName, dataScope);
if (roleExpression != null) {
if (unionExpression == null) {
unionExpression = roleExpression;
} else {
// 使用 OR 连接各角色的条件(并集)
unionExpression = new OrExpression(unionExpression, roleExpression);
}
}
}
if (unionExpression == null) {
return where;
}
// 用括号包裹并集条件
Expression finalExpression = CCJSqlParserUtil.parseCondExpression("(" + unionExpression + ")");
if (where == null) {
log.debug("DataPermission applied. mappedStatementId={}, segment={}", mappedStatementId, finalExpression);
return finalExpression;
}
Expression combined = new AndExpression(where, finalExpression);
log.debug("DataPermission applied. mappedStatementId={}, originWhere={}, segment={}, combined={}",
mappedStatementId, where, finalExpression, combined);
return combined;
}
/**
* 构建单个角色的数据权限SQL条件
* <p>
* 使用 JSQLParser 构建 Expression避免字符串拼接
*
* @param deptAlias 部门表别名
* @param deptIdColumnName 部门ID字段名
* @param userAlias 用户表别名
* @param userIdColumnName 用户ID字段名
* @param roleDataScope 角色数据权限
* @return 数据权限条件表达式
*/
private Expression buildRoleDataScopeExpression(String deptAlias, String deptIdColumnName,
String userAlias, String userIdColumnName,
RoleDataScope roleDataScope) {
Column deptColumn = buildColumn(deptAlias, deptIdColumnName);
Column userColumn = buildColumn(userAlias, userIdColumnName);
Long deptId = SecurityUtils.getDeptId();
Long userId = SecurityUtils.getUserId();
DataScopeEnum dataScopeEnum = DataScopeEnum.getByValue(roleDataScope.getDataScope());
if (dataScopeEnum == null) {
return null;
}
return switch (dataScopeEnum) {
case ALL -> null; // 全部数据权限,不添加过滤条件
case DEPT_AND_SUB -> buildDeptAndSubExpression(deptColumn, deptId);
case DEPT -> buildEqualsExpression(deptColumn, deptId);
case SELF -> buildEqualsExpression(userColumn, userId);
case CUSTOM -> buildCustomDeptExpression(deptColumn, roleDataScope.getCustomDeptIds());
};
}
/**
* 构建列引用
*
* @param alias 表别名
* @param columnName 列名
* @return 列引用
*/
private Column buildColumn(String alias, String columnName) {
if (StrUtil.isNotBlank(alias)) {
return new Column(alias + StringPool.DOT + columnName);
}
return new Column(columnName);
}
/**
* 构建等于条件
*
* @param column 列
* @param value 值
* @return 等于表达式
*/
private Expression buildEqualsExpression(Column column, Long value) {
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(column);
equalsTo.setRightExpression(new LongValue(value));
return equalsTo;
}
/**
* 构建部门及子部门数据权限条件
* <p>
* SQL: dept_id IN (SELECT id FROM sys_dept WHERE id = ? OR FIND_IN_SET(?, tree_path))
*
* @param deptColumn 部门列
* @param deptId 部门ID
* @return IN 子查询表达式
*/
@SneakyThrows
private Expression buildDeptAndSubExpression(Column deptColumn, Long deptId) {
// 使用字符串解析,避免不同 JSqlParser 版本下 InExpression/ItemsList 渲染差异导致 SQL 语法错误
// SQL: dept_id IN (SELECT id FROM sys_dept WHERE id = ? OR FIND_IN_SET(?, tree_path))
String columnName = deptColumn.toString();
String sql = columnName + " IN (SELECT " + DEPT_ID_COLUMN + " FROM " + DEPT_TABLE +
" WHERE " + DEPT_ID_COLUMN + " = " + deptId +
" OR FIND_IN_SET(" + deptId + ", " + DEPT_TREE_PATH_COLUMN + "))";
return CCJSqlParserUtil.parseCondExpression(sql);
}
/**
* 构建自定义部门数据权限条件
* <p>
* SQL: dept_id IN (?, ?, ...)
*
* @param deptColumn 部门列
* @param customDeptIds 自定义部门ID列表
* @return IN 表达式,如果没有部门则返回 1=0
*/
@SneakyThrows
private Expression buildCustomDeptExpression(Column deptColumn, List<Long> customDeptIds) {
if (CollectionUtil.isEmpty(customDeptIds)) {
// 没有自定义部门,返回 1=0无权限
EqualsTo falseCondition = new EqualsTo();
falseCondition.setLeftExpression(new LongValue(1));
falseCondition.setRightExpression(new LongValue(0));
return falseCondition;
}
// 使用字符串解析,确保渲染始终为 IN (..)
String columnName = deptColumn.toString();
String ids = customDeptIds.stream().map(String::valueOf).reduce((a, b) -> a + "," + b).orElse("");
String sql = columnName + " IN (" + ids + ")";
return CCJSqlParserUtil.parseCondExpression(sql);
}
}