一、业务场景:
根据业务需要,这里将角色按照数据范围做权限限定,提供四级权限分别为:
1、全部:可以查看所有的数据;
2、部门及以下:按照部门架构,可以查看部门内所有人的数据;
3、个人:仅能查看由自己创建;
4、自定义:可以进行动态拼接,根据不同的字段拼接SQL。
二、思路:
1、定义Mybatis拦截器DataScopeInterceptor,用于每次拦截查询sql语句,附带数据范围权限的SQL条件;
2、定义注解DataScope及其它类,用来声明用于权限过滤的接口;
3、SpringBoot装配该拦截器。
注:这里如果有使用MybatisPlus的分页插件,需要保证执行顺序:DataScopeInterceptor > PaginationInterceptor
三、具体说明:
我们有单独存储权限的表结构,分为可读自己可操作自己、读部门及以下可操作所有、读所有及可操作所有、可操作所有读自定义、可操作自己读自定义。并且这个权限表是和菜单权限挂在一起的,根据每个角色配置菜单的同时可以配置当前菜单所拥有的数据权限,其实就是角色绑定菜单Id绑定权限表Id。然后页面通过查询菜单接口将每个菜单绑定的权限信息返回前端,前端在接口查询时带着对应的参数返回后端,后端根据不同的权限写不同的查询并拼接到执行的SQL中,固定读自己、读部门、读所有是按创建人字段进行in查询,自定义可以随意组合查询字段。
四、权限代码:
1、注解
java
package com.sansint.canteen.datascope.annotation;
import com.sansint.canteen.datascope.constant.DatascopeConstants;
import com.sansint.canteen.datascope.enums.DataScopeType;
import com.sansint.canteen.datascope.holder.ExtraParamHolder;
import java.lang.annotation.*;
/**
* 数据权限过滤注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {
/**
* 范围权限类别
*/
public DataScopeType type() default DataScopeType.DATA_SCOPE_ALL;
/**
* 部门表的别名
*/
public String deptAlias() default "d";
/**
* 用户表的别名
*/
public String userAlias() default "u";
/**
* 是否附加"或条件"查询
* 附加的"或条件sql片段",通过{@link ExtraParamHolder#setParam(String, Object)}设置,key为{@link DatascopeConstants#CUSTOM_OR_CONDITION}<br/>
* 例:ExtraParamHolder.setParam(DatascopeConstants.CUSTOM_OR_CONDITION, " or cc_user_id = 5 ") 设置<br/>
* 1. 不附加"或条件"查询后,查询语句为:
* SELECT * FROM table WHERE condition1 = 1 and condition2 = 2 and (create_user IN (1,2,3))<br/>
* 2. 附加"或条件"查询后,查询语句为:
* SELECT * FROM table WHERE condition1 = 1 and condition2 = 2 and (create_user IN (1,2,3) or cc_user_id = 5 )
*/
boolean attachOrCondition() default false;
}
2、常量
这个根据实际项目中需要的字段进行调整
java
package com.sansint.canteen.datascope.constant;
/**
* 缓存的key 常量
*
* @author engineering
*/
public class DatascopeConstants
{
/**
* 数据权限code
*/
public static final String READ_SCOPE_CODE = "readScopeCode";
/**
* 当前登录用户
*/
public static final String SAN_SINT_USER = "sanSintUser";
/**
* 创建人字段名称
*/
public static final String CREATE_USER = "create_user";
/**
* 自定义权限字段名称 ThreadLocal 中存储
*/
public static final String CUSTOM_SCOPE = "read_custom";
/**
* 自定义或条件的sql片段
*/
public static final String CUSTOM_OR_CONDITION = "customOrCondition";
}
3、枚举
这个根据实际项目中需要的字段进行调整
java
package com.sansint.canteen.datascope.enums;
import java.util.Objects;
public enum DataScopeType {
/**
* 全部数据权限
*/
DATA_SCOPE_ALL("read_all"),
/**
* 自定数据权限
*/
DATA_SCOPE_CUSTOM("read_custom"),
/**
* 部门数据权限
*/
DATA_SCOPE_DEPT("read_only_manage"),
/**
* 部门及以下数据权限
*/
DATA_SCOPE_DEPT_AND_CHILD("read_manage"),
/**
* 仅本人数据权限
*/
DATA_SCOPE_SELF("read_only");
private String code;
public String getCode() {
return code;
}
DataScopeType(String code) {
this.code = code;
}
public static DataScopeType of(String code) {
Objects.requireNonNull(code, "数据范围权限类型不允许为空");
for (DataScopeType dataScopeType : DataScopeType.values()) {
if (dataScopeType.getCode().equals(code)) {
return dataScopeType;
}
}
throw new IllegalArgumentException(String.format("未识别的数据范围权限类型值[%s]", code));
}
}
4、设置权限参数类
这个根据实际项目中需要的字段进行调整
java
package com.sansint.canteen.datascope.holder;
import org.springframework.util.ObjectUtils;
import java.util.HashMap;
import java.util.Map;
import java.lang.ThreadLocal;
public class ExtraParamHolder {
// 存储额外参数的 ThreadLocal,键为参数名,值为参数值
private static final ThreadLocal<Map<String, Object>> extraParams = ThreadLocal.withInitial(HashMap::new);
// 设置参数
public static void setParam(String key, Object value) {
if (ObjectUtils.isEmpty(key) || ObjectUtils.isEmpty(value)) {
return;
}
extraParams.get().put(key, value);
}
// 获取参数
public static Object getParam(String key) {
return extraParams.get().get(key);
}
// 清除参数(避免内存泄漏)
public static void clear() {
extraParams.remove();
}
}
5、拦截器
java
package com.sansint.canteen.datascope.plugins;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.sansint.canteen.datascope.annotation.DataScope;
import com.sansint.canteen.datascope.constant.DatascopeConstants;
import com.sansint.canteen.datascope.enums.DataScopeType;
import com.sansint.canteen.datascope.holder.ExtraParamHolder;
import com.sansint.common.utils.SpringUtils;
import com.sansint.core.secure.SanSintUser;
import com.sansint.core.secure.utils.SecureUtil;
import com.sansint.system.user.feign.IUserClient;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.*;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;
import static com.sansint.canteen.datascope.constant.DatascopeConstants.CREATE_USER;
import static com.sansint.canteen.datascope.constant.DatascopeConstants.READ_SCOPE_CODE;
/**
* 基于 JSQLParser 4.2 版本的数据权限拦截器
*/
@Component
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class DataScopeInnerInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取 StatementHandler 及相关对象
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
// 2. 只处理 SELECT 语句
if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
return invocation.proceed();
}
// 3. 获取 Mapper 方法上的 @DataScope 注解
DataScope dataScope = getDataSourceAnnotation(mappedStatement);
if (dataScope == null) {
return invocation.proceed(); // 无注解,直接执行
}
// 4、获取调用方法的参数
Map<String, Object> paramValues = getParamValues(boundSql, mappedStatement);
// 定义实际要使用的权限类型
String actualType = null;
if (paramValues.containsKey(READ_SCOPE_CODE)) {
Object value = paramValues.get(READ_SCOPE_CODE);
if (value != null) {
actualType = value.toString();
}
}
if (StringUtils.isEmpty(actualType)) {
Object actualTypeObj = ExtraParamHolder.getParam(READ_SCOPE_CODE);
if (actualTypeObj == null) {
return invocation.proceed(); // 没有数据权限类型,直接执行
}
actualType = actualTypeObj.toString();
}
// 如果权限类型不存在,则无注解,直接执行
if(StringUtils.isEmpty(actualType)){
return invocation.proceed(); // 无注解,直接执行
}
// actualType 是 【read_only:operate_only】 这种格式的,只需要冒号前的部分
actualType = actualType.split(":")[0];
// 5. 解析原始 SQL,生成带权限条件的新 SQL
String originalSql = boundSql.getSql();
String newSql = buildDataScopeSql(originalSql, actualType, dataScope);
if (StringUtils.hasText(newSql)) {
metaObject.setValue("delegate.boundSql.sql", newSql); // 替换 SQL
}
return invocation.proceed();
}
/**
* 获取 Mapper 方法上的 @DataScope 注解
*/
private DataScope getDataSourceAnnotation(MappedStatement mappedStatement) throws ClassNotFoundException {
String id = mappedStatement.getId(); // 格式:com.xxx.mapper.UserMapper.selectList
String className = id.substring(0, id.lastIndexOf("."));
String methodName = id.substring(id.lastIndexOf(".") + 1);
methodName = methodName.replace("_COUNT", "");
Class<?> mapperClass = Class.forName(className);
for (Method method : mapperClass.getMethods()) {
if (method.getName().equals(methodName) && method.isAnnotationPresent(DataScope.class)) {
return method.getAnnotation(DataScope.class);
}
}
return null;
}
// 解析参数对象,获取参数名-值映射(适配不同类型的参数,支持基本类型、Map、JavaBean 等)
private Map<String, Object> getParamValues(BoundSql boundSql, MappedStatement mappedStatement) {
// 获取参数名称列表(从 MappedStatement 中获取参数映射信息)
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
// 获取参数值(从 BoundSql 中获取实际参数对象)
Object parameterObject = boundSql.getParameterObject();
Configuration configuration = mappedStatement.getConfiguration();
Map<String, Object> paramMap = new HashMap<>();
if (parameterObject == null) {
return paramMap;
}
// 1. 参数是 Map 类型(直接获取 key-value)
if (parameterObject instanceof Map) {
return (Map<String, Object>) parameterObject;
}
// 2. 参数是 JavaBean 或基本类型(通过 ParameterMapping 解析)
MetaObject metaParam = configuration.newMetaObject(parameterObject);
for (ParameterMapping mapping : parameterMappings) {
String paramName = mapping.getProperty();
Object value = metaParam.getValue(paramName);
paramMap.put(paramName, value);
}
return paramMap;
}
/**
* 构建带数据权限条件的 SQL(适配 JSQLParser 4.2)
*/
private String buildDataScopeSql(String originalSql, String actualType, DataScope dataScope) throws JSQLParserException {
// 解析 SQL 为抽象语法树(AST)
Statement statement = CCJSqlParserUtil.parse(originalSql);
Select select = (Select) statement;
SelectBody selectBody = select.getSelectBody();
// 处理不同类型的 SELECT 语句
processSelectBody(selectBody, actualType, dataScope);
// 将 AST 转回 SQL 字符串
return select.toString();
}
/**
* 处理 SELECT 主体(递归处理子查询)
*/
private void processSelectBody(SelectBody selectBody, String actualType, DataScope dataScope) {
if (selectBody instanceof PlainSelect) {
// 处理简单查询(最常见场景)
processPlainSelect((PlainSelect) selectBody, actualType, dataScope);
} else if (selectBody instanceof WithItem) {
WithItem withItem = (WithItem) selectBody;
if (withItem.getSubSelect().getSelectBody() != null) {
processSelectBody(withItem.getSubSelect().getSelectBody(), actualType, dataScope); // 递归处理子查询
}
} else if (selectBody instanceof SetOperationList) {
// 处理联合查询(UNION/INTERSECT 等)
SetOperationList setOperationList = (SetOperationList) selectBody;
// JSQLParser 4.2 中 SetOperationList 的 getSelects() 返回 List<SelectBody>
for (SelectBody body : setOperationList.getSelects()) {
processSelectBody(body, actualType, dataScope);
}
}
}
/**
* 处理简单查询(PlainSelect),添加数据权限条件
*/
private void processPlainSelect(PlainSelect plainSelect, String actualType, DataScope dataScope) {
// 1. 生成数据权限条件表达式
Expression scopeCondition = buildScopeCondition(actualType, dataScope);
if (scopeCondition == null) {
return; // 无权限条件,不处理
}
// 2. 获取原始 WHERE 条件
Expression where = plainSelect.getWhere();
// 3. 拼接新的 WHERE 条件(原始条件 + 权限条件)
if (where == null) {
plainSelect.setWhere(scopeCondition); // 无原始条件,直接设为权限条件
} else {
plainSelect.setWhere(new AndExpression(where, scopeCondition)); // 有原始条件,用 AND 拼接
}
// // 4. 递归处理 FROM 子句中的子查询
// FromItem fromItem = plainSelect.getFromItem();
// if (fromItem instanceof SubSelect) {
// SubSelect subSelect = (SubSelect) fromItem;
// processSelectBody(subSelect.getSelectBody(), dataScope);
// } else if (fromItem instanceof WithItem) {
// WithItem withItem = (WithItem) fromItem;
// processSelectBody(withItem.getSubSelect().getSelectBody(), dataScope);
// }
//
// // 5. 处理 JOIN 中的子查询(JSQLParser 4.2 中 Joins 为 List<Join>)
// List<Join> joins = plainSelect.getJoins();
// if (joins != null) {
// for (Join join : joins) {
// FromItem rightItem = join.getRightItem();
// if (rightItem instanceof SubSelect) {
// SubSelect subSelect = (SubSelect) rightItem;
// processSelectBody(subSelect.getSelectBody(), dataScope);
// }
// }
// }
}
/**
* 构建数据权限条件表达式(核心逻辑)
*/
private Expression buildScopeCondition(String actualType, DataScope dataScope) {
try {
// 实际场景:从 ThreadLocal 获取当前登录用户
SanSintUser sanSintUser = SecureUtil.getUser();
if (sanSintUser == null) {
return null; // 未登录用户,无权限
}
// 获取注解中的表别名
String deptAlias = dataScope.deptAlias(); // 部门表别名
String userAlias = dataScope.userAlias(); // 用户表别名
boolean attachOrCondition = dataScope.attachOrCondition(); // 是否附加"或条件"查询
Object orCondition = null;
if (attachOrCondition) {
orCondition = ExtraParamHolder.getParam(DatascopeConstants.CUSTOM_OR_CONDITION);
}
// 生成权限条件(示例:部门权限 + 用户创建权限)
StringBuilder condition = new StringBuilder();
// 部门及以下数据权限
if (actualType.equals(DataScopeType.DATA_SCOPE_DEPT_AND_CHILD.getCode())) {
IUserClient userClient = SpringUtils.getBean(IUserClient.class);
List<String> deptIdList = sanSintUser.getDeptIdList();
//如果后期有我是A部门领导,B部门员工的话这地方需要调整
List<Long> userIdList = userClient.selectUserIdListByDeptIds(deptIdList);
String userIdStr = "0";
if (userIdList != null && !userIdList.isEmpty()) {
userIdStr = userIdList.stream().map(String::valueOf).collect(Collectors.joining(","));
}
condition
.append("(")// 给or条件预添加括号,即使没有or条件,括号也不影响执行结果
.append(StringUtils.isEmpty(userAlias) ? CREATE_USER + " in (" : userAlias + "." + CREATE_USER + " in (")
//.append(StringUtils.isEmpty(deptAlias) ? userAlias + "." + CREATE_USER + " in (" : deptAlias + "." + CREATE_USER + " in (")
.append(userIdStr)
.append(") ")
.append(attachOrCondition && !ObjectUtils.isEmpty(orCondition) ? orCondition : "")
.append(")");
}
// 仅本人数据权限:create_by = 当前用户ID
if (actualType.equals(DataScopeType.DATA_SCOPE_SELF.getCode())) {
if (condition.length() > 0) {
condition.append(" AND "); // 拼接多个条件
}
condition
.append("(")// 给or条件预添加括号,即使没有or条件,括号也不影响执行结果
.append(StringUtils.isEmpty(userAlias) ? CREATE_USER + " = " : userAlias + "." + CREATE_USER + " = ")
.append(sanSintUser.getUserId().toString())
.append(" ")
.append(attachOrCondition && !ObjectUtils.isEmpty(orCondition) ? orCondition : "")
.append(")");
}
// 自定义数据权限 :自定义SQL语句
if (actualType.equals(DataScopeType.DATA_SCOPE_CUSTOM.getCode())) {
if (condition.length() > 0) {
condition.append(" AND "); // 拼接多个条件
}
Object param = ExtraParamHolder.getParam(DatascopeConstants.CUSTOM_SCOPE);
condition
.append("(")
.append(StringUtils.isEmpty(userAlias) ? param : userAlias + "." + param)
.append(")");
}
if (condition.length() == 0) {
return null; // 无权限条件
}
// JSQLParser 4.2 中解析条件表达式的方法
return CCJSqlParserUtil.parseCondExpression(condition.toString());
} catch (JSQLParserException e) {
throw new RuntimeException("生成数据权限条件失败", e);
}
}
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this); // 只代理 StatementHandler
}
return target;
}
@Override
public void setProperties(Properties properties) {
// 可配置参数(如默认表别名)
}
}
6、入口业务接口
根据前端传的权限字段及值,存入ThreadLocal中且使用后销毁。
java
/**
* 条件查询列表数据
*
* @param canteenAttendanceDto 查询条件
*/
@Override
public List<CanteenAttendance> list(CanteenAttendanceDto canteenAttendanceDto, SanSintUser sanSintUser) {
List<CanteenAttendance> canteenAttendanceList;
try {
ExtraParamHolder.setParam(READ_SCOPE_CODE, canteenAttendanceDto.getReadScopeCode());
canteenAttendanceList = canteenAttendanceMapper.selectList(new LambdaQueryWrapper<CanteenAttendance>()
.eq(CanteenAttendance::getTenantId, sanSintUser.getTenantId())
// 逻辑删除字段
.eq(CanteenAttendance::getDeletedFlag, NumberConstant.ZERO)
.eq(Objects.nonNull(canteenAttendanceDto.getIsEstablish()), CanteenAttendance::getIsEstablish, canteenAttendanceDto.getIsEstablish())
.eq(Objects.nonNull(canteenAttendanceDto.getDeptId()), CanteenAttendance::getDeptId, canteenAttendanceDto.getDeptId())
.eq(Objects.nonNull(canteenAttendanceDto.getPersonId()), CanteenAttendance::getPersonId, canteenAttendanceDto.getPersonId())
.like(Objects.nonNull(canteenAttendanceDto.getPersonNum()), CanteenAttendance::getPersonNum, canteenAttendanceDto.getPersonNum())
.eq(Objects.nonNull(canteenAttendanceDto.getMonth()), CanteenAttendance::getMonth, canteenAttendanceDto.getMonth())
.eq(Objects.nonNull(canteenAttendanceDto.getYear()), CanteenAttendance::getYear, canteenAttendanceDto.getYear())
.orderByDesc(CanteenAttendance::getCreateTime));
} finally {
ExtraParamHolder.clear();
}
return canteenAttendanceList;
}
7、入口Mapper接口
这个注解是必须加的,一般使用最多就是第二个参数,如果涉及多表联查的话,把主表别名写在第二个参数上。如果单表则图中所示即可。
java
package com.sansint.canteen.mapper.administration;
import com.baomidou.mybatisplus.annotation.SqlParser;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.sansint.canteen.datascope.annotation.DataScope;
import com.sansint.canteen.domain.administration.entity.CanteenAttendance;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface CanteenAttendanceMapper extends BaseMapper<CanteenAttendance> {
/**
* 查询所有行
* @param queryWrapper 查询条件
* @return 对象列表
*/
@DataScope(deptAlias = "", userAlias = "")
List<CanteenAttendance> selectList(@Param("ew") Wrapper<CanteenAttendance> queryWrapper);
}