问题描述
我有一个多租户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 请求防护内查询。