用户在 Blazor Webassembly 身份验证和授权中具有多个角色时出现问题?

Problem when user has multi roles in Blazor Webassembly authentication and authorisation?

我有一个 Blazor Webassembly 解决方案,当我使用用户(具有多个角色)登录时,登录操作显示错误:

fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
      An unhandled exception has occurred while executing the request.
      System.ArgumentException: An item with the same key has already been added. Key: http://schemas.microsoft.com/ws/2008/06/identity/claims/role
         at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
         at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
         at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
         at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector)
         at REMO.Server.Controllers.AuthController.CurrentUserInfo() in D:\REMO\REMO\Server\Controllers\AuthController.cs:line 87
         at lambda_method29(Closure , Object , Object[] )
         at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean&
isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()

关注我的自定义身份验证提供程序

public class CustomStateProvider : AuthenticationStateProvider
{
        private readonly IAuthService api;
    private CurrentUser _currentUser;
    public CustomStateProvider(IAuthService api)
    {
        this.api = api;
    }
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var identity = new ClaimsIdentity();
        try
        {
                var userInfo = await GetCurrentUser();
                var roleClaims = identity.FindAll(identity.RoleClaimType);
                if (userInfo.IsAuthenticated)
                
                {
                var claims = new[] 
                { 
                   new Claim(ClaimTypes.Name, _currentUser.UserName)
                }
                .Concat(_currentUser.Claims.Select(c => new Claim(c.Key, c.Value)));
                    identity = new ClaimsIdentity(claims, "Server authentication");
                }
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine("Request failed:" + ex.ToString());
        }

        return new AuthenticationState(new ClaimsPrincipal(identity));
    }

Startup.cs

...
    services.AddDbContext<ApplicationDBContext>(options => options .UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDBContext>();
...

问题似乎是一个身份存在多个角色。
如果身份只有一个角色,则不会出现此问题。

您的问题是角色声明密钥的构造。
在多个角色的情况下,此密钥必须是 array.
您的 JWT 令牌中不能有多个“角色”密钥。

我认为您已经使用了 Mukesh 中的代码并且是一个很好的起点,但是如果您阅读评论,最后一个解释了与您一样的问题。

所以你需要修改行

.Concat(_currentUser.Claims.Select(c => new Claim(c.Key, c.Value)));

通过 LINQ 提取所有 不是 类型 role 的声明,并将它们添加到 claims 数组。
现在您需要创建一个包含所有类型为 role 的声明的数组(我认为它们来自您的 API)并添加一个 role 声明数组类型的条目。

修改后我觉得应该可以了。

生成的解码 JWT 令牌应具有以下形式:

{
   "sub": "nbiada",
   "jti": "123...",
   "role" : [
      "User",
      "Admin"
   ],
   "exp": 12345467,
...
}

注意: 我缩短了 role 键,在您的实现中应该是 http://schemas.microsoft.com/ws/2008/06/identity/claims/role

正如Nicola Biada所说,你的问题是角色声明密钥的构造。 我实现了 Mukesh Sample 并只是修改 AuthController 中的 CurrentUserInfo 操作以将角色声明作为数组发送。 这是我的行动

public CurrentUser CurrentUserInfo()
{
    var roles = User.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToArray();
    var json = JsonSerializer.Serialize(roles);
    var claims = User.Claims.Where(c => c.Type != ClaimTypes.Role).ToDictionary(c => c.Type, c => c.Value);
    claims.Add(ClaimTypes.Role, json);
    return new CurrentUser
    {
        IsAuthenticated = User.Identity.IsAuthenticated,
        UserName = User.Identity.Name,
        Claims = claims
    };
}

然后在客户端i return回到之前的状态。为此,我更改了 CustomStateProvider 中的 GetAuthenticationStateAsync。

public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
    var identity = new ClaimsIdentity();
    try
    {
        var userInfo = await GetCurrentUser();
        if (userInfo.IsAuthenticated)
        {
            var claims = new[] { new Claim(ClaimTypes.Name, _currentUser.UserName) }.Concat(_currentUser.Claims.Select(c => new Claim(c.Key, c.Value))).ToList();
            var roles = claims.FirstOrDefault(c => c.Type == ClaimTypes.Role);
            claims.Remove(roles);
            var rolesString = JsonSerializer.Deserialize<string[]>(roles.Value);
            foreach (var role in rolesString)
            {
                claims.Add(new Claim(ClaimTypes.Role, role));
            }
            identity = new ClaimsIdentity(claims, "Server authentication");
        }
    }
    catch (HttpRequestException ex)
    {
        Console.WriteLine("Request failed:" + ex.ToString());
    }
    return new AuthenticationState(new ClaimsPrincipal(identity));
}