英文:
Identity library - Check Enabled & re-read Claims on each page
问题
我正在使用ASP.NET Identity库在Blazor(服务器端)应用程序中。我遇到了这个问题,但我认为它是更大问题的一部分。
首先,如果我更改用户的声明,特别是如果我减少它们,我希望这些更改立即生效 - 无论用户的操作如何。毕竟,如果我说“请注销然后重新登录以使您的权限减少生效” - 是的,人们会立即这样做</sarcasm>。
我喜欢缓存登录,这样用户不需要每次访问网站时都要登录。我绝对希望保持这一点。但我希望这个级联参数 Task<AuthenticationState>
每次转到新页面时都重新读取声明(是的,它是一个SPA,但你知道我的意思 - 转到SPA中的新URL)。不是为每个新会话重新读取,而是为每个新页面重新读取。这样更改会立即生效。
另外,我将在AspNetUsers表和IdentityUser模型中添加一个Enable列。再次,我希望每次页面更改时都会检查这一点,以便 Task<AuthenticationState>
知道用户已禁用,因此 @attribute [Authorize]
将不允许页面加载/显示,如果用户被禁用。
那么,我如何实现这两个功能呢?
英文:
I am using the ASP.NET Identity library in a Blazor (server side) application. I'm hitting this problem but I think it's part of a larger problem.
First off, if I change the claims a user has, especially if I'm reducing them, I want those to take effect immediately - regardless of the user's actions. After all, if I say please log out and back in so your reduction in permissions takes effect - yeah people are going to get right on that </sarcasm>.
I like caching the login so the user does not need to log in every time they hit the website. I absolutely want to keep that. But I want that cascading parameter Task<AuthenticationState>
to re-read the claims every time it goes to a new page (yes it's a SPA but you know what I mean - goes to a new url in the SPA). Not re-read for each new session, but re-read for each new page. So that change takes effect immediately.
In addition I am going to add an Enable column to the AspNetUsers table and the IdentityUser model. And again, I want this checked every time the page changes so that Task<AuthenticationState>
knows the user is disabled and so @attribute [Authorize]
will not allow a page to load/display if the user is disabled.
So how do I implement both of these features?
答案1
得分: 0
我已经让它工作了。在 Program.cs 中添加以下内容:
// 在 AddServerSideBlazor() 后面添加以下代码
builder.Services.AddScoped<AuthenticationStateProvider, ExAuthenticationStateProvider>();
这是 ExAuthenticationStateProvider.cs 文件的内容(该文件还实现了处理 ExIdentityUser.Enabled):
public class ExAuthenticationStateProvider : ServerAuthenticationStateProvider
{
// 以 UTC 时间表示的下次检查时间(此次或之后)
private DateTime _nextCheck = DateTime.MinValue;
private readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(30);
private readonly UserManager<ExIdentityUser> _userManager;
public ExAuthenticationStateProvider(UserManager<ExIdentityUser> userManager, IConfiguration config)
{
_userManager = userManager;
var minutes = config.GetSection("Identity:RevalidateMinutes").Value;
if (!string.IsNullOrEmpty(minutes) && int.TryParse(minutes, out var intMinutes))
_checkInterval = TimeSpan.FromMinutes(intMinutes);
}
/// <summary>
/// 重置下次检查时间为 DateTime.MinValue。
/// </summary>
public void ResetNextCheck()
{
_nextCheck = DateTime.MinValue;
}
/// <inheritdoc />
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
// 如果距离上次检查不到 30 分钟,则只返回默认值
var now = DateTime.UtcNow;
if (now < _nextCheck)
return await base.GetAuthenticationStateAsync();
_nextCheck = now + _checkInterval;
// 如果没有进行身份验证,则只返回默认值
var authState = await base.GetAuthenticationStateAsync();
if (authState.User.Identity == null)
{
Trap.Break();
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
// 他们没有进行身份验证,因此返回我们拥有的内容。
if ((!authState.User.Identity.IsAuthenticated) || string.IsNullOrEmpty(authState.User.Identity.Name))
return new AuthenticationState(new ClaimsPrincipal(authState.User.Identity));
// 如果用户不在数据库中,则只返回默认值
var user = await _userManager.FindByNameAsync(authState.User.Identity.Name);
if (user == null)
{
Trap.Break();
return new AuthenticationState(new ClaimsPrincipal(authState.User.Identity));
}
// 禁用 - 因此是匿名用户(系统没有禁用用户的概念)
if (!user.Enabled)
{
var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
return new AuthenticationState(anonymousUser);
}
// 更新为最新的声明 - 仅在更改时(如果没有更改则不调用 NotifyAuthenticationStateChanged())
var listDatabaseClaims = (await _userManager.GetClaimsAsync(user)).ToList();
var listExistingClaims = authState.User.Claims.Where(claim => AuthorizeRules.AllClaimTypes.Contains(claim.Type)).ToList();
bool claimsChanged;
if (listExistingClaims.Count != listDatabaseClaims.Count)
claimsChanged = true;
else
claimsChanged = listExistingClaims.Any(claim => listDatabaseClaims.All(c => c.Type != claim.Type));
if (!claimsChanged)
return authState;
// 存在的身份,但具有新的声明
// ToList() 用于复制声明,以便我们可以读取现有的声明,然后从声明中删除
var claimsIdentity = new ClaimsIdentity(authState.User.Identity);
foreach (var claim in claimsIdentity.Claims.ToList().Where(claim => AuthorizeRules.AllClaimTypes.Contains(claim.Type)))
claimsIdentity.RemoveClaim(claim);
claimsIdentity.AddClaims(listDatabaseClaims);
// 将此设置为身份验证状态
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
authState = new AuthenticationState(claimsPrincipal);
SetAuthenticationState(Task.FromResult(authState));
// 返回现有或更新的状态
return authState;
}
}
请注意,这是您提供的代码的翻译部分。
英文:
I got it working. In Program.cs add:
// after AddServerSideBlazor()
builder.Services.AddScoped<AuthenticationStateProvider, ExAuthenticationStateProvider>();
And here's ExAuthenticationStateProvider.cs (which also implements handling ExIdentityUser.Enabled):
public class ExAuthenticationStateProvider : ServerAuthenticationStateProvider
{
// in UTC - when to do the next check (this time or later)
private DateTime _nextCheck = DateTime.MinValue;
private readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(30);
private readonly UserManager<ExIdentityUser> _userManager;
public ExAuthenticationStateProvider(UserManager<ExIdentityUser> userManager, IConfiguration config)
{
_userManager = userManager;
var minutes = config.GetSection("Identity:RevalidateMinutes").Value;
if (!string.IsNullOrEmpty(minutes) && int.TryParse(minutes, out var intMinutes))
_checkInterval = TimeSpan.FromMinutes(intMinutes);
}
/// <summary>
/// Revalidate the next time GetAuthenticationStateAsync() is called.
/// </summary>
public void ResetNextCheck()
{
_nextCheck = DateTime.MinValue;
}
/// <inheritdoc />
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
// if less than 30 minutes since last check, then just return the default
var now = DateTime.UtcNow;
if (now < _nextCheck)
return await base.GetAuthenticationStateAsync();
_nextCheck = now + _checkInterval;
// if we're not authenticated, then just return the default
var authState = await base.GetAuthenticationStateAsync();
if (authState.User.Identity == null)
{
Trap.Break();
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
// they're not authenticated so return what we have.
if ((!authState.User.Identity.IsAuthenticated) || string.IsNullOrEmpty(authState.User.Identity.Name))
return new AuthenticationState(new ClaimsPrincipal(authState.User.Identity));
// if the user is not in the database, then just return the default
var user = await _userManager.FindByNameAsync(authState.User.Identity.Name);
if (user == null)
{
Trap.Break();
return new AuthenticationState(new ClaimsPrincipal(authState.User.Identity));
}
// disabled - so anonymous user (system doesn't have the concept of disabled users)
if (!user.Enabled)
{
var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
return new AuthenticationState(anonymousUser);
}
// update to the latest claims - only if changed (don't want to call NotifyAuthenticationStateChanged() if nothing changed)
var listDatabaseClaims = (await _userManager.GetClaimsAsync(user)).ToList();
var listExistingClaims = authState.User.Claims.Where(claim => AuthorizeRules.AllClaimTypes.Contains(claim.Type)).ToList();
bool claimsChanged;
if (listExistingClaims.Count != listDatabaseClaims.Count)
claimsChanged = true;
else
claimsChanged = listExistingClaims.Any(claim => listDatabaseClaims.All(c => c.Type != claim.Type));
if (!claimsChanged)
return authState;
// existing identity, but with new claims
// the ToList() is to make a copy of the claims so we can read the existing ones and then remove from claimsIdentity
var claimsIdentity = new ClaimsIdentity(authState.User.Identity);
foreach (var claim in claimsIdentity.Claims.ToList().Where(claim => AuthorizeRules.AllClaimTypes.Contains(claim.Type)))
claimsIdentity.RemoveClaim(claim);
claimsIdentity.AddClaims(listDatabaseClaims);
// set this as the authentication state
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
authState = new AuthenticationState(claimsPrincipal);
SetAuthenticationState(Task.FromResult(authState));
// return the existing or updates state
return authState;
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论