带有火箭和柴油的多租户Web应用

问题描述

我有一个多租户Web应用程序,可能需要支持数十个租户(公司)。我一直在寻找一种方法,以确保租户只能访问自己的数据(重要的是没有泄漏),而不必将{ "backgroundColor": { "light": "#ffffff","dark": "#000000" } } 传递给每个表单和SQL查询。我的想法是创建一个可更新的视图,以便用户查询只能在其公司数据的范围内进行。

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

tenant_id

这将创建一个可更新的视图,该视图仅允许查询该公司的数据,而不必指定其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);

在Diesel中,我已经写好桌子了!宏的视图,以便Diesel将其视为表。在Rocket中,我将数据库连接Request Guard包装在另一个Request Guard中,该Request Guard首先发送一个SQL查询以将tenant_id设置为用户my.tenant_id

tenant_id

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

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

解决方法

我只是在查询函数中过滤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 请求防护内查询。