Blazor Server混合策略多租户实现及身份权限问题咨询
解决方案:混合模式多租户+多租户角色集成
一、实现子域名+路径的混合租户解析策略
Finbuckle支持同时配置多个租户解析策略,结合租户Host属性调整优先级和匹配逻辑即可实现混合模式:
- 扩展租户实体
确保租户实体包含Host字段,用于存储子域名(可为空,为空则使用路径模式):
public class Tenant { public string Id { get; set; } public string Name { get; set; } public string? Host { get; set; } // 其他租户属性 }
- 配置Finbuckle混合解析逻辑
优先使用子域名解析,失败后再尝试路径解析,同时通过自定义解析器过滤不符合规则的匹配:
services.AddFinbuckleMultiTenant<Tenant>() .WithHostStrategy(options => { options.HostIdentifier = "tenant"; options.TryRemovePort = true; }) .WithPathStrategy(options => { options.PathSegment = 0; // 取路径第一个分段作为租户ID }) .WithEntityFrameworkStore<AppDbContext, Tenant>() // 替换为你的EF存储 .WithTenantResolver<HybridTenantResolver>();
- 自定义租户解析器
控制解析逻辑:若租户存在Host,则拒绝路径模式的匹配结果:
public class HybridTenantResolver : ITenantResolver<Tenant> { private readonly IEnumerable<ITenantResolver<Tenant>> _resolvers; private readonly ITenantStore<Tenant> _tenantStore; public HybridTenantResolver(IEnumerable<ITenantResolver<Tenant>> resolvers, ITenantStore<Tenant> tenantStore) { // 子域名解析优先 _resolvers = resolvers.OrderBy(r => r is HostTenantResolver<Tenant> ? 0 : 1); _tenantStore = tenantStore; } public async Task<TenantResolveResult<Tenant>> ResolveAsync(HttpContext context) { foreach (var resolver in _resolvers) { var result = await resolver.ResolveAsync(context); if (result.Tenant != null) { // 租户有Host但通过路径解析到,返回匹配失败 if (!string.IsNullOrEmpty(result.Tenant.Host) && resolver is PathTenantResolver<Tenant>) { return TenantResolveResult<Tenant>.Fail("Tenant requires subdomain access"); } return result; } } return TenantResolveResult<Tenant>.Fail("No tenant matched"); } }
二、拦截/重定向路径模式访问(租户有Host时)
添加自定义中间件,在租户解析完成后校验访问模式,不符合则重定向到子域名地址:
public class TenantAccessEnforcementMiddleware { private readonly RequestDelegate _next; public TenantAccessEnforcementMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { var tenant = context.GetMultiTenantContext<Tenant>()?.Tenant; if (tenant != null && !string.IsNullOrEmpty(tenant.Host)) { var pathSegments = context.Request.Path.Value?.Split('/').Where(s => !string.IsNullOrEmpty(s)).ToList(); // 判断是否为路径模式访问 if (pathSegments?.Count > 0 && pathSegments[0] == tenant.Id) { // 构造子域名地址并重定向 var newUrl = new UriBuilder(context.Request.Scheme, tenant.Host) { Path = string.Join('/', pathSegments.Skip(1)), Query = context.Request.QueryString.ToString() }.Uri.ToString(); context.Response.Redirect(newUrl, permanent: true); return; } } await _next(context); } } // 注册中间件(需放在Finbuckle中间件之后) app.UseFinbuckleMultiTenant(); app.UseMiddleware<TenantAccessEnforcementMiddleware>();
三、多租户角色与.NET Core Identity集成
默认Identity角色为全局,需扩展实现租户隔离的角色系统:
1. 扩展Identity实体
创建租户关联的角色实体,以及用户-租户-角色的关联表:
// 租户角色 public class ApplicationRole : IdentityRole { public string TenantId { get; set; } public virtual Tenant Tenant { get; set; } } // 用户-租户-角色关联 public class UserTenantRole { public string UserId { get; set; } public virtual ApplicationUser User { get; set; } public string RoleId { get; set; } public virtual ApplicationRole Role { get; set; } public string TenantId { get; set; } public virtual Tenant Tenant { get; set; } } // 扩展用户实体 public class ApplicationUser : IdentityUser { public virtual ICollection<UserTenantRole> UserTenantRoles { get; set; } = new List<UserTenantRole>(); }
2. 配置EF Core实体关系与查询筛选器
在DbContext中配置约束和全局筛选器,确保角色数据隔离:
public class AppDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string> { private readonly IMultiTenantContext<Tenant> _tenantContext; public AppDbContext(DbContextOptions<AppDbContext> options, IMultiTenantContext<Tenant> tenantContext) : base(options) { _tenantContext = tenantContext; } public DbSet<UserTenantRole> UserTenantRoles { get; set; } public DbSet<Tenant> Tenants { get; set; } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); // 角色名+租户ID唯一约束 builder.Entity<ApplicationRole>() .HasIndex(r => new { r.Name, r.TenantId }) .IsUnique(); // 用户-租户-角色复合主键 builder.Entity<UserTenantRole>() .HasKey(utr => new { utr.UserId, utr.RoleId, utr.TenantId }); // 全局筛选器:只查询当前租户的角色 builder.Entity<ApplicationRole>() .HasQueryFilter(r => r.TenantId == _tenantContext.Tenant.Id); } }
3. 自定义UserManager实现租户角色操作
扩展UserManager,添加创建/分配租户角色的方法:
public class ApplicationUserManager : UserManager<ApplicationUser> { private readonly RoleManager<ApplicationRole> _roleManager; private readonly IMultiTenantContext<Tenant> _tenantContext; public ApplicationUserManager(IUserStore<ApplicationUser> store, IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<ApplicationUser> passwordHasher, IEnumerable<IUserValidator<ApplicationUser>> userValidators, IEnumerable<IPasswordValidator<ApplicationUser>> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<ApplicationUser>> logger, RoleManager<ApplicationRole> roleManager, IMultiTenantContext<Tenant> tenantContext) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { _roleManager = roleManager; _tenantContext = tenantContext; } // 为用户分配当前租户的角色 public async Task<IdentityResult> AddToTenantRoleAsync(ApplicationUser user, string roleName) { var tenantId = _tenantContext.Tenant.Id; var role = await _roleManager.FindByNameAsync(roleName); if (role == null) { // 创建当前租户的角色 role = new ApplicationRole { Name = roleName, TenantId = tenantId }; var createResult = await _roleManager.CreateAsync(role); if (!createResult.Succeeded) return createResult; } else if (role.TenantId != tenantId) { return IdentityResult.Failed(new IdentityError { Description = "Role belongs to another tenant" }); } return await AddToRoleAsync(user, roleName); } }
4. 动态生成租户角色Claims
自定义ClaimsPrincipalFactory,登录时根据当前租户添加角色Claims,无需字符串拼接:
public class TenantClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, ApplicationRole> { private readonly IMultiTenantContext<Tenant> _tenantContext; public TenantClaimsPrincipalFactory(UserManager<ApplicationUser> userManager, RoleManager<ApplicationRole> roleManager, IOptions<IdentityOptions> optionsAccessor, IMultiTenantContext<Tenant> tenantContext) : base(userManager, roleManager, optionsAccessor) { _tenantContext = tenantContext; } protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user) { var identity = await base.GenerateClaimsAsync(user); var tenantId = _tenantContext.Tenant.Id; // 获取用户在当前租户的所有角色(全局筛选器已确保只返回当前租户角色) var roles = await UserManager.GetRolesAsync(user); foreach (var role in roles) { identity.AddClaim(new Claim(ClaimTypes.Role, role)); } // 添加租户ID Claim(可选) identity.AddClaim(new Claim("TenantId", tenantId)); return identity; } }
5. 注册自定义Identity服务
替换默认Identity服务为自定义实现:
services.AddIdentity<ApplicationUser, ApplicationRole>() .AddEntityFrameworkStores<AppDbContext>() .AddUserManager<ApplicationUserManager>() .AddClaimsPrincipalFactory<TenantClaimsPrincipalFactory>() .AddDefaultTokenProviders();
四、关键注意事项
- 中间件顺序:
UseFinbuckleMultiTenant必须放在UseAuthentication和UseAuthorization之前,确保租户信息在认证授权流程中可用。 - 数据隔离:所有租户隔离的业务实体都需添加全局查询筛选器,防止跨租户数据泄漏。
- 角色唯一性:同一租户内角色名唯一,不同租户可重复使用相同角色名。
内容的提问来源于stack exchange,提问作者Andreea Vasiliu
相关产品推荐
相关产品推荐

