识别&切换租户

在SaaS应用程序中,识别租户非常重要,是个必须的操作。

本文分为4类场景分别介绍:

  • 人工触发操作
    • 普通用户的页面操作(HttpAction)
    • 后台管理员的页面操作(HttpAction)
  • 消息处理(MessageHandler)
  • 周期性后台任务(BackgroundTask)



普通用户操作

这是最常见的场景:用户"张三"登录到租户A,那么他的后续操作都是基于租户A

实现过程

  • 登录时,根据登录表单提交的tenantId,创建 WebUserInfo 时指定,例如:
WebUserInfo userinfo = new WebUserInfo{
    TenantId = tenantId // 登录表单提交的 tenantId
    UserId = "U0001",
    UserName = "张三",
    UserRole = "NormalUser"
}
AuthenticationManager.Login(userInfo, seconds);
  • Action授权要求
[Authorize(Roles = "NormalUser")]
// 或者
[Authorize(Roles = "NormalUser,Admin")]
  • Action中获取租户ID
string tenantId = this.GetTenantId();
// 或者
string tenantId = this.GetUserInfo().TenantId;

从示例代码中应该可以看出:租户信息和用户身份信息是绑定在一起的

注意:

  • Nebula的这个设计与其它框架有很大的差异
  • 其它框架提供了多种租户解析器(CurrentUser/QueryString/Form/Route/Header/Cookie/Domain),还允许自定义租户解析器

Nebula这样设计的原因是:为了安全!

因为:

  • 此类应用程序对外公开,任何用户都可能访问到
  • 访问用户几乎是外部用户,任何已登录用户都可能发起安全攻击
  • HTTP请求的数据是可以伪造的!
  • 只有JWT-token的数据是防伪造的。




后台管理员操作

在SaaS应用程序中,租户管理操作通常以一个后台站点形式存在,这个站点有以下特点:

  • 有独立的域名,不对外公开
  • 只允许 后台管理员 登录和访问
  • 可以处理任何租户
    • 后台管理员的身份不同某个租户绑定
    • 每次操作的租户是明确的

实现过程

  • 登录时,从master库校验管理员帐号是否正确,创建 WebUserInfo 时指定,例如:
WebUserInfo userinfo = new WebUserInfo{
    TenantId = Consts.ZeroTenantId,  // 一个特殊的租户ID,不发挥作用
    UserId = "S0001",
    UserName = "李四",
    UserRole = "SaasAdmin"
}
  • Action授权要求
[Authorize(Roles = "SaasAdmin")]
  • Action中获取租户ID

此时租户ID从 提交数据 中获取,根据实际场景来定义数据结构即可。

说明:

  • 后台管理员与普通用户相比,身份不绑定租户,这是由于业务的需要而决定的
  • 理论上可以使用后台管理员的身份来发起伪造的HTTP请求,然而:
    • 没有必要,因为后台管理员本身就拥有这种权限
    • 后台管理站点的操作有限,管理员能执行的操作也是有限的
    • 前台站点可以不允许后台管理员登录,授权检查也能阻止后台管理员访问




消息处理

这种场景下,由于用户没法直接参入,因此可认为消息数据是无法伪造的。

所以,租户ID(TenantId)可以直接定义在消息结构中,例如:

public class XxxMessage1 {
  public string TenantId {get; set;}
  // 其它的消息成员
}

通常来说,消息是在 HttpAction 中创建并发送到消息队列的,只要保证HttpAction按照本文前面介绍的方法来实现,消息数据就是安全的(不会被伪造)




周期性后台任务

使用这种场景的租户操作,大致有2类:

  • 一次性处理所有租户(维护类任务)
    • 读取租户总表,然后循环处理每个租户
  • 根据配置处理特定的租户
    • 读取配置参数值,拆分成数组,然后循环处理每个租户

因此,这类场景并不存在安全伪造问题,这里就不再细述。




切换租户

按照 多租户架构 的介绍,各个租户的数据是隔离的,因此切换租户也就是切换到对应的租户数据库。

Nebula提供以下方法可供使用

// Controller
DbContext dbContext = this.CreateTenantConnection();

// MessageHandler or BackgroudTask
DbContext dbContext = DbConnManager.CreateTenant(tenantId);