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不会过滤的需求。