【问题标题】:Handling data access in multi tenant site处理多租户站点中的数据访问
【发布时间】:2012-07-15 05:49:38
【问题描述】:

我希望能提供一些关于基于 MVC 的多租户站点中的数据访问/控制的建议:

是否有更好/更安全/优雅的方法来确保在多租户站点中用户只能处理自己的数据。 有多个租户使用相同的应用程序:firstTenant.myapp.com, secondTenant.myapp.com...

    //
    // GET: /Customer/
    // show this tenant's customer info only

    public ViewResult Index()
    {
        //get TenantID from on server cache
        int TenantID =  Convert.ToInt16( new AppSettings()["TenantID"]);
        return View(context.Customers.ToList().Where(c => c.TenantID == TenantID));
    }

如果用户第一次登录并且没有此租户/用户的服务器端缓存 - AppSettings 会检查 db 并将 TenantID 存储在缓存中。

数据库中的每个表都包含 TenantID 字段,用于将数据的访问权限限制为只有适当的租户。

那么,言归正传,如果数据属于当前租户,我可以做一些更“高效”的事情,而不是检查每个控制器中的每个操作吗?

例子:

当 firstTenant 管理员尝试为用户 4 编辑一些信息时,url 有: http://firstTenant.myapp.com/User/Edit/4

假设 ID 为 2 的用户属于 secondTenant。 firstTenant 的管理员 put http://firstTenant.myapp.com/User/Edit/2 在 url 中,并尝试获取不属于他的公司的信息。

为了防止在控制器中发生这种情况,我检查正在编辑的信息是否实际上由当前租户拥有。

    //
    // GET: /User/Edit/

    public ActionResult Edit(int id)
    {
        //set tennant ID
        int TenanatID = Convert.ToInt32(new AppSettings()["TenantID"]);
        //check if asked info is actually owned by this tennant
        User user = context.Userss.Where(u => u.TenantID == TenantID).SingleOrDefault(u => u.UserID == id);

        //in case this tenant doesn't have this user ID, ie.e returned User == null
        //something is wrong, so handle bad request
        //

        return View(user);
    }

基本上这种集合需要放置在每个可以访问任何数据的控制器中。是否有(以及如何)更好的方法来处理这个问题? (过滤器、属性...)

【问题讨论】:

    标签: c# asp.net-mvc multi-tenant


    【解决方案1】:

    我选择使用操作过滤器来执行此操作。它可能不是最优雅的解决方案,但它是我们迄今为止尝试过的最干净的解决方案。

    我将租户(在我们的例子中是一个团队)保留在 URL 中,如下所示:https://myapp.com/{team}/tasks/details/1234

    我使用自定义绑定将 {team} 映射到实际的 Team 对象,因此我的操作方法如下所示:

    [AjaxAuthorize, TeamMember, TeamTask("id")]
    public ActionResult Details(Team team, Task id)
    

    TeamMember 属性验证当前登录的用户确实属于团队。它还验证团队确实存在:

    public class TeamMemberAttribute : ActionFilterAttribute
    {
      public override void OnActionExecuting(ActionExecutingContext filterContext)
      {
        base.OnActionExecuting(filterContext);
        var httpContext = filterContext.RequestContext.HttpContext;
    
        Team team = filterContext.ActionParameters["team"] as Team;
        long userId = long.Parse(httpContext.User.Identity.Name);
    
        if (team == null || team.Members.Where(m => m.Id == userId).Count() == 0)
        {
            httpContext.Response.StatusCode = 403;
            ViewResult insufficientPermssions = new ViewResult();
            insufficientPermssions.ViewName = "InsufficientPermissions";
            filterContext.Result = insufficientPermssions;
        }
      }
    }
    

    同样,TeamTask 属性确保相关任务实际上属于团队。

    【讨论】:

    • 感谢 Rages,我将使用过滤器以及在服务器上缓存 TenantID 数据,这样我就不会每次都访问数据库。 :)
    【解决方案2】:

    由于我的应用使用子域(sub1.app.com、sub2.app.com.....),我基本上选择:

    a) 使用类似下面的代码来缓存租户信息和

    b) 按照 Ragesh & Doc 的建议在每个控制器上调用操作过滤器:

    (以下代码来自博客:http://www.developer.com/design/article.php/10925_3801931_2/Introduction-to-Multi-Tenant-Architecture.htm

    // <summary>
    // This class is used to manage the Cached AppSettings
    // from the Database
    // </summary>
    public class AppSettings
    {
    // <summary>
    // This indexer is used to retrieve AppSettings from Memory
    // </summary>
    public string this[string Name]
    {
      get
      {
         //See if we have an AppSettings Cache Item
         if (HttpContext.Current.Cache["AppSettings"] == null)
         {
            int? TenantID = 0;
            //Look up the URL and get the Tenant Info
            using (ApplContext dc =
               new ApplContext())
            {
               Site result =
                      dc.Sites
                      .Where(a => a.Host ==
                         HttpContext.Current.Request.Url.
                            Host.ToLower())
                      .FirstOrDefault();
               if (result != null)
               {
                  TenantID = result.SiteID;
               }
            }
            AppSettings.LoadAppSettings(TenantID);
         }
    
         Hashtable ht =
           (Hashtable)HttpContext.Current.Cache["AppSettings"];
         if (ht.ContainsKey(Name))
         {
            return ht[Name].ToString();
         }
         else
         {
            return string.Empty;
         }
      }
    }
    
    // <summary>
    // This Method is used to load the app settings from the
    // database into memory
    // </summary>
    public static void LoadAppSettings(int? TenantID)
    {
      Hashtable ht = new Hashtable();
    
      //Now Load the AppSettings
      using (ShoelaceContext dc =
         new ShoelaceContext())
      {
    
          //settings are turned off
          // no specific settings per user needed currently
         //var results = dc.AppSettings.Where(a =>
         //   a.in_Tenant_Id == TenantID);
    
         //foreach (var appSetting in results)
         //{
         //   ht.Add(appSetting.vc_Name, appSetting.vc_Value);
         //}
                    ht.Add("TenantID", TenantID);
    
      }
    
      //Add it into Cache (Have the Cache Expire after 1 Hour)
      HttpContext.Current.Cache.Add("AppSettings",
         ht, null,
         System.Web.Caching.Cache.NoAbsoluteExpiration,
         new TimeSpan(1, 0, 0),
         System.Web.Caching.CacheItemPriority.NotRemovable, null);
    
         }
      }
    

    【讨论】:

      【解决方案3】:

      如果你想在Controller中的每一个Action上执行这样的通用代码,你可以这样做:

      protected override void OnActionExecuting(ActionExecutingContext filterContext)
      {
          base.OnActionExecuting(filterContext);
          // do your magic here, you can check the session and/or call the database
      }
      

      【讨论】:

      • 那很好。但是我将如何设置它以便可以检查不同的表(具有不同的键)。也许通过创建 [CheckSiteAccessAttribute("table (or class) as parameter", "class.KeyId as second parameter")] 并在属性代码中检查给定的参数(类,键)是否分配了适当的 TenantID.... ..
      • 是的,这可能是最好的方法。你可以用不同的参数装饰单个动作,猜我第一次没有仔细阅读你的问题。 :)
      【解决方案4】:

      我们还使用 ASP.NET MVC 开发了一个多租户应用程序,并且在每个查询中包含租户 ID 是完全可以接受且非常必要的事情。我不确定你在哪里托管你的应用程序,但如果你可以使用 SQL Azure,他们有一个名为 Federations 的新产品,可以让你轻松管理多租户数据。一个不错的功能是,当您打开连接时,您可以指定租户 ID,此后执行的所有查询都只会影响租户数据。它本质上只是在您的每个请求中包含他们的租户 ID,因此您不必手动执行此操作。 (请注意,联合数据并不是一个新概念,微软最近刚刚发布了他们自己的实现)

      【讨论】:

      • 感谢尼克提供的信息。计划是一旦我们接触到更多的客户群,就将整个应用程序转移(并开始转移)到 Azure。 :)
      猜你喜欢
      • 2011-12-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-14
      • 2018-07-04
      • 2013-07-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多