首先簡單說下多租戶的幾種實現方式
多租戶(Multi-Tenant ),即多個租戶共用一個實例,租戶的數據既有隔離又有共享,說到底是要解決數據存儲的問題。
常用的數據存儲方式有三種。
方案一:獨立數據庫
一個Tenant,一個Database的數據存儲方式。隔離級別最高、最安全,但成本也高。
優點:a.為不同租戶提供獨立數據庫,有助於簡化數據模型的擴展設計,滿足個性化需求;
b.數據恢復簡單;
缺點:增大了數據庫的安裝數量,購置和維護成本高;
方案二:共享數據庫,隔離數據架構
多個租戶或所有租戶共享Database,但一個Tenant,一個Schema的方式。
優點:a.一定程度的邏輯數據隔離(並非完全),可滿足較高程度的安全性保障;
b.每個數據庫,可支持更多租戶數量;
缺點:a.恢復數據較困難,因為將牽扯到其他租戶數據;
b.跨租戶統計數據,實現難度大;
方案三:共享數據庫,共享數據架構
一種租戶共享同一個Database、同一個Schema,而另行通過TenantID區分租戶數據的方式。
優點:a.每個數據庫可支持租戶數量多,維護和購置成本低;
缺點:a. 隔離級別低,安全性低,開發時需做大量安全開發工作;
b. 逐表逐條備份和還原數據,數據備份和恢復困難。
今天主要講的就是用WTM 改造簡易的多租戶,我這里用的是Layui版本,其他UI也可以用這種方式實現,我還沒有試過,大家有空可以自己試一試。我用的是方案一 獨立數據庫方式。技術有限,只是希望在這里可以給大家提供一個思路。
開始說下整體步驟
咱們先來創建一個租戶表,我這里簡單創建幾個字段,為了演示,大家根據實際需要自己調整。我這里為了演示方便租戶角色直接用系統自帶的角色表了,大家自己可以增加一個租戶角色表。
public class Tenant : BasePoco { [Display(Name = "編號")] [Required(ErrorMessage = "{0}是必填項")] public string Code { get; set; } [Display(Name = "域名")] [Required(ErrorMessage = "{0}是必填項")] public string DomainName { get; set; } [Display(Name = "租戶角色")] [Required(ErrorMessage = "{0}是必填項")] public Guid RoleId { get; set; } [Display(Name = "租戶角色")] public FrameworkRole Role { get; set; } [Display(Name = "賬號")] [Required(ErrorMessage = "{0}是必填項")] public string Account { get; set; } [Display(Name = "名稱")] [Required(ErrorMessage = "{0}是必填項")] public string Name { get; set; } }
添加完租戶信息后,Create方法里需要創建好 這個租戶的庫、基本信息和提供的域名。我這里租戶的庫生成規則直接就是默認用主庫名+編號生成的。
[HttpPost] [ActionDescription("Sys.Create")] public ActionResult Create(TenantVM vm) { using (var trans = DC.BeginTransaction()) { if (!ModelState.IsValid) { return PartialView(vm); } else { vm.DoAdd(); if (!ModelState.IsValid) { vm.DoReInit(); return PartialView(vm); } else { //我這代碼直接寫這了 生成租戶庫和基本信息 隨便寫了下簡單的系統表數據 var NDC = new DataContext(Wtm.ConfigInfo.Connections[0].Value.Replace("SAASDEMODB", "SAASDEMODB" + vm.Entity.Code), DBTypeEnum.SqlServer); var Result = NDC.Database.EnsureCreated(); if (Result) { var role = DC.Set<FrameworkRole>().Where(x => x.ID == vm.Entity.RoleId).FirstOrDefault(); //角色擁有的菜單權限 var pr = DC.Set<FunctionPrivilege>().Where(x => x.RoleCode == role.RoleCode).ToList(); var user = new FrameworkUser { ITCode = vm.Entity.Account, Password = Utils.GetMD5String("000000"), IsValid = true, Name = vm.Entity.Name }; var userrole = new FrameworkUserRole { UserCode = vm.Entity.Account, RoleCode = role.RoleCode }; NDC.Set<FrameworkUser>().Add(user); NDC.Set<FrameworkRole>().Add(role); NDC.Set<FrameworkUserRole>().Add(userrole); //這里框架自帶角色表 頁面權限FunctionPrivilege表沒有加父級菜單數據 會導致約束沖突。后期自己添加租戶角色表吧 NDC.Set<FrameworkMenu>().AddRange(DC.Set<FrameworkMenu>().CheckContain(pr.Select(x => x.MenuItemId).ToList(), x => x.ID).ToList()); NDC.Set<FunctionPrivilege>().AddRange(pr); NDC.SaveChanges(); //雲解析DNS-添加解析記錄 if (!new CommonHelp().DomainNameResolution(vm.Entity.Code)) { trans.Rollback(); return FFResult().CloseDialog().RefreshGrid().Alert("域名解析失敗!"); } } else { trans.Rollback(); return FFResult().CloseDialog().RefreshGrid().Alert("租戶信息初始化失敗!"); } trans.Commit(); return FFResult().CloseDialog().RefreshGrid(); } } } }
主要用到了一個雲解析DNS-添加解析記錄的方法。調用AddDomainRecord根據傳入參數添加解析記錄。
雲解析 DNS(Domain Name System,簡稱DNS) 是一種安全、快速、穩定、可靠的權威DNS解析管理服務。 它能夠幫助企業和開發者將易於管理識別的域名轉換為計算機用於互連通信的數字IP地址,從而將用戶的訪問路由到相應的網站或應用服務器。
具體看文檔 添加解析記錄 (aliyun.com)
#region 雲解析DNS-添加解析記錄 可以去阿里雲地址看文檔 https://help.aliyun.com/document_detail/29772.html 但是這種方式服務器要求 80端口只允許部署這一套系統,因為現在這種方式是域名直接指向服務器IP public bool DomainNameResolution(string Name) { IClientProfile profile = DefaultProfile.GetProfile("cn-hangzhou", "", "");//域名 AccessKeyID Secret DefaultAcsClient client = new DefaultAcsClient(profile); var request = new AddDomainRecordRequest(); request._Value = ""; //指向服務器IP request.Type = "A"; request.RR = Name; //隨便定義 request.DomainName = "xxx.com"; //域名 try { var response = client.GetAcsResponse(request); return true; //Console.WriteLine(System.Text.Encoding.Default.GetString(response.HttpResponse.Content)); } catch (ServerException e) { return false; } catch (ClientException e) { return false; } } #endregion
這種方式不好的一點是 服務器要求 80端口只允許部署這一套系統,因為現在這種方式是域名直接指向服務器IP。我是部署在IIS上,需要注意的一點是應用中不要綁定主機名。(如果大家有更好的辦法可以一起溝通溝通)
到這里創建的這個租戶的庫和基本信息和域名就創建好了。
這個時候所有域名都可以訪問到部署的系統了,但是appsettings.json文件中Connections只有一個默認的庫,當然不可能添加一個租戶就在這加一個連接字符串,不現實。
正好框架支持動態選擇連接字符串。框架可以根據頁面傳遞過來的數據,或者session里的信息等動態選擇需要連接的數據庫,只需編輯Startup文件中的CSSelector方法。
訪問系統肯定會先讀主庫,我這里是根據域名去租戶表里查,如果存在就動態添加一個ConnectionStrings,利用Wtm.Session.Set("TenantKey", CS.key);,否則就正常訪問主庫。
#region 獲取當前url public string GetAbsoluteUri(HttpRequest request) { return new StringBuilder() .Append(request.Scheme) .Append("://") .Append(request.Host) .Append(request.PathBase) .Append(request.Path) .Append(request.QueryString) .ToString(); } #endregion [Public] [ActionDescription("Login")] public IActionResult Login() { LoginVM vm = Wtm.CreateVM<LoginVM>(); string TenantKey = "default"; string displayUrl = GetAbsoluteUri(HttpContext.Request); displayUrl = displayUrl.Replace("http://", "").Replace("https://", "") + "/"; displayUrl = displayUrl.Substring(0, displayUrl.IndexOf("/")); var ZDC = new DataContext(Wtm.ConfigInfo.Connections[0].Value, DBTypeEnum.SqlServer); var Tenant = ZDC.Set<Tenant>().Where(x => x.DomainName.Replace("http://", "").Replace("https://", "") == displayUrl).FirstOrDefault(); if (Tenant != null) { TenantKey = "SAASDEMODB" + Tenant.Code; Wtm.Session.Set("TenantKey", "SAASDEMODB" + Tenant.Code); } else { Wtm.Session.Set("TenantKey", TenantKey); } int i = 0; foreach (var item in Wtm.ConfigInfo.Connections) { if (item.Key == TenantKey) { i++; break; } } if (i == 0) { CS cs = new CS(); cs.DbContext = "DataContext"; cs.DbType = DBTypeEnum.SqlServer; cs.Key = TenantKey; cs.Value = Wtm.ConfigInfo.Connections[0].Value.Replace("SAASDEMODB", cs.Key); cs.DcConstructor = Wtm.ConfigInfo.Connections[0].DcConstructor; Wtm.ConfigInfo.Connections.Add(cs); } vm.Redirect = HttpContext.Request.Query["ReturnUrl"]; if (Wtm.ConfigInfo.IsQuickDebug == true) { vm.ITCode = "admin"; vm.Password = "000000"; } return View(vm); } Startup文件中的CSSelector方法 public string CSSelector(ActionExecutingContext context) { var wtm = (context.Controller as IBaseController)?.Wtm; var TenantKey = wtm.Session.Get<string>("TenantKey"); return TenantKey; }
OK,咱們來測試一下下。
我這里就用默認超級管理員角色創建租戶了,為了添加一個租戶讓大家看下效果。
添加成功,訪問一下租戶的地址
添加一條新的角色數據,跟主庫作下比較,發下數據已經隔離了。
如果你跟着測試到這一步,說明已經通了,可以自己多試試。有問題或者有好的想法,可以在群里一起溝通學習學習。
有些可能需要用到數據共享,框架本身支持在控制器中設置[FixConnection(DBOperationEnum.Default, CsName = "")]設置Cs指定連接字符串。
項目我已經上傳到Gitee上了,大家可以下載看一下。
地址:wtm-layui版本多租戶: wtm框架 layui版本都租戶改造
下載完項目,如果想直接運行調試的話,記得去Common文件下的CommonHelp類中把DomainNameResolution方法中的參數補充全,就可以直接運行看效果了。
目前這種方式有正式的項目,目前也比較穩定。第一次寫文章,希望大家多支持哈。
==========================================================
WTM框架地址 https://wtmdoc.walkingtec.cn
支持4個版本:Layui React Vue Blazor
WtmPlus是建立在WTM開源框架基礎上的低代碼開發平台,他提供了可視化的模型和頁面編輯,更加復雜和智能的代碼生成,可使開發效率提升50%以上。
感興趣可以看一下 地址 WtmPlus