利用 MyBatis Plus 拦截器动态管理数据访问权限

利用 MyBatis Plus 拦截器动态管理数据访问权限

引言

功能权限与数据权限

在软件开发过程中,我们经常遇到需要根据用户角色来控制数据访问权限的需求。特别是在列表数据展示时,要确保用户只能查看其权限数据范围内的。本文将介绍一种通过MyBatis拦截器实现数据权限控制的方案,该方案灵活且易于集成到现有项目中。

数据权限分配

01

基础版本实现

1. 创建注解类

首先,我们需要创建一个自定义注解 @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 {

}

2. 创建拦截器

接下来,创建一个拦截器 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);

        }

    }

}

3. 创建处理类

创建一个处理类 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;

    }

}

4. 将拦截器加入 MyBatis-Plus 插件中

最后,将拦截器加入 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;

    }

}

功能权限与数据权限

02

进阶版实现

1. 建立范围枚举

定义一个范围枚举 DataScope,用于表示不同的数据权限范围。如下示例:

import lombok.AllArgsConstructor;

import lombok.Getter;

@AllArgsConstructor

@Getter

public enum DataScope {

    ALL("ALL"),

    DEPT("DEPT"),

    MYSELF("MYSELF");

    private String name;

}

2. 建立角色枚举

定义一个角色枚举 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;

    }

}

3. 重写拦截器处理方法

在 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);

}

03

结论

通过上述步骤,我们可以有效地使用 MyBatis Plus 拦截器实现数据权限控制。无论是基础版本还是进阶版,都能满足不同场景下的需求。希望本文能帮助你在实际项目中更好地管理和控制数据权限。

凯哥推荐

托尼学长,前美团高级技术经理,前新东方技术总监,曾就职于京东和去哪儿网,

我最近创建了一个技术陪伴群,每天分享我自己的一些独特的职场 & 技术观点,感兴趣的同学可以一起加入

也欢迎备注【面试题】,免费领取价值199元的Java高频场景面试题详解。

### MyBatis-Plus 拦截器实现数据权限控制 #### 使用自定义注解与拦截器相结合的方式 为了实现在MyBatis Plus中对查询结果的数据权限过滤,可以采用自定义注解配合拦截器的方式来完成。这种方式不仅能够简化开发流程,还能有效减少SQL解析的风险。 当开发者希望在不修改原有业务逻辑的前提下增加数据访问层的安全机制时,可以通过创建特定场景下的注解来标记需要应用此安全措施的服务接口或方法[^2]。例如: ```java @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface DataPermission { String value(); } ``` 接着,在配置类中注册一个实现了`Interceptor`接口的组件,并重写其内的`intercept()`函数用于实际处理逻辑。在此过程中,可以从上下文中获取由上述自定义注解传递过来的信息并据此调整最终发出给数据库引擎执行的SQL语句[^1]。 对于具体的应用实例来说,则是在目标Service层的方法上添加之前定义好的注解,指定相应的条件表达式或者其他必要的参数。之后每当触发带有该注解标注的操作时,系统便会自动调用对应的拦截器来进行额外校验工作,从而达到预期效果。 下面是一个简单的例子展示如何设置这样的环境以及编写相关代码片段: ```java // 定义数据权限注解 import java.lang.annotation.*; @Documented @Inherited @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @interface DataScope {} // 创建拦截器类 @Component @Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class})) class DynamicDataSourceInterceptor implements Interceptor { private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceInterceptor.class); @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms = (MappedStatement)invocation.getArgs()[0]; Object parameterObject = invocation.getArgs()[1]; Method currentMethod = ReflectUtil.getMethodByClass(ms.getId(), parameterObject.getClass()); if(null != currentMethod && currentMethod.isAnnotationPresent(DataScope.class)){ // 获取当前登录用户的部门id或其他信息 Long deptId = SecurityContextHolder.getContext().getAuthentication().getName(); BoundSql boundSql = ms.getBoundSql(parameterObject); String sql = boundSql.getSql(); // 动态拼接sql,这里只是简单示范,实际情况可能更复杂 StringBuilder newSqlBuilder = new StringBuilder(sql.length() + 100).append(sql); // 假设表中有dept_id字段用来存储所属部门编号 int index = newSqlBuilder.lastIndexOf("WHERE"); if(index >= 0){ newSqlBuilder.insert(index + 5," AND t.dept_id = #{deptId}"); }else{ newSqlBuilder.append(" WHERE t.dept_id = #{deptId}"); } // 更新boundSql对象中的原始sql字符串和其他属性... MetaObject metaObject = SystemMetaObject.forObject(boundSql); metaObject.setValue("sql",newSqlBuilder.toString()); // 将新的参数加入到原参数列表里以便后续使用 Map<String,Object> paramsMap = (Map<String,Object>)parameterObject; paramsMap.put("deptId",deptId); } return invocation.proceed(); } } // 应用至服务端点处 @Service public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements IUserService { @DataScope @Override public List<UserEntity> listUsers(){ return baseMapper.selectList(new QueryWrapper<>()); } } ``` 以上就是关于如何利用MyBatis Plus自带的拦截器特性结合自定义注解达成较为灵活高效的数据权限管理方案的一个基本介绍和实践案例说明。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凯哥Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值