我看到一个多年前就已经被问过的问题,它解释了与使用 AspNet Identity 时丢失自定义声明相关的问题。不幸的是,那里提到的解决方案对我不起作用,因为我在 .NET 6 Blazor Server 应用程序上使用 AspNet Core Identity。
问题类似(在下面几点解释):
我在登录期间添加了一些声明(这些声明来自某些 API 调用,而不是来自 Identity 数据库,因此我在登录期间添加它们)。
我可以从 Blazor 组件访问它们。
它在 30% 的情况下工作正常,但在 70% 的情况下,cookie 会丢失我在登录期间添加的自定义声明,并且我的应用程序会遇到问题。我什至无法弄清楚这些声明何时丢失,因为在
RevalidationInterval
期间也没有发生这种情况,因为我用 1 分钟的 TimeSpan 对其进行了测试,并且当我多次测试时它至少在 5 分钟内运行良好。搜索了一堆答案,但没有找到 AspNet Core Identity 的正确答案。
这就是我的代码的样子:
builder.Services
.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
// Set Password options here if you'd like:
options.Password.RequiredLength = 6;
})
.AddRoles<IdentityRole>()
.AddUserManager<ADUserManager<IdentityUser>>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<ApplicationUser>>();
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
if (!ModelState.IsValid) return Page();
try
{
var adLoginResult = ADHelper.ADLogin(Input.Username, Input.Password);
// Use adLoginResult data to populate custom claims here
// Set additional info about the user using empTimeId and other custom claims
var customClaims = new[]
{
new Claim("EmployeeTimeId", adLoginResult.TimeId)
};
// SignIn the user now
await _signInManager.SignInWithClaimsAsync(user, Input.RememberMe, customClaims);
return LocalRedirect(returnUrl);
}
catch (Exception ex)
{
ModelState.AddModelError(string.Empty, $"Login Failed. Error: {ex.Message}.");
return Page();
}
}
public class RevalidatingIdentityAuthenticationStateProvider<TUser>
: RevalidatingServerAuthenticationStateProvider where TUser : class
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly IdentityOptions _options;
public RevalidatingIdentityAuthenticationStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory,
IOptions<IdentityOptions> optionsAccessor)
: base(loggerFactory)
{
_scopeFactory = scopeFactory;
_options = optionsAccessor.Value;
}
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(1); // More frequent for ease of testing
protected override async Task<bool> ValidateAuthenticationStateAsync(AuthenticationState authenticationState, CancellationToken cancellationToken)
{
//Get the user manager from a new scope to ensure it fetches fresh data
var scope = _scopeFactory.CreateScope();
try
{
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TUser>>();
return await ValidateSecurityTimeStampAsync(userManager, authenticationState.User);
}
finally
{
if(scope is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
private async Task<bool> ValidateSecurityTimeStampAsync(UserManager<TUser> userManager, ClaimsPrincipal principal)
{
var user = await userManager.GetUserAsync(principal);
if(user == null)
{
return false;
}
else if (!userManager.SupportsUserSecurityStamp)
{
return true;
}
else
{
var principalStamp = principal.FindFirstValue(_options.ClaimsIdentity.SecurityStampClaimType);
var userStamp = await userManager.GetSecurityStampAsync(user);
return principalStamp == userStamp;
}
}
}
public class UserInfoService
{
private readonly AuthenticationStateProvider _authenticationStateProvider;
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
public UserInfoService(AuthenticationStateProvider authenticationStateProvider, IDbContextFactory<ApplicationDbContext> dbContextFactory)
{
_authenticationStateProvider = authenticationStateProvider;
_dbContextFactory = dbContextFactory;
}
public async Task<UserInfoFromAuthState?> GetCurrentUserInfoFromAuthStateAsync()
{
var userInfo = new UserInfoFromAuthState();
var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
if (authState == null ||
authState.User == null ||
authState.User.Identity == null ||
!authState.User.Identity.IsAuthenticated)
{
return null;
}
userInfo.UserName = authState.User.Identity.Name!;
// This comes out to be null after sometime a user has logged in
userInfo.EmployeeTimeId = int.TryParse(authState.User.FindFirstValue("EmployeeTimeId", out var timeId) ? timeId : null;
return userInfo;
}
}
这就是当我的自定义声明为空时我面临的问题:
"EmployeeTimeId"
。
最终通过在 aspnetcore github 存储库中提出问题解决了这个问题。 https://github.com/dotnet/aspnetcore/issues/49610
非常感谢@halter73的帮助!
事实证明,每当 SecurityStamp 重新验证时,主体就会被替换(默认间隔每 30 分钟发生一次)。因此,我通过在校长刷新时重新添加声明来解决此问题。我使用选项模式来完成此任务。
在 Identity 文件夹下添加一个名为
ConfigureSecurityStampOptions.cs
的新类(或任何您想要的位置):
ConfigureSecurityStampOptions.cs
的内容应该是:
public class ConfigureSecurityStampOptions : IConfigureOptions<SecurityStampValidatorOptions>
{
public void Configure(SecurityStampValidatorOptions options)
{
options.ValidationInterval = TimeSpan.FromMinutes(1);
// When refreshing the principal, ensure custom claims that
// might have been set with an external identity continue
// to flow through to this new one.
options.OnRefreshingPrincipal = refreshingPrincipal =>
{
ClaimsIdentity? newIdentity = refreshingPrincipal.NewPrincipal?.Identities.First();
ClaimsIdentity? currentIdentity = refreshingPrincipal.CurrentPrincipal?.Identities.First();
if (currentIdentity is not null && newIdentity is not null)
{
// Since this is refreshing an existing principal, we want to merge all claims.
// Only work with claims in current identity that are not already present in the new identity with the same Type and Value.
var currentClaimsNotInNewIdentity = currentIdentity.Claims.Where(c => !newIdentity.HasClaim(c.Type, c.Value));
foreach (Claim claim in currentClaimsNotInNewIdentity)
{
newIdentity.AddClaim(claim);
}
}
return Task.CompletedTask;
};
}
}
在
Program.cs
注册
// To ensure custom claims are added to new identity when principal is refreshed.
builder.Services.ConfigureOptions<ConfigureSecurityStampOptions>();
https://github.com/affableashish/blazor-server-auth/tree/feature/AddClaimsDuringLogin
在 ASP.NET Core 中,登录期间添加的声明将添加到
ClaimsIdentity
并序列化到身份验证 cookie 中。如果 cookie 未设置为持久,则每当会话结束时声明都会丢失。如果 cookie 设置为持久,则当用户返回时,声明仍将存在,但只有在身份验证 cookie 失效或过期之前才会出现这种情况。
您的问题可能与
RevalidatingIdentityAuthenticationStateProvider
的重新验证间隔无关。它可能与身份验证 cookie 的生命周期及其管理方式有关。
以下是一些需要检查的事项:
检查会话和 Cookie 设置:根据您的环境和应用程序设置,会话或身份验证 Cookie 可能会比您预期的更早到期,从而导致声明丢失。检查您的 cookie 设置和会话超时设置,看看它们是否设置为对您的应用程序有意义的值。
由于滑动过期而丢失声明:在 ASP.NET Core 中,滑动过期会在每次发送回服务器时重置 cookie 过期时间,但前提是已经过了一半以上的时间。这有时会导致客户端的 cookie 过期,从而导致声明丢失。
要解决此问题,您可以增加身份验证 cookie 的过期时间或设置持久 cookie。
重新验证期间声明丢失: RevalidatingIdentityAuthenticationStateProvider 正在重新验证用户的安全标记。如果安全标记已更改,用户将被注销。因此,如果您的应用程序中的某些代码正在更改安全标记,则可能会导致您的用户注销,并且声明丢失。要检查这一点,请检查您的代码以查看是否有任何地方正在更新安全标记。
如果这些都没有帮助,创建一个小型的、可重现的示例来演示问题可能是个好主意。这将允许您系统地测试系统的每个部分并隔离导致问题的部分。它还会让其他人更容易帮助您调试问题。
请注意,在索赔中存储敏感信息可能并不是适合所有情况的最佳方法。声明对于不经常更改的授权数据非常有用,但对于经常更改的数据,通常最好在需要时直接从源中检索它。如果
EmployeeTimeId
值经常更改,您可能需要考虑将其存储在数据库中并在需要时检索它,而不是将其存储在声明中。