ASP.NET Identity 在每次请求时重新生成 Identity
ASP.NET Identity regenerats Identity at each request
我有一个 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
它发生在我的 Google Chrome 中(我使用 glimpse 监控 SQL 连接)并且在删除存储的 cookie 后,它没有发生。其他浏览器登录会不会出现这个问题?
更新 3
同时注销 - 登录似乎可以暂时解决问题。
有两个可能的问题导致此重置:
- Cookie IssuedUtc Date 无效(Cookie 上的
issuedUtc
属性 是 null
)
- 身份验证间隔设置不正确。
在您的 startup.cs
class 中,您将找到 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
函数被调用之前保持有效。
我遇到了同样的问题,在深入研究源代码和一些侦探工作后,我找到了解决方案。问题出在用作默认 OnValidateIdentity
处理程序的 SecurityStampValidator
内部。请参阅源代码 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;
}
这部分 运行s 对于每个请求,如果 validate
为真,则调用 getUserIdCallback
和 regenerateIdentityCallback
(在您的跟踪输出中可见)。这里的问题是 issuedUtc
始终是创建 cookie 的日期,因此 validate
在 validateInterval
过去后始终为真。这解释了您遇到的奇怪行为。如果 validateInterval
为 10 分钟,则在创建 cookie 后 10 分钟或更长时间后收到的每个请求的验证逻辑将为 运行(应用程序部署、cookie 清除、注销和重新登录时重置 cookie)。
SecurityStampValidator
应该根据之前的验证日期(或第一次检查时的签发日期)来决定是否验证,但它没有这样做。要使 issuedUtc
日期向前推进,有 3 种可能的解决方案:
- 每个
validateInterval
重置 cookie,这意味着 SingOut
和 SignIn
。类似的解决方案here。这似乎是一项代价高昂的操作,特别是如果 validateInterval
设置为仅几分钟。
- 利用
CookieAuthenticationOptions.SlidingExpiration
逻辑自动重新发布 cookie。在 this post. 中解释得很好
If SlidingExpiration is set to true then the cookie would be re-issued on any request half way through the ExpireTimeSpan. For example, if the user logged in and then made a second request 16 minutes later the cookie would be re-issued for another 30 minutes. If the user logged in and then made a second request 31 minutes later then the user would be prompted to log in.
在我的例子中(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);
}
}
}
};
}
}
我有一个 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
它发生在我的 Google Chrome 中(我使用 glimpse 监控 SQL 连接)并且在删除存储的 cookie 后,它没有发生。其他浏览器登录会不会出现这个问题?
更新 3
同时注销 - 登录似乎可以暂时解决问题。
有两个可能的问题导致此重置:
- Cookie IssuedUtc Date 无效(Cookie 上的
issuedUtc
属性 是null
) - 身份验证间隔设置不正确。
在您的 startup.cs
class 中,您将找到 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
函数被调用之前保持有效。
我遇到了同样的问题,在深入研究源代码和一些侦探工作后,我找到了解决方案。问题出在用作默认 OnValidateIdentity
处理程序的 SecurityStampValidator
内部。请参阅源代码 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;
}
这部分 运行s 对于每个请求,如果 validate
为真,则调用 getUserIdCallback
和 regenerateIdentityCallback
(在您的跟踪输出中可见)。这里的问题是 issuedUtc
始终是创建 cookie 的日期,因此 validate
在 validateInterval
过去后始终为真。这解释了您遇到的奇怪行为。如果 validateInterval
为 10 分钟,则在创建 cookie 后 10 分钟或更长时间后收到的每个请求的验证逻辑将为 运行(应用程序部署、cookie 清除、注销和重新登录时重置 cookie)。
SecurityStampValidator
应该根据之前的验证日期(或第一次检查时的签发日期)来决定是否验证,但它没有这样做。要使 issuedUtc
日期向前推进,有 3 种可能的解决方案:
- 每个
validateInterval
重置 cookie,这意味着SingOut
和SignIn
。类似的解决方案here。这似乎是一项代价高昂的操作,特别是如果validateInterval
设置为仅几分钟。 - 利用
CookieAuthenticationOptions.SlidingExpiration
逻辑自动重新发布 cookie。在 this post. 中解释得很好
If SlidingExpiration is set to true then the cookie would be re-issued on any request half way through the ExpireTimeSpan. For example, if the user logged in and then made a second request 16 minutes later the cookie would be re-issued for another 30 minutes. If the user logged in and then made a second request 31 minutes later then the user would be prompted to log in.
在我的例子中(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); } } } }; } }