无锡网站搜索优化,首页模板,wordpress文章列表添加字段,wordpress怎么发到微信上一、简介ABP vNext 原生支持多租户体系#xff0c;可以让开发人员快速地基于框架开发 SaaS 系统。ABP vNext 实现多租户的思路也非常简单#xff0c;通过一个 TenantId 来分割各个租户的数据#xff0c;并且在查询的时候使用统一的全局过滤器(类似于软删除)来筛选数据。关于… 一、简介ABP vNext 原生支持多租户体系可以让开发人员快速地基于框架开发 SaaS 系统。ABP vNext 实现多租户的思路也非常简单通过一个 TenantId 来分割各个租户的数据并且在查询的时候使用统一的全局过滤器(类似于软删除)来筛选数据。关于多租户体系的东西基本定义与核心逻辑存放在 Volo.ABP.MultiTenancy 内部。针对 ASP.NET Core MVC 的集成则是由 Volo.ABP.AspNetCore.MultiTenancy 项目实现的针对多租户的解析都在这个项目内部。租户数据的存储和管理都由 Volo.ABP.TenantManagement 模块提供开发人员也可以直接使用该项目快速实现多租户功能。二、源码分析2.1 启动模块AbpMultiTenancyModule 模块是启用整个多租户功能的核心模块内部只进行了一个动作就是从配置类当中读取多租户的基本信息以 JSON Provider 为例就需要在 appsettings.json 里面有 Tenants 节。CopyTenants: [{Id: 446a5211-3d72-4339-9adc-845151f8ada0,Name: tenant1},{Id: 25388015-ef1c-4355-9c18-f6b6ddbaf89d,Name: tenant2,ConnectionStrings: {Default: ...write tenant2s db connection string here...}}]2.1.1 默认租户来源这里的数据将会作为默认租户来源也就是说在确认当前租户的时候会从这里面的数据与要登录的租户进行比较如果不存在则不允许进行操作。Copypublic interface ITenantStore
{TaskTenantConfiguration FindAsync(string name);TaskTenantConfiguration FindAsync(Guid id);TenantConfiguration Find(string name);TenantConfiguration Find(Guid id);
}默认的存储实现Copy[Dependency(TryRegister true)]
public class DefaultTenantStore : ITenantStore, ITransientDependency
{// 直接从 Options 当中获取租户数据。private readonly AbpDefaultTenantStoreOptions _options;public DefaultTenantStore(IOptionsSnapshotAbpDefaultTenantStoreOptions options){_options options.Value;}public TaskTenantConfiguration FindAsync(string name){return Task.FromResult(Find(name));}public TaskTenantConfiguration FindAsync(Guid id){return Task.FromResult(Find(id));}public TenantConfiguration Find(string name){return _options.Tenants?.FirstOrDefault(t t.Name name);}public TenantConfiguration Find(Guid id){return _options.Tenants?.FirstOrDefault(t t.Id id);}
}除了从配置文件当中读取租户信息以外开发人员也可以自己实现 ITenantStore 接口比如说像 TenantManagement 一样将租户信息存储到数据库当中。2.1.2 基于数据库的租户存储话接上文我们说过在 Volo.ABP.TenantManagement 模块内部有提供另一种 ITenantStore 接口的实现这个类型叫做 TenantStore内部逻辑也很简单就是从仓储当中查找租户数据。Copypublic class TenantStore : ITenantStore, ITransientDependency
{private readonly ITenantRepository _tenantRepository;private readonly IObjectMapperAbpTenantManagementDomainModule _objectMapper;private readonly ICurrentTenant _currentTenant;public TenantStore(ITenantRepository tenantRepository, IObjectMapperAbpTenantManagementDomainModule objectMapper,ICurrentTenant currentTenant){_tenantRepository tenantRepository;_objectMapper objectMapper;_currentTenant currentTenant;}public async TaskTenantConfiguration FindAsync(string name){// 变更当前租户为租主。using (_currentTenant.Change(null)) //TODO: No need this if we can implement to define host side (or tenant-independent) entities!{// 通过仓储查询租户是否存在。var tenant await _tenantRepository.FindByNameAsync(name);if (tenant null){return null;}// 将查询到的信息转换为核心库定义的租户信息。return _objectMapper.MapTenant, TenantConfiguration(tenant);}}// ... 其他的代码已经省略。
}可以看到最后也是返回的一个 TenantConfiguration 类型。关于这个类型是 ABP 在多租户核心库定义的一个基本类型之一主要是用于规定持久化一个租户信息需要包含的属性。Copy[Serializable]
public class TenantConfiguration
{// 租户的 Guid。public Guid Id { get; set; }// 租户的名称。public string Name { get; set; }// 租户对应的数据库连接字符串。public ConnectionStrings ConnectionStrings { get; set; }public TenantConfiguration(){}public TenantConfiguration(Guid id, [NotNull] string name){Check.NotNull(name, nameof(name));Id id;Name name;ConnectionStrings new ConnectionStrings();}
}2.2 租户的解析ABP vNext 如果要判断当前的租户是谁则是通过 AbpTenantResolveOptions 提供的一组 ITenantResolveContributor 进行处理的。Copypublic class AbpTenantResolveOptions
{// 会使用到的这组解析对象。[NotNull]public ListITenantResolveContributor TenantResolvers { get; }public AbpTenantResolveOptions(){TenantResolvers new ListITenantResolveContributor{// 默认的解析对象会通过 Token 内字段解析当前租户。new CurrentUserTenantResolveContributor()};}
}这里的设计与权限一样都是由一组 解析对象(解析器) 进行处理在上层开放的入口只有一个 ITenantResolver 内部通过 foreach 执行这组解析对象的 Resolve() 方法。下面就是我们 ITenantResolver 的默认实现 TenantResolver你可以在任何时候调用它。比如说你在想要获得当前租户 Id 的时候。不过一般不推荐这样做因为 ABP 已经给我们提供了 MultiTenancyMiddleware 中间件。也就是说在每次请求的时候都会将这个 Id 通过 ICurrentTenant.Change() 进行变更那么在这个请求执行完成之前通过 ICurrentTenant 取得的 Id 都会是解析器解析出来的 Id。Copypublic class TenantResolver : ITenantResolver, ITransientDependency
{private readonly IServiceProvider _serviceProvider;private readonly AbpTenantResolveOptions _options;public TenantResolver(IOptionsAbpTenantResolveOptions options, IServiceProvider serviceProvider){_serviceProvider serviceProvider;_options options.Value;}public TenantResolveResult ResolveTenantIdOrName(){var result new TenantResolveResult();using (var serviceScope _serviceProvider.CreateScope()){// 创建一个解析上下文用于存储解析器的租户 Id 解析结果。var context new TenantResolveContext(serviceScope.ServiceProvider);// 遍历执行解析器。foreach (var tenantResolver in _options.TenantResolvers){tenantResolver.Resolve(context);result.AppliedResolvers.Add(tenantResolver.Name);// 如果有某个解析器为上下文设置了值则跳出。if (context.HasResolvedTenantOrHost()){result.TenantIdOrName context.TenantIdOrName;break;}}}return result;}
}2.2.1 默认的解析对象如果不使用 Volo.Abp.AspNetCore.MultiTenancy 模块ABP vNext 会调用 CurrentUserTenantResolveContributor 解析当前操作的租户。Copypublic class CurrentUserTenantResolveContributor : TenantResolveContributorBase
{public const string ContributorName CurrentUser;public override string Name ContributorName;public override void Resolve(ITenantResolveContext context){// 从 Token 当中获取当前登录用户的信息。var currentUser context.ServiceProvider.GetRequiredServiceICurrentUser();if (currentUser.IsAuthenticated ! true){return;}// 设置解析上下文确认当前的租户 Id。context.Handled true;context.TenantIdOrName currentUser.TenantId?.ToString();}
}在这里可以看到如果从 Token 当中解析到了租户 Id会将这个 Id 传递给 解析上下文。这个上下文在最开始已经遇到过了如果 ABP vNext 在解析的时候发现租户 Id 被确认了就不会执行剩下的解析器。2.2.2 ABP 提供的其他解析器ABP 在 Volo.Abp.AspNetCore.MultiTenancy 模块当中还提供了其他几种解析器他们的作用分别如下。解析器类型作用优先级QueryStringTenantResolveContributor通过 Query String 的 __tenant 参数确认租户。2RouteTenantResolveContributor通过路由判断当前租户。3HeaderTenantResolveContributor通过 Header 里面的 __tenant 确认租户。4CookieTenantResolveContributor通过携带的 Cookie 确认租户。5DomainTenantResolveContributor二级域名解析器通过二级域名确定租户。第二2.2.3 域名解析器这里比较有意思的是 DomainTenantResolveContributor开发人员可以通过 AbpTenantResolveOptions.AddDomainTenantResolver() 方法添加这个解析器。 域名解析器会通过解析二级域名来匹配对应的租户例如我针对租户 A 分配了一个二级域名 http://a.system.com那么这个 a 就会被作为租户名称解析出来最后传递给 ITenantResolver 解析器作为结果。注意在使用 Header 作为租户信息提供者的时候开发人员使用的是 NGINX 作为反向代理服务器 时需要在对应的 config 文件内部配置 underscores_in_headers on; 选项。否则 ABP 所需要的 __tenantId 将会被过滤掉或者你可以指定一个没有下划线的 Key。域名解析器的详细代码解释Copypublic class DomainTenantResolveContributor : HttpTenantResolveContributorBase
{public const string ContributorName Domain;public override string Name ContributorName;private static readonly string[] ProtocolPrefixes { http://, https:// };private readonly string _domainFormat;// 使用指定的格式来确定租户前缀例如 “{0}.abp.io”。public DomainTenantResolveContributor(string domainFormat){_domainFormat domainFormat.RemovePreFix(ProtocolPrefixes);}protected override string GetTenantIdOrNameFromHttpContextOrNull(ITenantResolveContext context, HttpContext httpContext){// 如果 Host 值为空则不进行任何操作。if (httpContext.Request?.Host null){return null;}// 解析具体的域名信息并进行匹配。var hostName httpContext.Request.Host.Host.RemovePreFix(ProtocolPrefixes);// 这里的 FormattedStringValueExtracter 类型是 ABP 自己实现的一个格式化解析器。var extractResult FormattedStringValueExtracter.Extract(hostName, _domainFormat, ignoreCase: true);context.Handled true;if (!extractResult.IsMatch){return null;}return extractResult.Matches[0].Value;}
}从上述代码可以知道域名解析器是基于 HttpTenantResolveContributorBase 基类进行处理的这个抽象基类会取得当前请求的一个 HttpContext将这个传递与解析上下文一起传递给子类实现由子类实现负责具体的解析逻辑。Copypublic abstract class HttpTenantResolveContributorBase : TenantResolveContributorBase
{public override void Resolve(ITenantResolveContext context){// 获取当前请求的上下文。var httpContext context.GetHttpContext();if (httpContext null){return;}try{ResolveFromHttpContext(context, httpContext);}catch (Exception e){context.ServiceProvider.GetRequiredServiceILoggerHttpTenantResolveContributorBase().LogWarning(e.ToString());}}protected virtual void ResolveFromHttpContext(ITenantResolveContext context, HttpContext httpContext){// 调用抽象方法获取具体的租户 Id 或名称。var tenantIdOrName GetTenantIdOrNameFromHttpContextOrNull(context, httpContext);if (!tenantIdOrName.IsNullOrEmpty()){// 获得到租户标识之后填充到解析上下文。context.TenantIdOrName tenantIdOrName;}}protected abstract string GetTenantIdOrNameFromHttpContextOrNull([NotNull] ITenantResolveContext context, [NotNull] HttpContext httpContext);
}2.3 租户信息的传递租户解析器通过一系列的解析对象获取到了租户或租户 Id 之后会将这些数据给哪些对象呢或者说ABP 在什么地方调用了 租户解析器答案就是 中间件。在 Volo.ABP.AspNetCore.MultiTenancy 模块的内部提供了一个 MultiTenancyMiddleware 中间件。开发人员如果需要使用 ASP.NET Core 的多租户相关功能也可以引入该模块。并且在模块的 OnApplicationInitialization() 方法当中使用 IApplicationBuilder.UseMultiTenancy() 进行启用。这里在启用的时候需要注意中间件的顺序和位置不要放到最末尾进行处理。Copypublic class MultiTenancyMiddleware : IMiddleware, ITransientDependency
{private readonly ITenantResolver _tenantResolver;private readonly ITenantStore _tenantStore;private readonly ICurrentTenant _currentTenant;private readonly ITenantResolveResultAccessor _tenantResolveResultAccessor;public MultiTenancyMiddleware(ITenantResolver tenantResolver, ITenantStore tenantStore, ICurrentTenant currentTenant, ITenantResolveResultAccessor tenantResolveResultAccessor){_tenantResolver tenantResolver;_tenantStore tenantStore;_currentTenant currentTenant;_tenantResolveResultAccessor tenantResolveResultAccessor;}public async Task InvokeAsync(HttpContext context, RequestDelegate next){// 通过租户解析器获取当前请求的租户信息。var resolveResult _tenantResolver.ResolveTenantIdOrName();_tenantResolveResultAccessor.Result resolveResult;TenantConfiguration tenant null;// 如果当前请求是属于租户请求。if (resolveResult.TenantIdOrName ! null){// 查询指定的租户 Id 或名称是否存在不存在则抛出异常。tenant await FindTenantAsync(resolveResult.TenantIdOrName);if (tenant null){//TODO: A better exception?throw new AbpException(There is no tenant with given tenant id or name: resolveResult.TenantIdOrName);}}// 在接下来的请求当中将会通过 ICurrentTenant.Change() 方法变更当前租户直到// 请求结束。using (_currentTenant.Change(tenant?.Id, tenant?.Name)){await next(context);}}private async TaskTenantConfiguration FindTenantAsync(string tenantIdOrName){// 如果可以格式化为 Guid 则说明是租户 Id。if (Guid.TryParse(tenantIdOrName, out var parsedTenantId)){return await _tenantStore.FindAsync(parsedTenantId);}else{return await _tenantStore.FindAsync(tenantIdOrName);}}
}在取得了租户的标识(Id 或名称)之后将会通过 ICurrentTenant.Change() 方法变更当前租户的信息变更了当租户信息以后在程序的其他任何地方使用 ICurrentTenant.Id 取得的数据都是租户解析器解析出来的数据。下面就是这个当前租户的具体实现可以看到这里采用了一个 经典手法-嵌套。这个手法在工作单元和数据过滤器有见到过结合 DisposeAction() 在 using 语句块结束的时候把当前的租户 Id 值设置为父级 Id。即在同一个语句当中可以通过嵌套 using 语句块来处理不同的租户。Copyusing(_currentTenant.Change(A))
{Logger.LogInformation(_currentTenant.Id);using(_currentTenant.Change(B)){Logger.LogInformation(_currentTenant.Id);}
}具体的实现代码这里的 ICurrentTenantAccessor 内部实现就是一个 AsyncLocalBasicTenantInfo 用于在一个异步请求内部进行数据传递。Copypublic class CurrentTenant : ICurrentTenant, ITransientDependency
{public virtual bool IsAvailable Id.HasValue;public virtual Guid? Id _currentTenantAccessor.Current?.TenantId;public string Name _currentTenantAccessor.Current?.Name;private readonly ICurrentTenantAccessor _currentTenantAccessor;public CurrentTenant(ICurrentTenantAccessor currentTenantAccessor){_currentTenantAccessor currentTenantAccessor;}public IDisposable Change(Guid? id, string name null){return SetCurrent(id, name);}private IDisposable SetCurrent(Guid? tenantId, string name null){var parentScope _currentTenantAccessor.Current;_currentTenantAccessor.Current new BasicTenantInfo(tenantId, name);return new DisposeAction(() {_currentTenantAccessor.Current parentScope;});}
}这里的 BasicTenantInfo 与 TenantConfiguraton 不同前者仅用于在程序当中传递用户的基本信息而后者是用于定于持久化的标准模型。2.4 租户的使用2.4.1 数据库过滤租户的核心作用就是隔离不同客户的数据关于过滤的基本逻辑则是存放在 AbpDbContextTDbContext 的。从下面的代码可以看到在使用的时候会从注入一个 ICurrentTenant 接口这个接口可以获得从租户解析器里面取得的租户 Id 信息。并且还有一个 IsMultiTenantFilterEnabled() 方法来判定当前 是否应用租户过滤器。Copypublic abstract class AbpDbContextTDbContext : DbContext, IEfCoreDbContext, ITransientDependencywhere TDbContext : DbContext
{protected virtual Guid? CurrentTenantId CurrentTenant?.Id;protected virtual bool IsMultiTenantFilterEnabled DataFilter?.IsEnabledIMultiTenant() ?? false;// ... 其他的代码。public ICurrentTenant CurrentTenant { get; set; }// ... 其他的代码。protected virtual ExpressionFuncTEntity, bool CreateFilterExpressionTEntity() where TEntity : class{// 定义一个 Lambda 表达式。ExpressionFuncTEntity, bool expression null;// 如果聚合根/实体实现了软删除接口则构建一个软删除过滤器。if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity))){expression e !IsSoftDeleteFilterEnabled || !EF.Propertybool(e, IsDeleted);}// 如果聚合根/实体实现了多租户接口则构建一个多租户过滤器。if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity))){// 筛选 TenantId 为 CurrentTenantId 的数据。ExpressionFuncTEntity, bool multiTenantFilter e !IsMultiTenantFilterEnabled || EF.PropertyGuid(e, TenantId) CurrentTenantId;expression expression null ? multiTenantFilter : CombineExpressions(expression, multiTenantFilter);}return expression;}// ... 其他的代码。
}2.4.2 种子数据构建在 Volo.ABP.TenantManagement 模块当中如果用户创建了一个租户ABP 不只是在租户表插入一条新数据而已。它还会设置种子数据的 构造上下文并且执行所有的 种子数据构建者(IDataSeedContributor)。Copy[Authorize(TenantManagementPermissions.Tenants.Create)]
public virtual async TaskTenantDto CreateAsync(TenantCreateDto input)
{var tenant await TenantManager.CreateAsync(input.Name);await TenantRepository.InsertAsync(tenant);using (CurrentTenant.Change(tenant.Id, tenant.Name)){//TODO: Handle database creation?//TODO: Set admin email password..?await DataSeeder.SeedAsync(tenant.Id);}return ObjectMapper.MapTenant, TenantDto(tenant);
}这些构建者当中就包括租户的超级管理员(admin)和角色构建以及针对超级管理员角色进行权限赋值操作。这里需要注意第二点如果开发人员没有指定超级管理员用户和密码那么还是会使用默认密码为租户生成超级管理员具体原因看如下代码。Copypublic class IdentityDataSeedContributor : IDataSeedContributor, ITransientDependency
{private readonly IIdentityDataSeeder _identityDataSeeder;public IdentityDataSeedContributor(IIdentityDataSeeder identityDataSeeder){_identityDataSeeder identityDataSeeder;}public Task SeedAsync(DataSeedContext context){return _identityDataSeeder.SeedAsync(context[AdminEmail] as string ?? adminabp.io,context[AdminPassword] as string ?? 1q2w3E*,context.TenantId);}
}所以开发人员要实现为不同租户 生成随机密码那么就不能够使用 TenantManagement 提供的创建方法而是需要自己编写一个应用服务进行处理。2.4.3 权限的控制如果开发人员使用了 ABP 提供的 Volo.Abp.PermissionManagement 模块就会看到在它的种子数据构造者当中会对权限进行判定。因为有一些 超级权限 是租主才能够授予的例如租户的增加、删除、修改等这些超级权限在定义的时候就需要说明是否是数据租主独有的。关于这点可以参考租户管理模块在权限定义时传递的 MultiTenancySides.Host 参数。Copypublic class AbpTenantManagementPermissionDefinitionProvider : PermissionDefinitionProvider
{public override void Define(IPermissionDefinitionContext context){var tenantManagementGroup context.AddGroup(TenantManagementPermissions.GroupName, L(Permission:TenantManagement));var tenantsPermission tenantManagementGroup.AddPermission(TenantManagementPermissions.Tenants.Default, L(Permission:TenantManagement), multiTenancySide: MultiTenancySides.Host);tenantsPermission.AddChild(TenantManagementPermissions.Tenants.Create, L(Permission:Create), multiTenancySide: MultiTenancySides.Host);tenantsPermission.AddChild(TenantManagementPermissions.Tenants.Update, L(Permission:Edit), multiTenancySide: MultiTenancySides.Host);tenantsPermission.AddChild(TenantManagementPermissions.Tenants.Delete, L(Permission:Delete), multiTenancySide: MultiTenancySides.Host);tenantsPermission.AddChild(TenantManagementPermissions.Tenants.ManageFeatures, L(Permission:ManageFeatures), multiTenancySide: MultiTenancySides.Host);tenantsPermission.AddChild(TenantManagementPermissions.Tenants.ManageConnectionStrings, L(Permission:ManageConnectionStrings), multiTenancySide: MultiTenancySides.Host);}private static LocalizableString L(string name){return LocalizableString.CreateAbpTenantManagementResource(name);}
}下面是权限种子数据构造者的代码Copypublic class PermissionDataSeedContributor : IDataSeedContributor, ITransientDependency
{protected ICurrentTenant CurrentTenant { get; }protected IPermissionDefinitionManager PermissionDefinitionManager { get; }protected IPermissionDataSeeder PermissionDataSeeder { get; }public PermissionDataSeedContributor(IPermissionDefinitionManager permissionDefinitionManager,IPermissionDataSeeder permissionDataSeeder,ICurrentTenant currentTenant){PermissionDefinitionManager permissionDefinitionManager;PermissionDataSeeder permissionDataSeeder;CurrentTenant currentTenant;}public virtual Task SeedAsync(DataSeedContext context){// 通过 GetMultiTenancySide() 方法判断当前执行// 种子构造者的租户情况是租主还是租户。var multiTenancySide CurrentTenant.GetMultiTenancySide();// 根据条件筛选权限。var permissionNames PermissionDefinitionManager.GetPermissions().Where(p p.MultiTenancySide.HasFlag(multiTenancySide)).Select(p p.Name).ToArray();// 将权限授予具体租户的角色。return PermissionDataSeeder.SeedAsync(RolePermissionValueProvider.ProviderName,admin,permissionNames,context.TenantId);}
}而 ABP 在判断当前是租主还是租户的方法也很简单如果当前租户 Id 为 NULL 则说明是租主如果不为空则说明是具体租户。Copypublic static MultiTenancySides GetMultiTenancySide(this ICurrentTenant currentTenant)
{return currentTenant.Id.HasValue? MultiTenancySides.Tenant: MultiTenancySides.Host;
}2.4.4 租户的独立设置关于这块的内容可以参考之前的 这篇文章 ABP 也为我们提供了各个租户独立的自定义参数在这块功能是由 TenantSettingManagementProvider 实现的只需要在设置参数值的时候提供租户的 ProviderName 即可。例如CopysettingManager.SetAsync(WeChatIsOpen, true, TenantSettingValueProvider.ProviderName, tenantId.ToString(), false);