【问题标题】:ASP MVC EF6 Multi Tenant based on host基于主机的 ASP MVC EF6 多租户
【发布时间】:2016-12-09 18:07:28
【问题描述】:

对不起,另一个多租户帖子。我找不到一个好的网站解决方案,我已经阅读了大量关于 ASP MVC 多租户的精彩帖子,但我仍然需要一些好的建议。

我有一个 ASP MVC Entity Framework 6 Code First Web 应用程序。这个应用程序必须为许多不同的客户使用一个数据库。

我对所有客户端都有一个实体,每个客户端可以有不同的主机。

public class Client
{
    public int ClientId { get; set; }
    public string Name { get; set; }
    ...
    public ICollection<ClientHost> Hosts { get; set; }
}

public class ClientHost
{
    public int ClientId { get; set; }
    public Client Client { get; set; }
    public string Name { get; set; }
}

我在所有需要过滤的实体中添加了一列“ClientId”,这样我就可以将来自不同客户端的数据分开。

public class SomeEntity
{
    public int Id { get; set; }
    ...
    public int ClientId { get; set; }
}

我需要的第一件事是,基于主机,检索要使用的 ClientId。

private static int GetClientId()
{
    var currentClient = Convert.ToInt32(HttpRuntime.Cache[CacheClient]);
    if (currentClient != null) return currentClient;

    lock (Synclock)
    {
        using (var dataContext = new MyDataContext())
        {
            var urlHost = HttpContext.Current.Request.Url.Host;
            currentClient = dataContext.Clients
               .FirstOrDefault(p => p.Hosts.Any(h => h.Name == urlHost));

            if (currentClient == null) return null;

            HttpRuntime.Cache.Insert(CacheClient, currentClient, null, Cache.NoAbsoluteExpiration, TimeSpan.FromSeconds(0), CacheItemPriority.Default, null);

            return currentClient;
        }

    }
}

问题 1
如您所见,我从 DB 获取 clientId 并将其存储在缓存中,因此我不必每次需要时都调用 DB。 我不知道是否有更好的方法来获取客户端 ID,或者更好地存储它。

编辑
经过调查,我在 DbCONtext 中创建了一个变量,并在 Startup.cs 文件中对其进行了初始化。

    public class MyDataContext : IdentityDbContext<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>
{
    public static string ClientId { get; set; }

    public MyDataContext() : base("MyDataBase") { }

    public static MyDataContext Create()
    {
        return new myDataContext();
    }
    ....
}

在 Startup.cs 中

    public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        MyDataContext.ClientId = ClientConfiguration.GetCurrentClientId();

        ConfigureAuth(app);
    }
}

问题 2
获得 ClientId 后,我需要为每个需要它的查询添加一个过滤器。手动执行此操作可能会使您犯很多错误或在某些地方忘记执行此操作。

我需要一种方法,应用程序可以自动将过滤器添加到所有查询(仅限那些需要它的实体),因此我不必担心客户端获取其他客户端的数据。我还需要将 ClientId 添加到所有插入和更新命令中。

我已阅读过有关过滤和/或使用 EF 拦截器的信息,但在阅读了一些有关该内容的帖子后,我不知道该怎么做。在这里需要一些帮助。

提前致谢。

编辑

为了解决问题 2,我关注了 Xabikos 的这篇精彩文章: http://xabikos.com/2014/11/17/Create-a-multitenant-application-with-Entity-Framework-Code-First-Part-1/

我对其进行了一些更改,因为我不使用用户来获取当前租户,而是使用主机。这是程序的一部分,我还不知道我将如何解决,但假设我已经有了 ClientId,我可以为所有查询添加过滤器,而不会意识到正在发生这种情况:

我已经替换了所有的用户逻辑:

    private static void SetTenantParameterValue(DbCommand command)
    {
        if (MyDataContext.ClientId == 0) return;

        foreach (DbParameter param in command.Parameters)
        {
            if (param.ParameterName != TenantAwareAttribute.TenantIdFilterParameterName)
                continue;
            param.Value = MyDataContext.ClientId;
        }
    }

所有地方都一样...

我只需要标记必须使用 TenantAware 过滤的实体,指示属性。在这种情况下,我在我的基类中做,然后将该基类应用于我需要的所有实体。

[TenantAware("ClientId")]
public abstract class ClientEntity : Entity, IClientEntity
{
    public int ClientId { get; set; }
    public Client Client { get; set; }
}

【问题讨论】:

    标签: entity-framework filter multi-tenant interceptor


    【解决方案1】:

    以下是我过去做过的几件事可能会有所帮助。

    问题 1: 我不是 session 的忠实粉丝,因为网络应该是无状态的。但是,有时它是必要的。你的方法是合理的。您也可以使用 cookie。我使用的是通过我的身份验证提供程序 (Auth0.com) 的 Json Web 令牌 (JWT)。对于经过身份验证的每个请求,我都会查找此客户端 ID。这是一个例子。这也是 MVC 6。您可以使用 cookie 做同样类型的事情。

    public class Auth0ClaimsTransformer : IClaimsTransformer
        {
    
            private string _accountId = AdminClaimType.AccountId.DefaultValue;
            private string _clientId = AdminClaimType.ClientId.DefaultValue;
            private string _isActive = AdminClaimType.IsActive.DefaultValue;
    
            public Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
            {
                foreach (var claim in context.Principal.Claims)
                {
                    switch (claim.Type)
                    {
                        case "accountId":
                            _accountId = claim.Value ?? _accountId;
                            break;
                        case "clientId":
                            _clientId = claim.Value ?? _clientId;
                            break;
                        case "isActive":
                            _isActive = claim.Value ?? _isActive;
                            break;
                    }
                }
                ((ClaimsIdentity)context.Principal.Identity)
                    .AddClaims(new Claim[]
                    {
                        new Claim(AdminClaimType.AccountId.DisplayName, _accountId),
                        new Claim(AdminClaimType.ClientId.DisplayName, _clientId), 
                        new Claim(AdminClaimType.IsActive.DisplayName, _isActive)
                    });
    
                return Task.FromResult(context.Principal);
            }
    

    然后在我的 Startup.cs 配置方法中插入我的声明转换器。

    app.UseJwtBearerAuthentication(options);
    
        app.UseClaimsTransformation(new ClaimsTransformationOptions
        {
            Transformer = new Auth0ClaimsTransformer()
        });
    

    接下来我使用一个基本身份验证控制器,它将我的声明解析为我可以在我的控制器中使用的属性。

    [Authorize]
        [Route("api/admin/[controller]")]
        public class BaseAdminController : Controller
        {
            private long _accountId;
            private long _clientId;
            private bool _isActive;
    
            protected long AccountId
            {
                get
                {
                    var claim = GetClaim(AdminClaimType.AccountId);
                    if (claim == null)
                        return 0;
    
                    long.TryParse(claim.Value, out _accountId);
                    return _accountId;
                }
            }
    
            public long ClientId
            {
                get
                {
                    var claim = GetClaim(AdminClaimType.ClientId);
                    if (claim == null)
                        return 0;
    
                    long.TryParse(claim.Value, out _clientId);
                    return _clientId;
                }
            }
    
            public bool IsActive
            {
                get
                {
                    var claim = GetClaim(AdminClaimType.IsActive);
                    if (claim == null)
                        return false;
    
                    bool.TryParse(claim.Value, out _isActive);
                    return _isActive;
                }
            }
    
            public string Auth0UserId
            {
                get
                {
                    var claim = User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
                    return claim == null ? string.Empty : claim.Value;
                }
            }
    
            private Claim GetClaim(AdminClaimType claim)
            {
                return User.Claims.FirstOrDefault(x => x.Type == claim.DisplayName);
            }
    

    最后,在我的控制器中,提取正在拨打电话的租户是微不足道的。例如

    public FooController : BaseController
    {
     public async Task<IActionResult> Get(int id)
     {
       var foo = await _fooService.GetMultiTenantFoo(ClientId, id);
       return Ok(foo);
     }
    }
    

    问题 2: 我过去使用的一种方法是创建一个 BaseMultiTenant 类。

    public class BaseMultiTenant
    { 
      public int ClientId {get;set;}
    
      public virtual Client Client {get;set;}//if you are using EF
    }
    
    public class ClientHost : BaseMultiTenant
    {
      public string Name {get;set;}
      //etc
    }
    

    然后简单地为基于多租户的实体创建一个扩展方法。我知道这不会“自动执行”,但它是一种确保每个多租户实体仅由其所有者调用的简单方法。

    public static IQueryable<T> WhereMultiTenant<T>(this IQueryable<T> entity, int clientId, Expression<Func<T, bool>> predicate)
        where T : BaseMultiTenant
    {
        return entity.Where(x => x.ClientId == clientId)
                     .Where(predicate);
    }
    

    那么当有人要求他们的资源时,你可以:

    var clientHost = _myContext.ClientHosts
                               .WhereMultiTenant(ClientId, 
                                  x => x.Name == "foo")
                               .FirstOrDefault();
    

    希望这有帮助。

    还使用接口找到了similar example

    【讨论】:

    • 谢谢@treavorc,请查看我对问题 2 的解决方案。关于问题 1 我仍在考虑如何去做,因为我需要更改我所做的,因为如果我有它是无效的同一实例中的不同域,因为它仅在启动时获取 ClientId。
    • 这就是我推荐 cookie 或 JWT 类型方法的原因。我遗漏的是,当用户登录时,我会运行配置服务。该服务查看用户是否已注册,获取所有需要的值,并调用我的身份验证服务器 (Auth0)。每次用户提出请求时,我需要的值都在请求中。你可以用 cookie 做同样的事情。在注册和登录时创建一个带有客户端 ID 的 cookie。每次该用户提出请求时,他都必须拥有 cookie。然后,您可以读取 cookie 内容并使用我提供的方法来存储它们。
    猜你喜欢
    • 1970-01-01
    • 2011-01-04
    • 2020-09-04
    • 1970-01-01
    • 2020-02-02
    • 1970-01-01
    • 1970-01-01
    • 2016-09-24
    • 2020-06-26
    相关资源
    最近更新 更多