【问题标题】:Dynamic database connection using Asp.Net identity使用 Asp.Net 身份的动态数据库连接
【发布时间】:2019-03-16 05:24:54
【问题描述】:

我正在开发一个使用多个数据库的多租户应用程序。有一个包含用户信息的主数据库,然后每个租户数据库也有自己的该租户的用户(它们是主数据库中用户的子集)。

用户登录后会检查主数据库,然后根据他们的详细信息(即他们属于哪个租户),它将使用租户数据库上的用户详细信息将他们登录到应用程序中。

我每次都使用该线程中描述的方法 (Dynamic database connection using Asp.net MVC and Identity2) 为 UserManager 设置数据库,因为在应用程序启动时它不知道要使用哪个数据库,因此“Startup.Auth”中的以下代码" 将设置不正确的数据库:

app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

这似乎对大多数事情都很好,但我遇到的一个问题是用户在下面代码中显示的“validateInterval”中设置的时间之后退出(这已设置为 20 秒以进行测试):

 app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login"),
            Provider = new CookieAuthenticationProvider
            {
                // Enables the application to validate the security stamp when the user logs in.
                // This is a security feature which is used when you change a password or add an external login to your account.  
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                    validateInterval: TimeSpan.FromSeconds(20),
                    regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)),                        
                OnApplyRedirect = ctx =>
                {
                    if (!IsAjaxRequest(ctx.Request))
                    {
                        ctx.Response.Redirect(ctx.RedirectUri);
                    }
                }
            }
        });

我认为问题可能是因为在“Startup.Auth”文件中调用上面的代码时,它不知道要使用哪个数据库但是我没有确认这一点。

如果我调试“GenerateUserIdentityAsync”代码,我可以看到它正在从客户端数据库中为用户获取正确的“securityStamp”,这让我认为它正在找到正确的数据库,但我无法弄清楚为什么它仍在记录在为“validateInterval”设置的时间之后退出用户。

任何人都可以就如何解决此问题或至少尝试调试问题的可能方法提供任何建议吗?

【问题讨论】:

    标签: asp.net-mvc entity-framework asp.net-identity multi-tenant multi-database


    【解决方案1】:

    我的多租户 ASP.NET MVC 应用程序也遇到了同样的问题。 如果您的目标是为登录用户设置过期时间,只需删除 CookieAuthenticationProvider 中的代码并在父 CookieAuthenticationOptions 中设置 ExpireTimeSpan 属性。

    你的代码应该是:

    app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                ExpireTimeSpan = TimeSpan.FromMinutes(15), //cookie expiration after 15 mins of user inactivity
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Account/Login"),
                Provider = new CookieAuthenticationProvider
                {
                }
            });
    

    希望这会有所帮助。

    【讨论】:

    • 目标不是为登录用户设置过期时间,而是允许它在安全标记已更改(即他们在另一个浏览器中更改密码)时将用户注销。但是,由于您似乎遇到了同样的问题并且没有找到解决方案,那么我一直在研究的可能是唯一的选择,我基本上已经按照您的建议删除了代码,并开始研究在每个请求上运行的 IActionFilter 并检查当前登录用户的安全戳记对照数据库中的内容,如果不同,则将其注销。
    【解决方案2】:

    好的,这是我提出的完整解决方案,它部分使用了@jacktric 建议的内容,但如果用户密码已在其他地方更改,还允许验证安全标记。请让我知道是否有人可以推荐任何改进或发现我的解决方案中的任何缺陷。

    我已从 UseCookieAuthentication 部分中删除了 OnValidateIdentity 部分,如下所示:

    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/Account/Login"),
        Provider = new CookieAuthenticationProvider
        {
            OnApplyRedirect = ctx =>
            {
                if (!IsAjaxRequest(ctx.Request))
                {
                    ctx.Response.Redirect(ctx.RedirectUri);
                }
            }
        }
    });
    

    然后,我在 FilterConfig.cs 中注册了以下 IActionFilter,它检查用户是否已登录(我有匿名用户可以访问的系统部分)以及当前的安全标记是否与来自数据库。使用会话每 30 分钟进行一次检查,以确定上次检查的时间。

    public class CheckAuthenticationFilter : IActionFilter
    {
        public void OnActionExecuting(ActionExecutingContext filterContext)
        {
    
        }
    
        public void OnActionExecuted(ActionExecutedContext filterContext)
        {
            try
            {
                // If not a child action, not an ajax request, not a RedirectResult and not a PartialViewResult
                if (!filterContext.IsChildAction
                    && !filterContext.HttpContext.Request.IsAjaxRequest()
                    && !(filterContext.Result is RedirectResult)
                    && !(filterContext.Result is PartialViewResult))
                {
                    // Get current ID
                    string currentUserId = filterContext.HttpContext.User.Identity.GetUserId();
    
                    // If current user ID exists (i.e. it is not an anonymous function)
                    if (!String.IsNullOrEmpty(currentUserId))
                    {
                        // Variables
                        var lastValidateIdentityCheck = DateTime.MinValue;
                        var validateInterval = TimeSpan.FromMinutes(30);
                        var securityStampValid = true;
    
                        // Get instance of userManager   
                        filterContext.HttpContext.GetOwinContext().Get<DbContext>().Database.Connection.ConnectionString = DbContext.GetConnectionString();
                        var userManager = filterContext.HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
    
                        // Find current user by ID
                        var currentUser = userManager.FindById(currentUserId);
    
                        // If "LastValidateIdentityCheck" session exists
                        if (HttpContext.Current.Session["LastValidateIdentityCheck"] != null)
                            DateTime.TryParse(HttpContext.Current.Session["LastValidateIdentityCheck"].ToString(), out lastValidateIdentityCheck);
    
                        // If first validation or validateInterval has passed
                        if (lastValidateIdentityCheck == DateTime.MinValue || DateTime.Now > lastValidateIdentityCheck.Add(validateInterval))
                        {
                            // Get current security stamp from logged in user
                            var currentSecurityStamp = filterContext.HttpContext.User.GetClaimValue("AspNet.Identity.SecurityStamp");
    
                            // Set whether security stamp valid
                            securityStampValid = currentUser != null && currentUser.SecurityStamp == currentSecurityStamp;
    
                            // Set LastValidateIdentityCheck session variable
                            HttpContext.Current.Session["LastValidateIdentityCheck"] = DateTime.Now;
                        }
    
                        // If current user doesn't exist or security stamp invalid then log them off 
                        if (currentUser == null || !securityStampValid)
                        {
                            filterContext.Result = new RedirectToRouteResult(
                                    new RouteValueDictionary { { "Controller", "Account" }, { "Action", "LogOff" }, { "Area", "" } });
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                // Log error                
            }
        }
    }
    

    我有以下扩展方法来获取和更新登录用户的声明(取自这篇帖子https://stackoverflow.com/a/32112002/1806809):

    public static void AddUpdateClaim(this IPrincipal currentPrincipal, string key, string value)
    {
        var identity = currentPrincipal.Identity as ClaimsIdentity;
        if (identity == null)
            return;
    
        // Check for existing claim and remove it
        var existingClaim = identity.FindFirst(key);
        if (existingClaim != null)
            identity.RemoveClaim(existingClaim);
    
        // Add new claim
        identity.AddClaim(new Claim(key, value));
    
        // Set connection string - this overrides the default connection string set 
        // on "app.CreatePerOwinContext(DbContext.Create)" in "Startup.Auth.cs"
        HttpContext.Current.GetOwinContext().Get<DbContext>().Database.Connection.ConnectionString = DbContext.GetConnectionString();
        var authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
        authenticationManager.AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(identity), new AuthenticationProperties() { IsPersistent = true });
    }
    
    public static string GetClaimValue(this IPrincipal currentPrincipal, string key)
    {
        var identity = currentPrincipal.Identity as ClaimsIdentity;
        if (identity == null)
            return null;
    
        var claim = identity.Claims.FirstOrDefault(c => c.Type == key);
        return claim.Value;
    }
    

    最后,在用户密码更新的任何地方,我都会调用以下命令,这会更新正在编辑密码的用户的安全标记,如果正在编辑的是当前登录的用户密码,那么它会更新 securityStamp 声明对于当前用户,以便他们在下次进行有效性检查时不会退出当前会话:

    // Update security stamp
    UserManager.UpdateSecurityStamp(user.Id);
    
    // If updating own password
    if (GetCurrentUserId() == user.Id)
    {
        // Find current user by ID
        var currentUser = UserManager.FindById(user.Id);
    
        // Update logged in user security stamp (this is so their security stamp matches and they are not signed out the next time validity check is made in CheckAuthenticationFilter.cs)
        User.AddUpdateClaim("AspNet.Identity.SecurityStamp", currentUser.SecurityStamp);
    }
    

    【讨论】:

      猜你喜欢
      • 2016-07-02
      • 1970-01-01
      • 2018-05-05
      • 1970-01-01
      • 2012-06-15
      • 2019-10-12
      • 2012-05-22
      • 2015-08-09
      • 1970-01-01
      相关资源
      最近更新 更多