脚本宝典收集整理的这篇文章主要介绍了基于RLS的微服务租户隔离(Tenant Isolation)解决方案,脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。
现在的软件设计中,微服务的解决方案越来越流行, 每个微服务负责一个特定的业务,大量的微服务相互协作通讯组成了一个完整的系统。 每个微服务通常都有自己管理的业务数据和对应的数据库。 通常每个公司(或者称租户)的数据在数据库中的存储有两种设计的方案:
2. 所有的租户共享一个Schema。每个租户的数据都存储在同一张表中, 然后每一张业务表都有一列(通常这一列叫做 TenantID)来表示当前的行数据是属于那个租户。
第一个方案的好处, 就是各个租户之间的数据是完全的隔离, 一个公司的用户只能访问自己数据库里面的数据, 实现的原理通常是根据当前的用户所在的公司, 获取对应的数据库的Connection,该Connection只能对自己公司的 Schema进行操作。缺点是数据库的资源会消耗的比较多, 而且不易统一的升级管理, 需要对数据库逐一的进行升级,投入的维护人力会比较大。 通常这种解决方案适合数据量大, 对数据安全要求高的大公司。
第二种方案的优势和第一种方案的缺点相对, 就是数据库是统一的升级和维护, 如果大量的租户是数据量不大的小公司, 那么把它们的数据放在同一张表里面无疑是非常合适的。 缺点也很明显, 那就是使用同一个数据库连接,如果不在应用层做额外的处理, 理论上登陆用户能访问到不同租户的数据。
第一种方案是传统的租户数据的存储方案, 在微服务流行之前, 广泛的存在于各个面向企业的应用程序软件中。 第二种方案, 则随着微服务的流行, 变得越来越普遍, 因为每个微服务通常是由一个小团队来负责, 他们通常都没有足够的人力来为每一个客户公司升级维护一套 Schema。
问题来了, 如果是用 Share Schema这样的多租户数据存储方案, 怎么如何来保证数据访问的安全性?
一种方式就是在应用层来做数据访问的控制。 在业界流行的基于SPRing-Boot的微服务的设计中, 通常使用两种数据库访问框架, 一个是 Mybatis, 这个在国内互联网公司比较的流行。另外一个是JPA, 这个在国外用的比较多。 JPA作为一个标准它有两个比较流行的实现一个是 hibernate, 一个是 EclipseLink。
可惜的是, Mybatis自己并没有针对Share Schema的多租户数据库在框架级别提供解决方案。 也就是说, 每个开发人员必须自己保证自己所写的SQL语句包含了对当前公司Tenant ID的过滤, 这对开发人员的要求就相应的比较高。 当然也有一些开源项目, 在 mybatis的基础之上, 增加了对 Share Schema的支持, 比如说 mybatis-plus(https://mp.baomidou.COM/guide/tenant.htML), 但是似乎他自身的限制还比较多。
JPA的标准包含了对 Share Schema的数据库表的支持, 它把存储TenantID的列定义为Tenant Discriminator。 JPA的实现需要将这个 Tenant Discriminator 放在最终生成的SQL语句中, 这样保证不同的用户, 使用相同的jqL, 访问的只能是自己公司的数据。 可惜的是, 只有EclipseLink目前对这个规范有比较好的支持, 流行最广的Hibernate要到7.0版本之后才能对此有支持(https://hibernate.atlassian.net/browse/HHH-6054)。
这样一来, 似乎在应用程序的数据库访问层来做租户数据隔离变成了一个不易完成的任务。
幸运的是, 很多的数据库(可惜除了 MySQL), 提供了一种新的数据访问控制, 就是本文要介绍的 RLS(Row Level SecurITy)。 基于RLS, 微服务能够轻易的就实现租户的数据隔离。
以下以PostgreSQL为例,我们假设在Public schema下面有一张商品表,为了简化, 这张表现在只有ID,NamE, TENANT_ID, 创建时间和更新时间这些列。
-- create table
CREATE TABLE IF NOT EXISTS PRODUCT(
ID UUID Primary KEY,
NAME VArchAR(256) unique NOT NULL,
TENANT_ID VARCHAR(256) NOT NULL,
CREATED_DATETIME TIMESTAMP NOT NULL,
UPDATED_DATETIME TIMESTAMP NOT NULL
);
然后我们打开这张表的 RLS功能:
-- ALTER TABLE to enable row security
ALTER TABLE PRODUCT ENABLE ROW LEVEL SECURITY;
创建一个对 Product表进行访问控制的Product_policy, Product_Policy限制了只有tenant_id列的数据和当前的session变量myapp.current_tenant的值相同的那些行才能被访问。
-- create policy
CREATE POLICY product_policy ON product
FOR ALL
USING (current_setting('myapp.current_tenant') = tenant_id)
当然我们还要创建一个普通用户dev, 并授予他访问这张表的权利:
-- create normal user;
CREATE USER dev NOSUPERUSER PASSWORD 'dev';
-- grant privileges
GRANT ALL ON SCHEMA PUBLIC TO dev;
GRANT ALL ON TABLE PRODUCT TO dev;
这样数据库的RLS的功能已经被打开并且准备就绪了,接下来就是应用程序里面要做的事情。
众所周知,在SpringBoot中, 如果需要访问一个数据库, 那么需要在应用程序的配置文件中配好数据库的连接信息, 然后spring boot会自动的给我们配置一个DataSource 的实例, 并从这个实例中来获取到数据库的连接。
#application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/testdb
spring.datasource.username=dev
spring.datasource.password=dev
之前我们在定义 product_policy的时候, 我们看到了我们引用了一个myapp.current_tenant的session变量。 那么这个变量是什么时候被定义的呢? 答案是每次启动事务的时候 。 因为无论Spring事务管理的@Transactional注解或者是 Transactional Template, 它们都会调用DataSource实例的getConnection方法,所以我们可以定制一个DataSource, 这个DataSource是一个实际被使用的Datasource的代理, 只是它覆盖了getConnection方法, 在这个方法返回connection之前将myapp.current_tenant赋予了当前登陆用户所属的tenant的值。
// MultiTenantDataSource.java
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.Preparedstatement;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.LOGging.Logger;
public class MultiTenantDataSource implements DataSource {
private static final LoggingUtil logger = LoggingUtilFactory.getLogger(MultiTenantDataSource.class);
private final DataSource delegate;
private final TenantIDProvider tenantIDProvider;
public MultiTenantDataSource(DataSource delegate, TenantIDProvider tenantIDProvider) {
this.delegate = delegate;
this.tenantIDProvider = tenantIDProvider;
}
@override
public Connection getConnection() throws SQLException {
Connection connection = delegate.getConnection();
enableTenantIsolation(connection);
return connection;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
Connection connection = delegate.getConnection(username, password);
enableTenantIsolation(connection);
return connection;
}
@Override
public <T> T