使用 Asp.Net 身份的动态数据库连接

Dynamic database connection using Asp.Net identity

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

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

我每次都使用此线程 (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 应用程序中遇到了同样的问题。 如果您的目标是为登录用户设置过期时间,只需删除 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
            {
            }
        });

希望对您有所帮助。

好的,这是我想出的完整解决方案,它部分使用了@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                
        }
    }
}

我有以下扩展方法来获取和更新登录用户的声明(取自此 post ):

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);
}