pingssys-springboot+dubbo+mybatisplus数据过滤

1.问题

  • 本人在为pingss-sys脚手架(项目地址)编写WMS(仓库管理系统)时,遇到如下需求:
    • 一个库有很多仓库,每个仓库也有很多客户。当用户选择某个仓库登录后,就只能操作这个仓库的数据,而某个客户所属的用户登录后,也只能操作这个客户的数据,这就需要在操作数据时对数据进行过滤(前提是所有的业务表都有相同的仓库编码和客户编码两个字段),当然也有部分基础数据又是不能过滤的;
  • 思路
    • 最简单的方法当然就是每个查询都手动添加过滤条件,但是会很麻烦,并且后续如果需求有变化,修改也很麻烦,最重要的是很low;
    • 数据过滤的思路是使用mybatis拦截器,拦截到sql,并添加过滤条件"warehouse_code = ‘xxx’ and customer_code = ‘xxx’";
      • 比较简单并且成熟的方案是实现是mybaitsplus的ISqlParser接口。最好是直接继承自TenantSqlParser,实现TenantHandler接口
    • dubbo provider是没有状态的,不知道当前请求的用户是哪个仓库的哪个客户,所以需要consumer传送过来,比较方便的方法是通过隐式参数传送过来;

2.实现过程

2.1.服务端mybatis拦截器

自定义一个mybatis拦截器实现数据隔离效果有比较麻烦,且容易因为考虑不完善导致出现各种问题。使用mybatisplus的ISqlParser扩展则简单得多;

  • 首先定义ISqlParser,直接继承自TenantSqlParser;
public class WmsTenantSqlParser extends TenantSqlParser {
    
    //**当插入语句中已经存在数据隔离的字段时,则不在重新生成该字段
    @Override
    public void processInsert(Insert insert) {
        String columnName = this.getTenantHandler().getTenantIdColumn();
        if(insert.getColumns().stream().noneMatch(column -> columnName.equalsIgnoreCase(column.getColumnName()))) {
            super.processInsert(insert);
        }
    }
}
  • 然后实现TenantHandler接口,定义隔离的字段和取值;
//**隔离客户
public class FilterCustomerUtil {

    //**忽略所属客户字段的表名
    private static final List<String> ignoreTable = new ArrayList<>();
    static {
        ignoreTable.add("wms_settlement_type");
    }

    public static TenantSqlParser getSqlParse() {
        TenantSqlParser rst = new WmsTenantSqlParser();
    
        rst.setTenantHandler(new TenantHandler() {
            //**获取当前隔离字段的值
            @Override
            public Expression getTenantId(boolean where) {
                return new StringValue(DubboAttachment.CUSTOMER_CODE.getValue());
            }
            
            //**获取隔离字段的和数据库字段名
            @Override
            public String getTenantIdColumn() {
                return "customer_code";
            }
            
            //**是否需要隔离
            @Override
            public boolean doTableFilter(String tableName) {
                boolean ignore = ignoreTable.contains(tableName);
                String filterCustomer = DubboAttachment.FILTER_CUSTOMER.getValue();
                return ignore || filterCustomer == null || filterCustomer.equals("false");
            }
        });
        
        return rst;
    }
}
  • 最后把自定义的ISqlParser添加到PaginationInterceptor对象的sqlParserList中(一定要添加到PaginationInterceptor的sqlParserList才会生效,添加到自定义的Interceptor无法生效);
/**分页配置*/
@Bean
public PaginationInterceptor paginationInterceptor() {
    PaginationInterceptor interceptor = new PaginationInterceptor();

    List<ISqlParser> sqlParserList = new ArrayList<>();
    //*过滤客户数据
    sqlParserList.add(FilterCustomerUtil.getSqlParse());
    interceptor.setSqlParserList(sqlParserList);

    return interceptor;
}

2.2.客户端传送隐式参数

2.2.1.定义注解

存在有些接口需要过滤,有些接口不需要过滤的情况,就需要有开关可以进行控制。本人想到的是使用注解,通过注解参数进行控制;

//**数据过滤器,过滤仓库/客户的数据
public @interface DataFilter {
    //**是否过滤客户数据
    boolean filterCustomer() default true;
    //**是否过滤仓库数据
    boolean filterWarehouse() default true;
}

在接口/方法上配置注解;

@DataFilter
public interface CustomerService extends BaseService<Customer> {

    //**查询所有客户的树形结构
    @DataFilter(filterCustomer = false, filterWarehouse = false)
    List<Customer> findTreeAll();

    //**根据编码获取客户
    Customer getByCode(String code);
}

2.2.2.使用spring aop

使用spring aop,拦截dubbo service调用,添加隐式参数;

  • 由于spring aop无法拦截客户端接口上配置的注解(原因是接口中的注解无法被实现类继承)
    而本人在consumer中使用注解引入provider接口,所以aop的方式无法生效;

2.2.3.使用dubbo filter

  • 使用dubbo过滤器,拦截dubbo service调用,添加隐式参数;
//**调用dubbo接口时传入隐式参数,配合mybatis拦截器过滤仓库和客户的数据
public class DubboDataFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        try {
            //**接口type的过滤器
            DataFilter classDataFilter = invoker.getInterface().getAnnotation(DataFilter.class);
            boolean filterCustomer = classDataFilter != null && classDataFilter.filterCustomer();
            boolean filterWarehouse = classDataFilter != null && classDataFilter.filterWarehouse();

            //**接口method的过滤器
            Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
            DataFilter methodDataFilter = method.getAnnotation(DataFilter.class);
            if(methodDataFilter != null){
                filterCustomer = methodDataFilter.filterCustomer();
                filterWarehouse = methodDataFilter.filterWarehouse();
            }

            //**当前仓库和客户
            RpcContext.getContext().setAttachment(Constants.DubboAttachment.CUSTOMER_CODE.toString(), JwtClaims.CUSTOMER_CODE.getValue());
            RpcContext.getContext().setAttachment(Constants.DubboAttachment.WAREHOUSE_CODE.toString(), JwtClaims.WAREHOUSE_CODE.getValue());

            //**过滤仓库数据
            RpcContext.getContext().setAttachment(Constants.DubboAttachment.FILTER_WAREHOUSE.toString(), (hasWarehouse() && filterWarehouse) + "");

            //**过滤客户数据
            if(hasCustomer() && filterCustomer && StringUtils.isNotBlank(JwtClaims.CUSTOMER_CODE.getValue())){
                //**如果用户属于某个客户则过滤客户数据
                RpcContext.getContext().setAttachment(Constants.DubboAttachment.FILTER_CUSTOMER.toString(), "true");
            } else {
                //**如果用户没有指定客户,则不过滤客户数据
                RpcContext.getContext().setAttachment(Constants.DubboAttachment.FILTER_CUSTOMER.toString(), "false");
            }
        } catch (NoSuchMethodException e) {
            throw new WmsException(e);
        }

        return invoker.invoke(invocation);
    }

    //**是否过滤客户数据(预留接口,方便后续扩展)
    private boolean hasCustomer(){
        return true;
    }

    //**是否过滤仓库数据(预留接口,方便后续扩展)
    private boolean hasWarehouse(){
        return true;
    }
}
  • 在引用provider时添加过滤器:
@Reference(version = "${wms.service.version}", filter = "dataFilter")
private CustomerService customerService;
  • 配合注解实现了调用CustomerService的方法时自动过滤仓库和客户数据,但findTreeAll不会过滤的需求。
    在这里插入图片描述
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值