识别&切换租户
在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);