【问题标题】:ASP.NET Identity regenerats Identity at each requestASP.NET Identity 在每次请求时重新生成 Identity
【发布时间】:2015-03-31 21:00:53
【问题描述】:

我有一个 ASP.NET MVC 应用程序,我正在使用 ASP.NET Identity 2。我遇到了一个奇怪的问题。 ApplicationUser.GenerateUserIdentityAsync 被浏览器向我的网站发出的每个请求调用。我添加了一些Trace.WriteLine,这是删除 IIS 输出后的结果:

IdentityConfig.Configuration called
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Content/bootstrap.css
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Scripts/modernizr-2.8.3.js
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Content/site.css
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Scripts/jquery-2.1.3.js
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Scripts/bootstrap.js
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Scripts/respond.js
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Scripts/script.js
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Glimpse.axd?n=glimpse_client&hash=8913cd7e
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Glimpse.axd?n=glimpse_metadata&hash=8913cd7e&callback=glimpse.data.initMetadata
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Glimpse.axd?n=glimpse_request&requestId=6171c2b0-b6e5-4495-b495-4fdaddbe6e8f&hash=8913cd7e&callback=glimpse.data.initData
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Glimpse.axd?n=glimpse_sprite&hash=8913cd7e
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/__browserLink/requestData/38254292a54f4595ad26158540adbb6a?version=2

如果我运行由模板创建的默认 MVC 应用程序,我会得到:

IdentityConfig.Configuration called

只有当我登录时,它才会调用ApplicationUser.GenerateUserIdentityAsync

我找遍了所有我认为可能存在的地方,但没有找到任何结果。我正在使用(如果有帮助)

StructureMap 3
Elmah
Glimpse
ASP.NET MVC 5
EF6
ASP.NET Identity 2

其他信息

我将用户直接添加到数据库中,而不使用 UserManage。我不确定它是否对身份有任何问题。


更新

我已经删除了数据库,但它不再发生了。发生了什么?

更新 2

它发生在我的谷歌浏览器中(我使用 glimpse 监控 SQL 连接),在删除存储的 cookie 后,它没有发生。用其他浏览器登录会不会出现这个问题?

更新 3

同时注销 - 登录似乎可以暂时解决问题。

【问题讨论】:

  • 这可能无济于事,只是一个想法。您是否尝试在发布模式而不是调试模式下运行?调试模式可能会进行大量额外调用
  • @ScottSelby 好笑!我已经在发布模式下运行它,一切都很好。然后我将它发布到我的服务器上,它又发生了:|
  • ASP.NET Identity 是否使用会话?这听起来像是在请求之间没有保留会话 ID 的情况。如果是这样,请尝试设置一个虚拟会话变量。
  • @SimonSvensson 看来 ASP.NET Identity 不使用会话。

标签: c# asp.net asp.net-mvc iis asp.net-identity


【解决方案1】:

我遇到了同样的问题,在挖掘了源代码和一些侦探工作后,我找到了解决方案。问题出在SecurityStampValidator 内部,它被用作默认的OnValidateIdentity 处理程序。见源代码here。有趣的部分:

var issuedUtc = context.Properties.IssuedUtc;

// Only validate if enough time has elapsed
var validate = (issuedUtc == null);
if (issuedUtc != null)
{
    var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
    validate = timeElapsed > validateInterval;
}

这部分针对每个请求运行,如果validate 为真,则调用getUserIdCallbackregenerateIdentityCallback(在您的跟踪输出中可见)。这里的问题是issuedUtc 始终是cookie 的创建日期,所以validatevalidateInterval 过去后始终为真。这解释了您遇到的奇怪行为。如果validateInterval 是 10 分钟,则验证逻辑将在 cookie 创建后 10 分钟或更长时间内为每个请求运行(应用程序部署、cookie 清除、注销和重新登录时 cookie 重置)。

SecurityStampValidator 应该根据之前的验证日期(或首次检查时的签发日期)来决定是否验证,但它没有这样做。要使 issuedUtc 日期向前移动,有 3 种可能的解决方案:

  • 重置每个validateInterval的cookie,这意味着SingOutSignIn。类似的解决方案here。这似乎是一项代价高昂的操作,尤其是在 validateInterval 设置为仅几分钟的情况下。
  • 利用CookieAuthenticationOptions.SlidingExpiration 逻辑自动重新发布cookie。 this post 解释得很好。

如果 SlidingExpiration 设置为 true,那么在 ExpireTimeSpan 中途的任何请求都会重新发出 cookie。例如,如果用户登录并在 16 分钟后发出第二个请求,则 cookie 将在另外 30 分钟内重新发出。如果用户登录并在 31 分钟后发出第二次请求,则会提示用户登录。

在我的情况下(Intranet 应用程序)用户在 30 分钟不活动后退出是不可接受的。我需要有默认的ExpireTimeSpan,即 14 天。所以这里的选择是实现某种 ajax 轮询来延长 cookie 的寿命。听起来要付出很多努力才能完成这个相当简单的场景。

  • 我最终选择使用的最后一个选项是修改SecurityStampValidator 实现以具有滑动验证方法。下面的示例代码。请记住在 Startup.Auth.cs 中将 SecurityStampValidator 替换为 SlidingSecurityStampValidator。我在原始实现中添加了IdentityValidationDates 字典来存储每个用户的验证日期,然后在检查是否需要验证时使用它。

    public static class SlidingSecurityStampValidator
    {
        private static readonly IDictionary<string, DateTimeOffset> IdentityValidationDates = new Dictionary<string, DateTimeOffset>();
    
        public static Func<CookieValidateIdentityContext, Task> OnValidateIdentity<TManager, TUser, TKey>(
            TimeSpan validateInterval, Func<TManager, TUser, Task<ClaimsIdentity>> regenerateIdentityCallback,
            Func<ClaimsIdentity, TKey> getUserIdCallback)
            where TManager : UserManager<TUser, TKey>
            where TUser : class, IUser<TKey>
            where TKey : IEquatable<TKey>
        {
            if (getUserIdCallback == null)
            {
                throw new ArgumentNullException(nameof(getUserIdCallback));
            }
    
            return async context =>
            {
                var currentUtc = DateTimeOffset.UtcNow;
                if (context.Options != null && context.Options.SystemClock != null)
                {
                    currentUtc = context.Options.SystemClock.UtcNow;
                }
                var issuedUtc = context.Properties.IssuedUtc;
    
                // Only validate if enough time has elapsed
                var validate = issuedUtc == null;
                if (issuedUtc != null)
                {
                    DateTimeOffset lastValidateUtc;
                    if (IdentityValidationDates.TryGetValue(context.Identity.Name, out lastValidateUtc))
                    {
                        issuedUtc = lastValidateUtc;
                    }
    
                    var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
                    validate = timeElapsed > validateInterval;
                }
    
                if (validate)
                {
                    IdentityValidationDates[context.Identity.Name] = currentUtc;
    
                    var manager = context.OwinContext.GetUserManager<TManager>();
                    var userId = getUserIdCallback(context.Identity);
                    if (manager != null && userId != null)
                    {
                        var user = await manager.FindByIdAsync(userId);
                        var reject = true;
    
                        // Refresh the identity if the stamp matches, otherwise reject
                        if (user != null && manager.SupportsUserSecurityStamp)
                        {
                            var securityStamp = context.Identity.FindFirstValue(Constants.DefaultSecurityStampClaimType);
                            if (securityStamp == await manager.GetSecurityStampAsync(userId))
                            {
                                reject = false;
                                // Regenerate fresh claims if possible and resign in
                                if (regenerateIdentityCallback != null)
                                {
                                    var identity = await regenerateIdentityCallback.Invoke(manager, user);
                                    if (identity != null)
                                    {
                                        // Fix for regression where this value is not updated
                                        // Setting it to null so that it is refreshed by the cookie middleware
                                        context.Properties.IssuedUtc = null;
                                        context.Properties.ExpiresUtc = null;
                                        context.OwinContext.Authentication.SignIn(context.Properties, identity);
                                    }
                                }
                            }
                        }
    
                        if (reject)
                        {
                            context.RejectIdentity();
                            context.OwinContext.Authentication.SignOut(context.Options.AuthenticationType);
                        }
                    }
                }
            };
        }
    }
    

【讨论】:

  • 感谢您的回答。尽管我不再从事该项目,但您的答案似乎正确,因此我将其接受了答案
【解决方案2】:

有两个可能导致此重置的问题:

  1. Cookie IssuedUtc Date 无效(Cookie 上的issuedUtc 属性为null
  2. 身份验证间隔设置不正确。

在您的startup.cs 类中,您将找到Cookie 身份验证配置。配置内部是一个函数委托,应该如下设置:

public partial class Startup
{
    public void ConfigureAuth(IAppBuilder app)
    {
        ...

        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login"),
            Provider = new CookieAuthenticationProvider
            {
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                    validateInterval: TimeSpan.FromMinutes(30),
                    regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
            }
        });            

        ...
    }
}

检查您的validateInterval 是否设置为低值。在上述情况下,将在发出有效 cookie 30 分钟后调用数据库 (user.GenerateUserIdentityAsync)。在您的情况下,它可能会设置为像每秒一样的低值。

如果您使用到处注销功能(安全标记),更改 validateInterval 将允许 cookie 保持有效,直到调用 OnValidateIdentity 函数。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-12-21
    • 1970-01-01
    • 2015-04-13
    • 1970-01-01
    • 1970-01-01
    • 2014-01-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多