带有 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 中创建的单个记录
在使用 Oauth、SAML 和 spring-security 的多租户的情况下从 spring-security.xml 中获取错误