本文是基于ruoyi-vue-plus5.x版本,仅为学习用,有误的地方欢迎大家批评指正。
权限分类
若依框架权限又分为菜单权限和数据权限
1. 菜单权限
菜单权限,也常被称为功能权限 或操作权限 ,它控制了用户能够访问系统中的哪些功能模块以及在这些模块中能够执行哪些操作。
在若依中,它具体表现为:
- 菜单的可见性:左侧的菜单栏中,用户能看到并点击哪些菜单项。
- 页面的访问权:用户是否能通过URL直接访问某个功能页面。
- 按钮的可用性:在某个页面上,用户能看到并点击哪些按钮(如:新增、修改、删除、导出等)。
实现原理
菜单权限是基于 角色(Role) 来实现的,其逻辑链条如下:
用户 (User) → 角色 (Role) → 菜单/按钮 (Menu/Button)
2. 数据权限
数据权限控制了当用户访问某个功能列表(如用户列表、订单列表)时,能够查看到的数据范围 。它是在用户已经拥有了菜单权限(即能进入这个页面)的前提下,对返回的数据行进行过滤。
实现原理
数据权限同样是基于 角色(Role) 来配置的,但其技术实现更为巧妙,主要利用了 AOP(面向切面编程) 和 SQL 动态拼接。
技术实现架构
菜单权限我们比较常见,就不着重剖析菜单权限了,今天的主角是数据权限。
数据权限核心类/组件表格
类/组件名称 | 说明 | 功能 |
---|---|---|
MybatisPlusConfig | 数据权限配置 | 配置 MyBatis-Plus 的全局设置,包括注入数据权限拦截器(PlusDataPermissionInterceptor),确保所有 SQL 查询都能被拦截和处理。 |
DataScopeType | 数据权限模板定义 | 定义数据权限的模板类型(如 ALL: 全部数据、DEPT: 本部门数据、CUSTOM: 自定义),用于标准化权限过滤逻辑。 |
DataPermission | 数据权限组注解 | 标注在 Mapper 或 Service 方法上,开启数据权限过滤(默认过滤部门权限)。可指定多个数据列,支持忽略某些角色。 |
DataColumn | 具体的数据权限字段标注 | 与 DataPermission 配合使用,替换模板中的 key 变量(如指定部门字段 "deptId"),允许自定义 SQL 过滤条件中的字段。 |
PlusDataPermissionInterceptor | 数据权限 SQL 拦截器 | 拦截所有 SQL 执行,检查方法是否标注了 DataPermission 注解,如果有,则委托处理器添加过滤条件。 |
PlusDataPermissionHandler | 数据权限处理器 | 处理被拦截的 SQL,根据用户上下文(如部门、角色)动态添加 WHERE 子句,实现权限过滤。 |
DataPermissionHelper | 数据权限助手 | 操作线程级上下文变量(如设置/获取当前用户的数据权限范围),支持在代码中手动控制权限上下文。 |
SysDataScopeService | 自定义 Bean 处理数据权限 | 提供自定义扩展接口,用于复杂场景下的数据权限处理(如多租户、角色扩展),可注入到处理器中实现个性化逻辑。 |
代码拦截流程
核心代码
MybatisPlusConfig
全局启用数据权限拦截,确保所有 Mapper 的 SQL 都能被检查。原理基于 MyBatis-Plus 的插件机制(InnerInterceptor 接口),在 SQL 执行前插入自定义逻辑。
java
@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
@MapperScan("${mybatis-plus.mapperPackage}")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 数据权限处理
interceptor.addInnerInterceptor(dataPermissionInterceptor());
// 分页插件
interceptor.addInnerInterceptor(paginationInnerInterceptor());
// 乐观锁插件
interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
return interceptor;
}
//....其他配置
}
DataPermission
这是一个方法级注解,支持配置多个数据权限注解
java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataPermission {
DataColumn[] value();
}
DataColumn
辅助注解,用于 DataPermission 内定义字段映射,一个注解只能对应一个模板。
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataColumn {
/**
* 占位符关键字
*/
String[] key() default "deptName";
/**
* 占位符替换值
*/
String[] value() default "dept_id";
}
PlusDataPermissionInterceptor
实现 MyBatis-Plus 的 InnerInterceptor 接口,重写 beforeQuery 方法,在 SQL 执行前拦截,以及继承JsqlParserSupport
实现对sql的解析(依赖JsqlParser
库实现)
查询语句数据权限源码
java
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 检查忽略注解
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
return;
}
// 检查是否无效 无数据权限注解
if (dataPermissionHandler.isInvalid(ms.getId())) {
return;
}
// 解析 sql 分配对应方法
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
}
首先是检查是否存在忽略插件的注解@InterceptorIgnore
,mybatis-plus自带的插件忽略,忽略则直接跳过

java
// 内部实现的一个简单内存缓存,如果没有权限注解@DataColumn,下次就直接跳过了,就不会走下面的解析
if (dataPermissionHandler.isInvalid(ms.getId())) {
return;
}
// 这里就是包装了一下,增加了MyBatis反射解析 MetaObject boundSql,
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
//反射调用MetaObject boundSql解析后的set方法,替换为增加权限过滤的sql
mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
更新和删除数据权限源码
这里就很简单了,解析判断sql语句类型,然后包装反射替换sql
java
@Override
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
MappedStatement ms = mpSh.mappedStatement();
SqlCommandType sct = ms.getSqlCommandType();
if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
return;
}
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
mpBs.sql(parserMulti(mpBs.sql(), ms.getId()));
}
}
具体sql解析替换源码,依赖JsqlParser
库解析,然后取得条件where部分,然后增加数据权限过滤sql
java
@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
SelectBody selectBody = select.getSelectBody();
if (selectBody instanceof PlainSelect) {
this.setWhere((PlainSelect) selectBody, (String) obj);
} else if (selectBody instanceof SetOperationList) {
SetOperationList setOperationList = (SetOperationList) selectBody;
List<SelectBody> selectBodyList = setOperationList.getSelects();
selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
}
}
@Override
protected void processUpdate(Update update, int index, String sql, Object obj) {
Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), (String) obj, false);
if (null != sqlSegment) {
update.setWhere(sqlSegment);
}
}
@Override
protected void processDelete(Delete delete, int index, String sql, Object obj) {
Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), (String) obj, false);
if (null != sqlSegment) {
delete.setWhere(sqlSegment);
}
}
/**
* 设置 where 条件
*
* @param plainSelect 查询对象
* @param mappedStatementId 执行方法id
*/
protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true);
if (null != sqlSegment) {
plainSelect.setWhere(sqlSegment);
}
}
java
SelectBody selectBody = select.getSelectBody();
作用:从 Select 对象中提取查询的主体部分(SelectBody)。
原理
-
select 是一个 JSQLParser 的 Select 对象,表示解析后的 SELECT 语句。
-
getSelectBody()
返回 SELECT 语句的核心部分(SelectBody),它可能是:PlainSelect(普通查询 )、SetOperationList(包含 UNION、INTERSECT 等集合操作的查询) 、其他类型(如 WithItem,但此处未处理)。
处理 SetOperationList
java
else if (selectBody instanceof SetOperationList) {
SetOperationList setOperationList = (SetOperationList) selectBody;
List<SelectBody> selectBodyList = setOperationList.getSelects();
selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
}
如果 SelectBody 是 SetOperationList 类型(包含 UNION 等操作),提取其子查询列表,并为每个子查询(PlainSelect)添加权限过滤条件。
setWhere 方法
setWhere 方法定义如下(在 PlusDataPermissionInterceptor 中):
java
protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true);
if (null != sqlSegment) {
plainSelect.setWhere(sqlSegment);
}
}
为 PlainSelect 的 WHERE 子句添加权限条件。
-
dataPermissionHandler.getSqlSegment
根据@DataPermission
注解和用户上下文生成权限条件(Expression 对象,例如 dept_id = 100)。 -
plainSelect.getWhere()
获取当前的 WHERE 子句(可能为 null)。 -
如果 sqlSegment 不为 null,通过 plainSelect.setWhere 设置新的 WHERE 子句。
-
如果已有 WHERE 子句,
getSqlSegment
可能合并条件(例如WHERE existing_condition AND dept_id = 100
)。
PlusDataPermissionHandler
核心处理器,基于用户上下文构建过滤条件
java
public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) {
//先查找是否存在数据权限注解,如果没有就加到无需数据权限校验的缓存方法,也就是上面拦截器中的第二个判断条件
DataColumn[] dataColumns = findAnnotation(mappedStatementId);
if (ArrayUtil.isEmpty(dataColumns)) {
invalidCacheSet.add(mappedStatementId);
return where;
}
// 拿到当前用户的登录信息
LoginUser currentUser = DataPermissionHelper.getVariable("user");
if (ObjectUtil.isNull(currentUser)) {
currentUser = LoginHelper.getLoginUser();
DataPermissionHelper.setVariable("user", currentUser);
}
// 如果是超级管理员,则不过滤数据
if (LoginHelper.isAdmin()) {
return where;
}
//真正的按照权限构建sql语句
String dataFilterSql = buildDataFilter(dataColumns, isSelect);
if (StringUtils.isBlank(dataFilterSql)) {
return where;
}
try {
Expression expression = CCJSqlParserUtil.parseExpression(dataFilterSql);
// 数据权限sql,数据权限使用单独的括号 防止与其他条件冲突
Parenthesis parenthesis = new Parenthesis(expression);
if (ObjectUtil.isNotNull(where)) {
return new AndExpression(where, parenthesis);
} else {
return parenthesis;
}
} catch (JSQLParserException e) {
throw new ServiceException("数据权限解析异常 => " + e.getMessage());
}
}
过滤sql代码
java
/**
* 构造数据过滤sql
*/
private String buildDataFilter(DataColumn[] dataColumns, boolean isSelect) {
// 更新或删除需满足所有条件
String joinStr = isSelect ? " OR " : " AND ";
//这里将用户的信息设置到spel的上下文中,这样在DataScopeType数据权限的模板sql中调用方法时就可以从这个user变量取得对应的角色,从而调用数据权限查询的bean的方法,返回权限 IN (a,b,c)这个集合
LoginUser user = DataPermissionHelper.getVariable("user");
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(beanResolver);
DataPermissionHelper.getContext().forEach(context::setVariable);
Set<String> conditions = new HashSet<>();
for (RoleDTO role : user.getRoles()) {
//设置角色id,用于DataScopeType数据权限的模板sql调用,如#{@sdss.getRoleCustom( #user.roleId )}
user.setRoleId(role.getRoleId());
// 获取角色权限泛型
DataScopeType type = DataScopeType.findCode(role.getDataScope());
if (ObjectUtil.isNull(type)) {
throw new ServiceException("角色数据范围异常 => " + role.getDataScope());
}
// 全部数据权限直接返回
if (type == DataScopeType.ALL) {
return "";
}
boolean isSuccess = false;
for (DataColumn dataColumn : dataColumns) {
if (dataColumn.key().length != dataColumn.value().length) {
throw new ServiceException("角色数据范围异常 => key与value长度不匹配");
}
// 不包含 key 变量 则不处理
if (!StringUtils.containsAny(type.getSqlTemplate(),
Arrays.stream(dataColumn.key()).map(key -> "#" + key).toArray(String[]::new)
)) {
continue;
}
// 设置注解变量 key 为表达式变量 value 为变量值
for (int i = 0; i < dataColumn.key().length; i++) {
context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);
}
// 解析sql模板并填充
String sql = parser.parseExpression(type.getSqlTemplate(), parserContext).getValue(context, String.class);
conditions.add(joinStr + sql);
isSuccess = true;
}
// 未处理成功则填充兜底方案
if (!isSuccess && StringUtils.isNotBlank(type.getElseSql())) {
conditions.add(joinStr + type.getElseSql());
}
}
//这里汇总了多个角色的时候的数据权限
if (CollUtil.isNotEmpty(conditions)) {
//多个条件拼接为一个:如查询的 OR deptId=xx OR userId=xx
String sql = StreamUtils.join(conditions, Function.identity(), "");
//去掉首个条件标识变成:deptId=xx OR userId=xx
return sql.substring(joinStr.length());
}
return "";
}
DataPermissionHelper
使用 ThreadLocal 存储上下文,如当前的用户信息等
java
//如存储当前用户信息
LoginUser currentUser = DataPermissionHelper.getVariable("user");
if (ObjectUtil.isNull(currentUser)) {
currentUser = LoginHelper.getLoginUser();
DataPermissionHelper.setVariable("user", currentUser);
}
//构建过滤sql
//取得当前用户的西悉尼,然后添加到spel解析器中,用于后续的数据权限模板中替换变量
private String buildDataFilter(DataColumn[] dataColumns, boolean isSelect) {
// 更新或删除需满足所有条件
String joinStr = isSelect ? " OR " : " AND ";
LoginUser user = DataPermissionHelper.getVariable("user");
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(beanResolver);
DataPermissionHelper.getContext().forEach(context::setVariable);
Set<String> conditions = new HashSet<>();
for (RoleDTO role : user.getRoles()) {
user.setRoleId(role.getRoleId());
DataScopeType
这是一个枚举类,定义权限模板常量。
java
@Getter
@AllArgsConstructor
public enum DataScopeType {
/**
* 全部数据权限
*/
ALL("1", "", ""),
/**
* 自定数据权限
*/
CUSTOM("2", " #{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) ", ""),
/**
* 部门数据权限
*/
DEPT("3", " #{#deptName} = #{#user.deptId} ", ""),
/**
* 部门及以下数据权限
*/
DEPT_AND_CHILD("4", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )", ""),
/**
* 仅本人数据权限
*/
SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 "),
/**
* 销售主体权限
*/
SALE("6", " #{#saleName} = #{#user.deptId} ", " 1 = 0 ")
;
private final String code;
/**
* 语法 采用 spel 模板表达式
*/
private final String sqlTemplate;
/**
* 不满足 sqlTemplate 则填充
*/
private final String elseSql;
public static DataScopeType findCode(String code) {
if (StringUtils.isBlank(code)) {
return null;
}
for (DataScopeType type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return null;
}
}
-
code:作为权限的标识符,角色表存在对应的字段记录对应的数据权限
-
sqlTemplate:需要替换的sql模板,使用的是spel解析,比如:#{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) 其中deptName是数据注解@DataColumn的key,会将key替换为value,而value对应的是这次sql查询的某个字段,
java@DataPermission({ @DataColumn(key = "deptName", value = "bab.duty_subject"), @DataColumn(key = "userName", value = "bab.ar_user_id") })
如bab.duty_subject表的这个字段需要做数据权限,则实际会替换为
select xxx from xxx as bab where bab.duty_subject in (xxx)
,sql模板中的#{@sdss.getRoleCustom( #user.roleId )}
其实是调用了一个数据权限BeanSysDataScopeService
的方法,sdss为bean的名称。java@RequiredArgsConstructor @Service("sdss") public class SysDataScopeServiceImpl implements ISysDataScopeService { private final SysRoleDeptMapper roleDeptMapper; private final SysDeptMapper deptMapper; @Override public String getRoleCustom(Long roleId) { ....权限实现 } //其他权限方法 }
核心代码调用时序图
总结
通过这套机制,若依实现了与业务逻辑的解耦,基于MyBatis-Plus的数据权限框架在设计上更加现代化和灵活,通过模板化的配置和多层次的扩展点,可自动应用复杂的数据权限逻辑,极大提高了开发效率和代码的可维护性。