spring-boot 整合 mybatis-plus 之数据范围(文末有彩蛋)

为什么要有数据范围?

首先,大家应该都听说过垂直越权和水平越权?什么没有听过?那么我大致讲下:

  • 垂直越权:发生在不同级别的用户之间,即低级用户尝试访问或操作高级用户的功能。例如,一个普通用户通过不当手段获取到管理员账号,然后利用该账号访问管理员才有权访问的敏感数据。
  • 水平越权:在同一级别用户之间发生,即用户A尝试访问或操作用户B的数据或功能,即使用户A的权限低于用户B。例如,一个普通用户试图访问另一个普通用户的数据。

通过上面描述相信大家已经了解什么是垂直权限和水平权限。
两者各有不同,但又有共同点。就是越权访问原本没有权限访问的数据。
本文主要讲解并提供数据范围权限的解决方案,希望能给大家带来一些帮助。

解决方案

核心思想就是拦截最终执行的 SQL 并重写,使其满足要求。示例通过整合 mybatis-plus 实现该功能。为方便演示,采用 H2
内存数据库,直接启动项目即可。

1.定义数据范围注解

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {

}

2.数据范围处理逻辑

@Slf4j
public abstract class DataScopeHandler {

    /**
     * 获取数据范围 SQL 片段
     *
     * @param plainSelect  查询对象
     * @param whereSegment 查询条件片段
     * @return JSqlParser 条件表达式
     */
    @SneakyThrows(Exception.class)
    public Expression getSqlSegment(PlainSelect plainSelect, String whereSegment) {
        // 待执行 sql where 条件表达式
        Expression where = plainSelect.getWhere();
        if (where == null) {
            where = new HexValue(" 1 = 1 ");
        }
        log.info("开始进行权限过滤,where:{},mappedStatementId:{}", where, whereSegment);
        // 获取 mapper 名称
        String className = whereSegment.substring(0, whereSegment.lastIndexOf("."));
        // 获取方法名
        String methodName = whereSegment.substring(whereSegment.lastIndexOf(".") + 1);
        // 获取当前 mapper 的方法
        Method[] methods = Class.forName(className).getMethods();
        // 遍历判断 mapper 的所有方法,判断方法上是否有 DataScope 注解
        for (Method m : methods) {
            if (Objects.equals(m.getName(), methodName)) {
                DataScope annotation = m.getAnnotation(DataScope.class);
                if (annotation == null) {
                    return where;
                }
                // 数据范围封装
                return this.setExpression(where, DataScopeUtil.getTableName(plainSelect));
            }
        }
        // 说明无权查看
        where = new HexValue(" 1 = 2 ");
        return where;
    }

    public abstract Expression setExpression(Expression where, String tableName);

}

示例中通过组织机构进行数据范围的拦截

public class DataPermissionHandler extends DataScopeHandler {

    @Resource
    private UserService userService;

    // 根据组织机构拦截数据
    private static final String SCOPE_FIELD = "org_id";
    // 根据操作用户拦截数据
    private static final String USER_FIELD = "create_by";

    @Override
    public Expression setExpression(Expression where, String tableName) {
        // 模拟获取当前登录用户信息
        LoginUser user = userService.getLoginUser();
        if (null != user) {
            DataScope dataScope = DataScope.valueOfId(user.getDatascope());
            return switch (dataScope) {
                case ALL -> DataScopeUtil.all(where);
                case CUS, CURRENT_SUB ->
                        DataScopeUtil.inExpression(where, tableName, SCOPE_FIELD, List.of(user.getRangeList()));
                case CURRENT ->
                        DataScopeUtil.equalsExpression(where, tableName, SCOPE_FIELD, List.of(user.getRangeList()));
                case MYSELF ->
                        DataScopeUtil.equalsExpression(where, tableName, USER_FIELD, List.of(user.getRangeList()));
            };
        }
        // 获取不到用户信息,无权查看任何数据
        return DataScopeUtil.noData(where);
    }

}

3.实现拦截器插件

@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class DataScopeInterceptor extends JsqlParserSupport implements InnerInterceptor {

    private DataScopeHandler dataScopeHandler;

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
                            RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        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) {
            List<SelectBody> selectBodyList = setOperationList.getSelects();
            selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
        }
    }

    private void setWhere(PlainSelect plainSelect, String whereSegment) {
        Expression sqlSegment = this.dataScopeHandler.getSqlSegment(plainSelect, whereSegment);
        if (null != sqlSegment) {
            plainSelect.setWhere(sqlSegment);
        }
    }

}

4.插件配置

@Configuration
public class MybatisConfiguration {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 数据范围插件
        interceptor.addInnerInterceptor(dataScopeInterceptor());
        // 防止入参错误导致全表更新或删除
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        // 分页插件
        interceptor.addInnerInterceptor(new PageInterceptor(DbType.H2));
        return interceptor;
    }

    @Bean
    public DataScopeInterceptor dataScopeInterceptor() {
        // 数据范围插件
        DataScopeInterceptor dataScopeInterceptor = new DataScopeInterceptor();
        dataScopeInterceptor.setDataScopeHandler(dataScopeHandler());
        return dataScopeInterceptor;
    }

    @Bean
    public DataScopeHandler dataScopeHandler() {
        // 数据范围处理器
        return new DataPermissionHandler();
    }

}

至此,基本功能已实现。我们可以通过对 mapper 层方法添加注解进行 SQL 的拦截重写。

@Mapper
public interface ArticleMapper extends BaseMapper<Article> {

    @DataScope
    List<Article> list();

}
@Mapper
public interface ArticleMapper extends DataScopeMapper<Article> {

}

大家可以像上面一样操作,继承 BaseMapper,针对自定义方法加上注解进行拦截,也可以继承 DataScopeMapper 默认方法全部进行拦截。

整合DEMO仓库地址

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ゞ註﹎錠oo

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

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

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

打赏作者

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

抵扣说明:

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

余额充值