基类使用私有数据_使用Hibernate filter实现微服务的租户隔离(Tenant Isolation)

在之前的文章(基于RLS的微服务租户隔离(Tenant Isolation)解决方案)中我们介绍了当微服务使用One Schema(或者称做 Shared Schema)的方法来存储多租户数据的时候, 如何使用数据库自带的RLSRow Level Security)的功能来实现租户隔离。这个方案是笔者比较认同的方案, 它容易实现,对业务开发的程序员完全透明, 如果你用的数据库恰好有这个功能, 那么就用它吧。 但是现实世界总是没有那么简单, 并不是所有的数据库都支持RLS功能, 尤其是使用的很广泛的MySQL,到目前为止还没有支持它。所以如果你的微服务是用MySQL, 我们就要寻求另外的解决方案。

下面要介绍的解决方案适用于使用了Spring data jpa + hibernate + mysql(或者其他不支持RLS的数据库)的微服务。

我们都知道,Hibernate有一个特性叫做filter。如果 filter定义在指定的Entity上面, 那么当使用jpa从Entity来读取数据的时候, filter会自动的添加到生成的sql语句中,filter效果类似于给这个Entity所对应的表建立了一个View, 用户可以在运行时动态的设置filter参数值达到过滤数据的目的。

我们可以如下所示, 为微服务中定义一个抽象基类,这个抽象基类包含一个TENANT_ID的列, 然后在定义个一个filter, 这个filter的条件就是令TENANT_ID列的值等于一个动态的参数“current_tenant”。

@FilterDef(name = "tenant_filter", parameters = @ParamDef(name = "current_tenant", type = "string"))
@Filter(name = "tenant_filter", condition = "TENANT_ID = :current_tenant")
@MappedSuperclass
public abstract class AbstractBusinessObject {

    @Id
    @Column(name="ID")
    protected UUID id;

    @Column(name="TENANT_ID")
    protected String tenantID;

   //其余字段省略
}

然后可以令应用程序中所有的entity都继承自这个抽象基类。假设我们有一个叫做产品(product)的表,可以定义如下:

@Entity
@Table(name="PRODUCT")
public class Product extends AbstractBusinessObject {

    @Column(name="NAME", updatable = false, nullable = false)
    private String name;

    @Column(name="DESCRIPTION", updatable = true, nullable = true)
    private String description;

    //其余代码省略
}

如果你的微服务应用是一个标准的springboot的web application, 为了访问Product表,通常还要定义相应的ProductRepository(数据访问层), ProductService(业务层)ProductController(web层) 来组成一个三层结构。 因为这不是本文的重点, 我就略过不在描述, 熟悉spring boot的同学应该理解起来没有什么问题。

那么重点来了, 我们如何去动态的设置filter的参数“current_tenant”. 答案是使用aop,我们可以在每一个需要访问数据库的方法被调用之前, 利用aop去读取登陆时用户所属的租户信息, 动态的设置到参数current_tenant中, 代码如下所示:

@Aspect
@Component
public class TenantFilterAspect {
  
    @Autowired
    EntityManager entityManager;

    @Autowired
    TenantIDProvider tenantIDProvider;

    @Before("execution(* com.bruce.ProductSerice.*(..))")
    public void beforeQueryExecution(JoinPoint pjp) throws Throwable {
        org.hibernate.Filter filter = entityManager.unwrap(Session.class).enableFilter("tenant_filter");
        filter.setParameter("current_tenant", tenantIDProvider.getCurrentTenantID());
        filter.validate();
    }
}

以上的代码表示, 如果在spring boot的应用程序调用到ProductService之前, 需要先调用到beforeQueryExecution, 在beforeQueryExecution方法中我们通过tenantIDProvider读取当前客户所对应的租户的信息然后设置到filter的参数中。

这样一个基于hibernate filter的 租户隔离的解决方案就大致ready了。如果你的ProductController提供了对于Product名字(name)或者描述(description)的查询, 这个时候用户就只能查询到自己所对应租户里面名字或者描述和查询条件一致的产品。

那么和之前我们所提到RLS方案相比,它有什么优缺点呢:

优点:当然是使用所有的数据库。 对于使用mysql数据库的程序员来说, 是一个可以考虑的方案。

缺点

1) Hibernate filter 只适用于Query,不适用于direct fetching。

Filters apply to entity queries, but not to direct fetching.

(https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#mapping-column-filter)。 这意味着如果你如果意外的知道了另外一家公司的Product的ID号, 然后ProductController又恰好有一个根据id号进行查询的API,那么光靠这个方案还不能阻止你读取到这个id的Product的数据, 你还要在加上额外的检查逻辑在这个API中。

2)因为用到了AOP, 所以开发人员必须清晰定义那些方法执行之前需要去动态设置这个filter参数的值。这样对业务开发人员就提出了比较高的要求, 他必须在实现业务代码同时还要更新上面TenantFilterAspect的适用范围。

正是因为这两个缺点,所以这个基于hibernate filter方案对业务程序员来讲并不是完全透明, 所以并不算是一个很完美的解决方案。

参考

  1. Support for discriminator-based multi-tenancy https://hibernate.atlassian.net/browse/HHH-6054
  2. https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#mapping-column-filter
  3. https://www.citusdata.com/blog/2018/02/13/using-hibernate-and-spring-to-build-multitenant-java-apps/

你可能感兴趣的:(基类使用私有数据)