身份验证库 – 检查已启用并在每个页面上重新读取声明

huangapple go评论61阅读模式
英文:

Identity library - Check Enabled & re-read Claims on each page

问题

我正在使用ASP.NET Identity库在Blazor(服务器端)应用程序中。我遇到了这个问题,但我认为它是更大问题的一部分。

首先,如果我更改用户的声明,特别是如果我减少它们,我希望这些更改立即生效 - 无论用户的操作如何。毕竟,如果我说“请注销然后重新登录以使您的权限减少生效” - 是的,人们会立即这样做</sarcasm>。

我喜欢缓存登录,这样用户不需要每次访问网站时都要登录。我绝对希望保持这一点。但我希望这个级联参数 Task&lt;AuthenticationState&gt; 每次转到新页面时都重新读取声明(是的,它是一个SPA,但你知道我的意思 - 转到SPA中的新URL)。不是为每个新会话重新读取,而是为每个新页面重新读取。这样更改会立即生效。

另外,我将在AspNetUsers表和IdentityUser模型中添加一个Enable列。再次,我希望每次页面更改时都会检查这一点,以便 Task&lt;AuthenticationState&gt; 知道用户已禁用,因此 @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&lt;AuthenticationState&gt; 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&lt;AuthenticationState&gt; 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&lt;AuthenticationStateProvider, ExAuthenticationStateProvider&gt;();

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&lt;ExIdentityUser&gt; _userManager;
public ExAuthenticationStateProvider(UserManager&lt;ExIdentityUser&gt; userManager, IConfiguration config)
{
_userManager = userManager;
var minutes = config.GetSection(&quot;Identity:RevalidateMinutes&quot;).Value;
if (!string.IsNullOrEmpty(minutes) &amp;&amp; int.TryParse(minutes, out var intMinutes)) 
_checkInterval = TimeSpan.FromMinutes(intMinutes);
}
/// &lt;summary&gt;
/// Revalidate the next time GetAuthenticationStateAsync() is called.
/// &lt;/summary&gt;
public void ResetNextCheck()
{
_nextCheck = DateTime.MinValue;
}
/// &lt;inheritdoc /&gt;
public override async Task&lt;AuthenticationState&gt; GetAuthenticationStateAsync()
{
// if less than 30 minutes since last check, then just return the default
var now = DateTime.UtcNow;
if (now &lt; _nextCheck)
return await base.GetAuthenticationStateAsync();
_nextCheck = now + _checkInterval;
// if we&#39;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&#39;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&#39;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&#39;t want to call NotifyAuthenticationStateChanged() if nothing changed)
var listDatabaseClaims = (await _userManager.GetClaimsAsync(user)).ToList();
var listExistingClaims = authState.User.Claims.Where(claim =&gt; AuthorizeRules.AllClaimTypes.Contains(claim.Type)).ToList();
bool claimsChanged;
if (listExistingClaims.Count != listDatabaseClaims.Count)
claimsChanged = true;
else
claimsChanged = listExistingClaims.Any(claim =&gt; listDatabaseClaims.All(c =&gt; 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 =&gt; 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;
}
}

huangapple
  • 本文由 发表于 2023年6月26日 09:55:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/76553121.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定