利用 MyBatis Plus 拦截器动态管理数据访问权限
利用 MyBatis Plus 拦截器动态管理数据访问权限
引言
在软件开发过程中,我们经常遇到需要根据用户角色来控制数据访问权限的需求。特别是在列表数据展示时,要确保用户只能查看其权限数据范围内的。本文将介绍一种通过MyBatis拦截器实现数据权限控制的方案,该方案灵活且易于集成到现有项目中。
数据权限分配
首先,我们需要创建一个自定义注解 @UserDataPermission,用于标记需要进行数据权限控制的方法或类。具体代码如下:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface UserDataPermission {
}
接下来,创建一个拦截器 MyDataPermissionInterceptor,实现 InnerInterceptor 接口,并重写查询方法。代码如下:
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.EqualsAndHashCode;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectBody;
import net.sf.jsqlparser.statement.select.SetOperationList;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.sql.SQLException;
import java.util.List;
@Data
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class MyDataPermissionInterceptor extends JsqlParserSupport implements InnerInterceptor {
private MyDataPermissionHandler dataPermissionHandler;
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
return;
}
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
mpBs.sql(this.parserSingle(mpBs.sql(), ms.getId()));
}
@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));
}
}
private void setWhere(PlainSelect plainSelect, String whereSegment) {
Expression sqlSegment = this.dataPermissionHandler.getSqlSegment(plainSelect, whereSegment);
if (sqlSegment != null) {
plainSelect.setWhere(sqlSegment);
}
}
}
创建一个处理类 MyDataPermissionHandler,用于生成数据权限的 SQL 片段。如下代码:
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.HexValue;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.PlainSelect;
import java.lang.reflect.Method;
@Slf4j
public class MyDataPermissionHandler {
private RemoteRoleService remoteRoleService;
private RemoteUserService remoteUserService;
@SneakyThrows(Exception.class)
public Expression getSqlSegment(PlainSelect plainSelect, String whereSegment) {
Expression where = plainSelect.getWhere();
if (where == null) {
where = new HexValue("1=1");
}
log.info("开始进行权限过滤, where: {}, mappedStatementId: {}", where, whereSegment);
String className = whereSegment.substring(0, whereSegment.lastIndexOf("."));
String methodName = whereSegment.substring(whereSegment.lastIndexOf(".") + 1);
Table fromItem = (Table) plainSelect.getFromItem();
// 示例:根据用户角色生成不同的 SQL 片段
String roleName = "DATA_MANAGER"; // 假设从上下文中获取当前用户的角色
DataScope scope = DataPermission.getScope(List.of(roleName));
if (scope == DataScope.ALL) {
return where;
} else if (scope == DataScope.DEPT) {
// 假设有一个字段 department_id
Column column = new Column(new Table(fromItem.getName()), "department_id");
StringValue value = new StringValue("123"); // 假设从上下文中获取当前用户的部门ID
return new EqualsTo(column, value);
} else if (scope == DataScope.MYSELF) {
// 假设有一个字段 user_id
Column column = new Column(new Table(fromItem.getName()), "user_id");
StringValue value = new StringValue("456"); // 假设从上下文中获取当前用户的ID
return new EqualsTo(column, value);
}
return where;
}
}
最后,将拦截器加入 MyBatis-Plus 插件中。具体入下代码:
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Component
public class MyBatisPlusConfig {
@Autowired
private MyDataPermissionHandler myDataPermissionHandler;
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
MyDataPermissionInterceptor dataPermissionInterceptor = new MyDataPermissionInterceptor();
dataPermissionInterceptor.setDataPermissionHandler(myDataPermissionHandler);
interceptor.addInnerInterceptor(dataPermissionInterceptor);
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
功能权限与数据权限
定义一个范围枚举 DataScope,用于表示不同的数据权限范围。如下示例:
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum DataScope {
ALL("ALL"),
DEPT("DEPT"),
MYSELF("MYSELF");
private String name;
}
定义一个角色枚举 DataPermission,用于表示不同的角色及其对应的数据权限范围。代码如下:
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Collection;
@AllArgsConstructor
@Getter
public enum DataPermission {
DATA_MANAGER("数据管理员", "DATA_MANAGER", DataScope.ALL),
DATA_AUDITOR("数据审核员", "DATA_AUDITOR", DataScope.DEPT),
DATA_OPERATOR("数据业务员", "DATA_OPERATOR", DataScope.MYSELF);
private String name;
private String code;
private DataScope scope;
public static String getName(String code) {
for (DataPermission type : DataPermission.values()) {
if (type.getCode().equals(code)) {
return type.getName();
}
}
return null;
}
public static String getCode(String name) {
for (DataPermission type : DataPermission.values()) {
if (type.getName().equals(name)) {
return type.getCode();
}
}
return null;
}
public static DataScope getScope(Collection<String> code) {
for (DataPermission type : DataPermission.values()) {
for (String v : code) {
if (type.getCode().equals(v)) {
return type.getScope();
}
}
}
return DataScope.MYSELF;
}
}
在 MyDataPermissionHandler 中重写 getSqlSegment 方法,根据角色生成不同的 SQL 片段。完整代码:
import cn.hutool.core.collection.CollectionUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.relational.*;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.PlainSelect;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
public class MyDataPermissionHandler {
private RemoteRoleService remoteRoleService;
private RemoteUserService remoteUserService;
@SneakyThrows(Exception.class)
public Expression getSqlSegment(PlainSelect plainSelect, String whereSegment) {
Expression where = plainSelect.getWhere();
if (where == null) {
where = new HexValue("1=1");
}
log.info("开始进行权限过滤, where: {}, mappedStatementId: {}", where, whereSegment);
String className = whereSegment.substring(0, whereSegment.lastIndexOf("."));
String methodName = whereSegment.substring(whereSegment.lastIndexOf(".") + 1);
Table fromItem = (Table) plainSelect.getFromItem();
// 示例:根据用户角色生成不同的 SQL 片段
String roleName = "DATA_MANAGER"; // 假设从上下文中获取当前用户的角色
DataScope scope = DataPermission.getScope(List.of(roleName));
if (scope == DataScope.ALL) {
return where;
} else if (scope == DataScope.DEPT) {
// 假设有一个字段 department_id
Column column = new Column(new Table(fromItem.getName()), "department_id");
StringValue value = new StringValue("123"); // 假设从上下文中获取当前用户的部门ID
return new EqualsTo(column, value);
} else if (scope == DataScope.MYSELF) {
// 假设有一个字段 user_id
Column column = new Column(new Table(fromItem.getName()), "user_id");
StringValue value = new StringValue("456"); // 假设从上下文中获取当前用户的ID
return new EqualsTo(column, value);
}
return where;
}
}
在实际项目中,我们可以在 Mapper 层的方法上添加 @UserDataPermission 注解,实现数据权限控制。
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import org.apache.ibatis.annotations.Param;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public interface DataPermissionMapper<T> extends BaseMapper<T> {
@Override
@UserDataPermission
T selectById(Serializable id);
@Override
@UserDataPermission
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
@Override
@UserDataPermission
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
@Override
@UserDataPermission
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
@Override
@UserDataPermission
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
@Override
@UserDataPermission
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
@Override
@UserDataPermission
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
@Override
@UserDataPermission
<E extends IPage<T>> E selectPage(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
@Override
@UserDataPermission
<E extends IPage<Map<String, Object>>> E selectMapsPage(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
}
通过上述步骤,我们可以有效地使用 MyBatis Plus 拦截器实现数据权限控制。无论是基础版本还是进阶版,都能满足不同场景下的需求。希望本文能帮助你在实际项目中更好地管理和控制数据权限。
- 点赞
- 收藏
- 关注作者
评论(0)