You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor Server混合策略多租户实现及身份权限问题咨询

解决方案:混合模式多租户+多租户角色集成

一、实现子域名+路径的混合租户解析策略

Finbuckle支持同时配置多个租户解析策略,结合租户Host属性调整优先级和匹配逻辑即可实现混合模式:

  1. 扩展租户实体
    确保租户实体包含Host字段,用于存储子域名(可为空,为空则使用路径模式):
public class Tenant
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string? Host { get; set; }
    // 其他租户属性
}
  1. 配置Finbuckle混合解析逻辑
    优先使用子域名解析,失败后再尝试路径解析,同时通过自定义解析器过滤不符合规则的匹配:
services.AddFinbuckleMultiTenant<Tenant>()
    .WithHostStrategy(options => 
    {
        options.HostIdentifier = "tenant";
        options.TryRemovePort = true;
    })
    .WithPathStrategy(options => 
    {
        options.PathSegment = 0; // 取路径第一个分段作为租户ID
    })
    .WithEntityFrameworkStore<AppDbContext, Tenant>() // 替换为你的EF存储
    .WithTenantResolver<HybridTenantResolver>();
  1. 自定义租户解析器
    控制解析逻辑:若租户存在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 23:04:53