带有 Rocket 和 Diesel 的多租户 Web 应用程序

Posted

技术标签:

【中文标题】带有 Rocket 和 Diesel 的多租户 Web 应用程序【英文标题】:Multi-tenant web app with Rocket and Diesel 【发布时间】:2020-12-03 12:25:54 【问题描述】:

我有一个多租户网络应用程序,可能需要支持数十个租户(公司)。我一直在寻找一种方法来确保租户只能访问他们自己的数据(重要的是没有泄漏),而不必将tenant_id 传递给每个表单和 SQL 查询。我的想法是创建一个可更新的视图,以便用户的查询只能在他们公司的数据范围内操作。

我通过创建一个视图(postgres)来做到这一点:

CREATE VIEW products_tenant AS
SELECT *
FROM products
WHERE company_id = cast(current_setting('my.tenant_id') as int)
with local check option;
ALTER VIEW products_tenant ALTER COLUMN company_id SET DEFAULT cast(current_setting('my.tenant_id') as int);

这将创建一个可更新的视图,只允许查询该公司的数据,而无需指定其tenant_id

在 Diesel 中,我已经编写了表格!视图的宏,因此 Diesel 将它们视为表格。在 Rocket 中,我将我的数据库连接 Request Guard 包装在另一个 Request Guard 中,它首先发送 SQL 查询以将 my.tenant_id 设置为用户的 tenant_id

pub struct TenantView(DbConn);
...

impl<'a, 'r> FromRequest<'a, 'r> for TenantView 
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> Outcome<Self, ()> 
        let conn = request.guard::<DbConn>().unwrap();
        let company_id = request.guard::<User>().unwrap().get_company_id();

        let query = sql_query(format!("SET session my.tenant_id = ", company_id));
        query.execute(&*conn).expect("Failed to set session variable");
        Outcome::Success(Self(conn))
    

然后,用户可以使用请求保护进行数据库查询,并且只能访问其公司的数据。租户特定的可更新视图。

但我担心这可能会导致竞争状况。我不清楚 postgres 会话变量如何与 Diesel 和 Rocket 一起使用。假设来自两个不同公司的用户同时向 Rocket 提交请求,并且用户 A 的会话变量设置为他们的租户 ID,但在他们的交易之前,用户 B 将会话变量设置为他们的租户 ID,导致两个数据库请求都写入用户 B 的租户 ID。任何人都可以阐明这是否会成为问题?或者是否有更直接的方式来处理多租户应用?

【问题讨论】:

【参考方案1】:

我只会在查询函数中过滤 company_id。我知道这正是您不想要的,但我认为多一个参数不会使您的代码混乱。

use crate::schema::
    products::dsl::products as all_products,
    products,
;
...

#[derive(Queryable)]
#[table_name="products"]
pub struct Product 
    company_id: i32,
    ...


impl Product 
    pub fn all(user: &User, conn: &PgConnection) -> Option<Vec<Product>> 
        all_products.filter(products::company_id.eq(user.get_company_id())).load(conn).ok()
    

    ...


我猜您通过使用 用户 请求保护来限制对产品页面(或类似页面)的访问。然后你可以将你已经拥有的用户传递给函数。

#[derive(serde::Serialize)]
pub struct AppContext<'a, T> 
where T: 'a 
    body: &'a T,


#[get("/product/all", rank = 1)]
pub fn products(conn: DbConn, user: User) -> Template 
    let context: AppContext<'_, Option<Vec<Product>>> = AppContext  
        body: &Product::all(&user, &conn)  // <= get products
    ;
    
    Template::render("products", &context)


#[get("/product/all", rank = 2)]
pub fn products_redirect() -> Redirect 
    Redirect::to("/login")

此外,如果您的 FromRequest 实现从数据库中检索用户,您应该考虑使用request.local_cache。否则,您将在每个请求中至少查询给定用户两次,一次用于视图函数中的用户请求保护,另一次用于 TennantView 请求保护。

【讨论】:

以上是关于带有 Rocket 和 Diesel 的多租户 Web 应用程序的主要内容,如果未能解决你的问题,请参考以下文章

返回使用 Rocket 和 Diesel (Rust) 在 PostgreSQL 中创建的单个记录

如何在生产中使用 Rocket 运行 Diesel 迁移?

带有 sequelize 和 nest.js 的多租户

Diesel 获取单亲和许多孩子 - 火箭

使用 Diesel 从 mySQL 数据库中检索日期时间

在使用 Oauth、SAML 和 spring-security 的多租户的情况下从 spring-security.xml 中获取错误