(译)创建.NET Core多租户应用程序-租户解析
介紹
本系列博客文章探討了如何在ASP.NET?Core Web應用程序中實現(xiàn)多租戶。這里有很多代碼段,因此您可以按照自己的示例應用程序進行操作。在此過程的最后,沒有對應的NuGet程序包,但這是一個很好的學習和練習。它涉及到框架的一些“核心”部分。
在本系列的改篇中,我們將解析對租戶的請求,并介紹訪問該租戶信息的能力。
系列目錄
第1部分:租戶解析(本篇)
第2部分:租戶containers
第3部分:每個租戶的選項配置
第4部分:每個租戶的身份驗證
附加:升級到.NET?Core 3.1(LTS)
什么是多租戶應用程序?
它是一個單一的代碼庫,根據(jù)訪問它的“租戶”不同而做出不同的響應,您可以使用幾種不同的模式,例如
應用程序級別隔離:為每個租戶啟動一個新網(wǎng)站和相關的依存關系
多租戶應用都擁有自己的數(shù)據(jù)庫:租戶使用相同的網(wǎng)站,但是擁有自己的數(shù)據(jù)庫
多租戶應用程序使用多租戶數(shù)據(jù)庫:租戶使用相同的網(wǎng)站和相同的數(shù)據(jù)庫(需要注意不要將數(shù)據(jù)暴露給錯誤的租戶!)
這里有關于每種模式的非常深入的指南。在本系列中,我們將探討多租戶應用程序選項。https://docs.microsoft.com/zh-cn/azure/sql-database/saas-tenancy-app-design-patterns
多租戶應用程序需要什么?
多租戶應用程序需要滿足幾個核心要求。
租戶解析
從HTTP請求中,我們將需要能夠確定在哪個租戶上下文中運行請求。這會影響諸如訪問哪個數(shù)據(jù)庫或使用哪種配置等問題。
租戶應用程序配置
根據(jù)加載的租戶上下文,可能會對應用程序進行不同的配置,例如OAuth提供程序的身份驗證密鑰,連接字符串等。
租戶數(shù)據(jù)隔離
租戶將需要能夠訪問他們的數(shù)據(jù),以及僅僅訪問他們自己的數(shù)據(jù)。這可以通過在單個數(shù)據(jù)存儲中對數(shù)據(jù)進行分區(qū)或通過使用每個租戶的數(shù)據(jù)存儲來實現(xiàn)。無論我們使用哪種模式,我們都應該使開發(fā)人員在跨租戶場景中難以公開數(shù)據(jù)以避免編碼錯誤。
租戶解析
對于任何多租戶應用程序,我們都需要能夠識別請求在哪個租戶下運行,但是在我們太興奮之前,我們需要確定查找租戶所需的數(shù)據(jù)。在此階段,我們實際上只需要一個信息,即租戶標識符。
/// <summary> /// Tenant information /// </summary> public class Tenant {/// <summary>/// The tenant Id/// </summary>public string Id { get; set; }/// <summary>/// The tenant identifier/// </summary>public string Identifier { get; set; }/// <summary>/// Tenant items/// </summary>public Dictionary<string, object> Items { get; private set; } = new Dictionary<string, object>(); }我們將Identifier根據(jù)解析方案策略使用來匹配租戶(可能是租戶的域名,例如https://{tenant}.myapplication.com)。
我們將使用它Id作為對租戶的持久引用(Identifier可能會更改,例如主機域更改)。
該屬性Items僅用于讓開發(fā)人員在請求管道期間向租戶添加其他內(nèi)容,如果他們需要特定的屬性或方法,他們還可以擴展該類。
常見的租戶解決策略
我們將使用解決方案策略將請求匹配到租戶,該策略不應依賴任何外部數(shù)據(jù)來使其變得美觀,快速。
主機頭
將根據(jù)瀏覽器發(fā)送的主機頭來推斷租戶,如果所有租戶都具有不同的域(例如)https://host1.example.com,https://host2.example.com或者https://host3.com您支持自定義域,則這是完美的選擇。
例如,如果主機標頭是,https://host1.example.com我們將Tenant使用Identifier持有值加載host1.example.com。
請求路徑
可以根據(jù)路線推斷租戶,例如?https://example.com/host1/...
標頭值
可以根據(jù)標頭值來推斷承租人,例如x-tenant: host1,如果所有承租人都可以在核心api上訪問,https://api.example.com并且客戶端可以指定要與特定標頭一起使用的承租人,則這可能很有用。
定義租戶解析策略
為了讓應用程序知道使用哪種策略,我們應該能夠?qū)崿F(xiàn)ITenantResolutionStrategy將請求解析為租戶標識符的服務。
public interface ITenantResolutionStrategy {Task<string> GetTenantIdentifierAsync(); }在這篇文章中,我們將實現(xiàn)一個策略,從主機頭那里解析租戶。
/// <summary> /// Resolve the host to a tenant identifier /// </summary> public class HostResolutionStrategy : ITenantResolutionStrategy {private readonly IHttpContextAccessor _httpContextAccessor;public HostResolutionStrategy(IHttpContextAccessor httpContextAccessor){_httpContextAccessor = httpContextAccessor;}/// <summary>/// Get the tenant identifier/// </summary>/// <param name="context"></param>/// <returns></returns>public async Task<string> GetTenantIdentifierAsync(){return await Task.FromResult(_httpContextAccessor.HttpContext.Request.Host.Host);} }租戶存儲
現(xiàn)在我們知道要加載哪個租戶,該從哪里獲取?那將需要某種租戶存儲。我們將需要實現(xiàn)一個ITenantStore接受承租人標識符并返回Tenant信息的。
public interface ITenantStore<T> where T : Tenant {Task<T> GetTenantAsync(string identifier); }我為什么要使泛型存儲?萬一我們想在使用我們庫的項目中獲得更多特定于應用程序的租戶信息,我們可以擴展租戶使其具有應用程序級別所需的任何其他屬性,并適當?shù)嘏渲么鎯?/p>
如果要針對租戶存儲連接字符串之類的內(nèi)容,則需要將其放置在安全的地方,并且最好使用每個租戶模式的選項配置,并從諸如Azure Key Vault之類的安全地方加載這些字符串。
在這篇文章中,為了簡單起見,我們將為租戶存儲執(zhí)行一個硬編碼的內(nèi)存中模擬。
/// <summary> /// In memory store for testing /// </summary> public class InMemoryTenantStore : ITenantStore<Tenant> {/// <summary>/// Get a tenant for a given identifier/// </summary>/// <param name="identifier"></param>/// <returns></returns>public async Task<Tenant> GetTenantAsync(string identifier){var tenant = new[]{new Tenant{ Id = "80fdb3c0-5888-4295-bf40-ebee0e3cd8f3", Identifier = "localhost" }}.SingleOrDefault(t => t.Identifier == identifier);return await Task.FromResult(tenant);} }與ASP.NET?Core管道集成
有兩個主要組成部分
注冊你的服務,以便可以解析它們
重新注冊一些中間件,以便您可以HttpContext在請求管道中將租戶信息添加到當前信息中,從而使下游消費者可以使用它
注冊服務
現(xiàn)在,我們有一個獲取租戶的策略,以及一個使租戶脫離的位置,我們需要在應用程序容器中注冊這些服務。我們希望該庫易于使用,因此我們將使用構(gòu)建器模式來提供積極的服務注冊體驗。
首先,我們添加一點擴展以支持.AddMultiTenancy()語法。
/// <summary> /// Nice method to create the tenant builder /// </summary> public static class ServiceCollectionExtensions {/// <summary>/// Add the services (application specific tenant class)/// </summary>/// <param name="services"></param>/// <returns></returns>public static TenantBuilder<T> AddMultiTenancy<T>(this IServiceCollection services) where T : Tenant=> new TenantBuilder<T>(services);/// <summary>/// Add the services (default tenant class)/// </summary>/// <param name="services"></param>/// <returns></returns>public static TenantBuilder<Tenant> AddMultiTenancy(this IServiceCollection services) => new TenantBuilder<Tenant>(services); }然后,我們將讓構(gòu)建器提供“流暢的”擴展。
/// <summary> /// Configure tenant services /// </summary> public class TenantBuilder<T> where T : Tenant {private readonly IServiceCollection _services;public TenantBuilder(IServiceCollection services){_services = services;}/// <summary>/// Register the tenant resolver implementation/// </summary>/// <typeparam name="V"></typeparam>/// <param name="lifetime"></param>/// <returns></returns>public TenantBuilder<T> WithResolutionStrategy<V>(ServiceLifetime lifetime = ServiceLifetime.Transient) where V : class, ITenantResolutionStrategy{_services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();_services.Add(ServiceDescriptor.Describe(typeof(ITenantResolutionStrategy), typeof(V), lifetime));return this;}/// <summary>/// Register the tenant store implementation/// </summary>/// <typeparam name="V"></typeparam>/// <param name="lifetime"></param>/// <returns></returns>public TenantBuilder<T> WithStore<V>(ServiceLifetime lifetime = ServiceLifetime.Transient) where V : class, ITenantStore<T>{_services.Add(ServiceDescriptor.Describe(typeof(ITenantStore<T>), typeof(V), lifetime));return this;} }現(xiàn)在,在.NET?Core Web應用程序ConfigureServices中的StartUp類部分中,您可以添加以下內(nèi)容。
services.AddMultiTenancy().WithResolutionStrategy<HostResolutionStrategy>().WithStore<InMemoryTenantStore>();這是一個很好的開始但接下來您可能會希望支持傳遞選項,例如,如果不使用整個域,可能會有一個模式從主機中提取tenantId等,但它現(xiàn)在可以完成任務。
此時,您將能夠?qū)⒋鎯蚪馕龇桨覆呗宰⑷氲娇刂破髦?#xff0c;但這有點低級。您不想在要訪問租戶的任何地方都必須執(zhí)行這些解決步驟。接下來,讓我們創(chuàng)建一個服務以允許我們訪問當前的租戶對象。
/// <summary> /// Tenant access service /// </summary> /// <typeparam name="T"></typeparam> public class TenantAccessService<T> where T : Tenant {private readonly ITenantResolutionStrategy _tenantResolutionStrategy;private readonly ITenantStore<T> _tenantStore;public TenantAccessService(ITenantResolutionStrategy tenantResolutionStrategy, ITenantStore<T> tenantStore){_tenantResolutionStrategy = tenantResolutionStrategy;_tenantStore = tenantStore;}/// <summary>/// Get the current tenant/// </summary>/// <returns></returns>public async Task<T> GetTenantAsync(){var tenantIdentifier = await _tenantResolutionStrategy.GetTenantIdentifierAsync();return await _tenantStore.GetTenantAsync(tenantIdentifier);} }并更新構(gòu)建器以也注冊此服務
public TenantBuilder(IServiceCollection services) {services.AddTransient<TenantAccessService<T>>();_services = services; }酷酷酷酷。現(xiàn)在,您可以通過將服務注入控制器來訪問當前租戶
/// <summary> /// A controller that returns a value /// </summary> [Route("api/values")] [ApiController] public class Values : Controller {private readonly TenantAccessService<Tenant> _tenantService;/// <summary>/// Constructor with required services/// </summary>/// <param name="tenantService"></param>public Values(TenantAccessService<Tenant> tenantService){_tenantService = tenantService;}/// <summary>/// Get the value/// </summary>/// <param name="definitionId"></param>/// <returns></returns>[HttpGet("")]public async Task<string> GetValue(Guid definitionId){return (await _tenantService.GetTenantAsync()).Id;} }運行,您應該會看到根據(jù)URL返回的租戶ID。
接下來,我們可以添加一些中間件,以將當前的Tenant注入到HttpContext中,這意味著我們可以在可以訪問HttpContext的任何地方獲取Tenant,從而更加方便。這將意味著我們不再需要大量地注入TenantAccessService。
注冊中間件
ASP.NET?Core中的中間件使您可以將一些邏輯放入請求處理管道中。在本例中,我們應該在需要訪問Tenant信息的任何內(nèi)容(例如MVC中間件)之前注冊中間件。這很可能需要處理請求的控制器中的租戶上下文。
首先讓我們創(chuàng)建我們的中間件類,這將處理請求并將其注入Tenant當前HttpContext-超級簡單。
internal class TenantMiddleware<T> where T : Tenant {private readonly RequestDelegate next;public TenantMiddleware(RequestDelegate next){this.next = next;}public async Task Invoke(HttpContext context){if (!context.Items.ContainsKey(Constants.HttpContextTenantKey)){var tenantService = context.RequestServices.GetService(typeof(TenantAccessService<T>)) as TenantAccessService<T>;context.Items.Add(Constants.HttpContextTenantKey, await tenantService.GetTenantAsync());}//Continue processingif (next != null)await next(context);} }接下來,我們創(chuàng)建一個擴展類使用它。
/// <summary> /// Nice method to register our middleware /// </summary> public static class IApplicationBuilderExtensions {/// <summary>/// Use the Teanant Middleware to process the request/// </summary>/// <typeparam name="T"></typeparam>/// <param name="builder"></param>/// <returns></returns>public static IApplicationBuilder UseMultiTenancy<T>(this IApplicationBuilder builder) where T : Tenant=> builder.UseMiddleware<TenantMiddleware<T>>();/// <summary>/// Use the Teanant Middleware to process the request/// </summary>/// <typeparam name="T"></typeparam>/// <param name="builder"></param>/// <returns></returns>public static IApplicationBuilder UseMultiTenancy(this IApplicationBuilder builder) => builder.UseMiddleware<TenantMiddleware<Tenant>>(); }最后,我們可以注冊我們的中間件,這樣做的最佳位置是在中間件之前,例如MVC可能需要訪問Tenant信息的地方。
app.UseMultiTenancy(); app.UseMvc()現(xiàn)在,Tenant它將位于items集合中,但我們并不是真的要強迫開發(fā)人員找出將其存儲在哪里,記住類型,需要對其進行轉(zhuǎn)換等。因此,我們將創(chuàng)建一個不錯的擴展方法來提取列出當前的租戶信息。
/// <summary> /// Extensions to HttpContext to make multi-tenancy easier to use /// </summary> public static class HttpContextExtensions {/// <summary>/// Returns the current tenant/// </summary>/// <typeparam name="T"></typeparam>/// <param name="context"></param>/// <returns></returns>public static T GetTenant<T>(this HttpContext context) where T : Tenant{if (!context.Items.ContainsKey(Constants.HttpContextTenantKey))return null;return context.Items[Constants.HttpContextTenantKey] as T;}/// <summary>/// Returns the current Tenant/// </summary>/// <param name="context"></param>/// <returns></returns>public static Tenant GetTenant(this HttpContext context){return context.GetTenant<Tenant>();} }現(xiàn)在,我們可以修改我們的Values控制器,演示使用當前的HttpContext而不是注入服務。
/// <summary> /// A controller that returns a value /// </summary> [Route("api/values")] [ApiController] public class Values : Controller {/// <summary>/// Get the value/// </summary>/// <param name="definitionId"></param>/// <returns></returns>[HttpGet("")]public async Task<string> GetValue(Guid definitionId){return await Task.FromResult(HttpContext.GetTenant().Id);} }如果運行,您將得到相同的結(jié)果????
我們的應用程序是“租戶感知”的。這是一個重大的里程碑。
‘加個餐’,租戶上下文訪問者
在ASP.NET?Core中,可以使用IHttpContextAccessor訪問服務內(nèi)的HttpContext,為了開發(fā)人員提供對租戶信息的熟悉訪問模式,我們可以創(chuàng)建ITenantAccessor服務。
首先定義一個接口
public interface ITenantAccessor<T> where T : Tenant {T Tenant { get; } }然后實現(xiàn)
public class TenantAccessor<T> : ITenantAccessor<T> where T : Tenant {private readonly IHttpContextAccessor _httpContextAccessor;public TenantAccessor(IHttpContextAccessor httpContextAccessor){_httpContextAccessor = httpContextAccessor;}public T Tenant => _httpContextAccessor.HttpContext.GetTenant<T>(); }現(xiàn)在,如果下游開發(fā)人員想要向您的應用程序添加一個需要訪問當前租戶上下文的服務,他們只需以與使用IHttpContextAccessor完全相同的方式注入ITenantAccessor<T>??
只需將該TenantAccessService<T>類標記為內(nèi)部類,這樣就不會在我們的程序集之外錯誤地使用它。
小結(jié)
在這篇文章中,我們研究了如何將請求映射到租戶。我們將應用程序容器配置為能夠解析我們的租戶服務,甚至創(chuàng)建了ITenantAccessor服務,以允許在其他服務(如IHttpContextAccessor)內(nèi)部訪問該租賃者。我們還編寫了自定義中間件,將當前的租戶信息注入到HttpContext中,以便下游中間件可以輕松訪問它,并創(chuàng)建了一個不錯的擴展方法,以便您可以像HttpContext.GetTenant()一樣輕松地獲取當前的Tenant。在下一篇文章中,我們將研究按租戶隔離數(shù)據(jù)訪問。
在本系列的下一篇文章中,我們將介紹如何在每個租戶的基礎上配置服務,以便我們可以根據(jù)活動的租戶解析不同的實現(xiàn)。
總結(jié)
以上是生活随笔為你收集整理的(译)创建.NET Core多租户应用程序-租户解析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: DotNetCore三大Redis客户端
- 下一篇: Docker-HealthCheck指令