不同用户间如何实现隔离,用户会话如何区分?

1 个回答

  • 周公旦
    希望有人告诉你开心比对错更重要。
    ## 作用 在多租户 SaaS 系统中,不同用户或租户之间需要在数据库、会话、缓存以及异步任务中隔离数据,防止一个租户访问或污染另一个租户的数据。 ## 原理 ### 1. 租户识别 - 请求进入后,先完成身份认证,再识别租户。 - 可通过以下方式获取租户标识: - 子域名或 `Host` 请求头; - JWT 中的 `tenant_id`; - 签名或加密后的 Session; - 其他可信请求头。 - JWT 验证应早于租户路由和业务处理,避免伪造请求绕过租户限制。 ### 2. 数据库隔离 可选隔离方式包括: - **独立数据库实例**:隔离性最强,每个租户使用自己的数据库。 - **独立 Schema**:如 PostgreSQL 可使用 `schema_translate_map`,比独立数据库更轻量。 - **共享表加 `tenant_id`**:成本较低,但所有查询都必须正确过滤,漏加条件就可能造成越权。 在 Flask-SQLAlchemy 中,可在请求阶段根据租户动态选择数据库连接,并用字典缓存对应的 SQLAlchemy Engine,避免每次请求都新建连接。 需要注意: - 不建议依赖静态的 `SQLALCHEMY_BINDS` 来实现请求级动态切换。 - 模型查询必须绑定到当前租户对应的 Engine、Schema,或强制带出租户条件。 - SQLite 不支持 PostgreSQL 那种 Schema 隔离;MySQL 中 Schema 与 Database 的语义也需要结合版本确认。 ### 3. 租户上下文传递 - Flask 的 `g` 对象可保存当前请求的 `tenant_id`,但只在当前请求生命周期内有效。 - 在协程或异步场景下,应使用 `contextvars.ContextVar` 保存租户上下文。 - 不建议在模型内部直接依赖全局 `g.tenant_id`,更适合通过显式参数或依赖注入传递,便于测试和保证安全。 ### 4. 用户会话区分 会话必须同时关联“用户”和“租户”: - 登录成功后,将 `tenant_id` 写入 Session 或加密 Token。 - Session 内容必须由服务端签名或加密,不能信任客户端可篡改的普通字段。 - 加载用户时,同时按 `user_id` 和 `tenant_id` 查询,校验用户是否属于当前租户。 - “记住我”等持久登录 Token 也必须包含并校验租户标识。 基本逻辑是: ```python @login_manager.user_loader def load_user(user_id): tenant_id = session.get('tenant_id') return User.query.filter_by( id=user_id, tenant_id=tenant_id ).first() ``` 因此,A 租户的登录态不能被 B 租户的请求复用。 ### 5. 缓存隔离 共享 Redis、Memcached 时,缓存键也必须带出租户维度: - 最稳妥的方式是不同租户使用独立缓存实例或不同连接。 - 次选方式是统一给缓存 Key 增加租户前缀,例如 `tenant_id:user:123`。 - 模板片段缓存等默认没有租户意识的缓存机制,也需要改造或禁用。 否则不同租户中相同的 `user:123` 可能发生缓存串读。 ### 6. 异步任务隔离 Celery、定时任务、`asyncio` 等后台执行流程通常没有原始请求上下文,因此: - 创建任务时必须显式传入 `tenant_id`; - 执行任务前重新校验租户身份和权限; - 不能假设 `g`、Session 或请求上下文仍然存在。 综上,用户会话通过认证后的用户标识与租户标识共同区分;租户隔离则必须贯穿数据库连接、查询条件、Session、Token、缓存、日志和异步任务,而不是只在业务表中增加一个 `tenant_id` 字段。
    发布于 2026-10-26 12:33 •IP属地 湖南
    举报
    评论
    收藏