从使用“dotnet new react -au Individual”创建的项目中通过“IHttpContextAccessor”访问用户信息?

Accessing user information via `IHttpContextAccessor` from project created with `dotnet new react -au Individual`?

背景

我一直在关注 documentation for using IdentityServer4 with single-page-applications on ASP.NET-Core 3.1,因此通过 dotnet new react -au Individual 命令创建了一个项目。 这将创建一个使用 Microsoft.AspNetCore.ApiAuthorization.IdentityServer NuGet 包的项目。

到目前为止,它真的很棒,它为我的 ReactJS 应用程序获得了 token-based 身份验证,没有任何痛苦! 从我的 ReactJS 应用程序,我可以访问由 oidc-client npm package 填充的用户信息,例如用户名。

此外,使用 [Authorize] 属性调用我的 Web APIs 会按预期工作:只有在请求 header 中使用有效 JWT 访问令牌的调用才能访问 API.

问题

我现在正尝试通过注入的 IHttpContextAccessor 从 GraphQL 突变解析器中访问基本用户信息(特别是用户名),但我能找到的唯一用户信息是 [=16= 下的以下声明]:

nbf: 1600012246
exp: 1600015846
iss: https://localhost:44348
aud: MySite.HostAPI
client_id: MySite
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier: (actual user GUID here)
auth_time: 1600012235
http://schemas.microsoft.com/identity/claims/identityprovider: local
scope: openid
scope: profile
scope: MySite.HostAPI
http://schemas.microsoft.com/claims/authnmethodsreferences: pwd

Web API 控制器也会出现同样的问题。

详情

MySite 是我的解决方案的命名空间,也是我在 appsettings.json 文件中定义为客户端的名称:

{
    "IdentityServer": {
        "Clients": {
            "MySite": {
                "Profile": "IdentityServerSPA"
            }
        }
    }
}

我的 web 应用程序项目的名称是 MySite.Host 所以 MySite.HostAPI 通过调用 AuthenticationBuilder.AddIdentityServerJwt().

自动生成的资源和范围的名称

... this method registers an <<ApplicationName>>API API resource with IdentityServer with a default scope of <<ApplicationName>>API and configures the JWT Bearer token middleware to validate tokens issued by IdentityServer for the app.

研究

根据 Stack Overflow 上的一些答案,通过 IIdentityServerBuilder.AddInMemoryIdentityResources() 添加 IdentityResources.Profile() 资源应该可以解决问题,但看起来它已经可以通过我在上面发布的声明获得(scope: profile ). 尽管如此,我还是尝试了它,但结果是身份验证流程被破坏:重定向到登录页面不起作用。

我找到的所有答案还参考了配置 class,例如 this demo file,其中包含主要提供给 IIdentityServerBuild.AddInMemory...() 方法的配置。 但是,似乎 Microsoft.AspNetCore.ApiAuthorization.IdentityServer 在其实现中完成了大部分工作,而是提供了 extendable builders 供使用。

根据 IdentityServer 文档,我认为我不需要添加客户端,因为访问令牌已经存在。客户端 ReactJS 应用程序使用 oidc-client 中的 access_token 对我的 Web APIs.

进行授权调用

它似乎也不需要为用户名信息添加资源或范围,因为我相信这些已经存在并且被命名为 profile。更重要的是 documentation for "IdentityServerSPA" client profile 指出:

The set of scopes includes the openid, profile, and every scope defined for the APIs in the app.

我还查看了实施 IProfileService 因为 according to the documentation this is where additional claims are populated。默认实现当前用于填充 ProfileDataRequestContext.RequestedClaimTypes object 请求的声明,并且此机制已经起作用,因为 ReactJS 客户端代码就是这样接收它们的。这意味着当我试图从 ASP.NET-Core Identity 获取用户声明时,它没有正确填充 ProfileDataRequestContext.RequestedClaimTypes 或者甚至根本没有调用 IProfileServices.GetProfileDataAsync

问题

考虑到我的项目使用 Microsoft.AspNetCore.ApiAuthorization.IdentityServer,我如何从我的 ASP.NET-Core C# 代码中查看用户名,最好使用 IHttpContextAccessor?

可以通过项目模板设置的范围UserManager<ApplicationUser>服务检索用户信息。用户声明包含"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"ClaimTypes.NameIdentifier),其值为用户标识符。 UserManager<>.FindByIdAsync() 然后可用于检索与用户相关联且包含其他用户信息的 ApplicationUser

注意每次调用时都会联系用户存储。更好的解决方案是在声明中包含额外的用户信息。

首先,如果您还没有通过调用 services.AddHttpContextAccessor();

显式添加 IHttpContextAccessor 服务

来自任意单例服务:

public class MyService
{
    public MyService(
        IHttpContextAccessor httpContextAccessor,
        IServiceProvider serviceProvider
    )
    {
        var nameIdentifier = httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;

        using (var scope = serviceProvider.CreateScope())
        {
            var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
            var user = await userManager.FindByIdAsync(nameIdentifier);
            // Can access user.UserName.
        }
    }
}

UserManager<ApplicationUser> 可以在 Razor 页面和控制器中直接访问,因为它们已经在范围内。

您需要做的是用您的自定义声明扩展 IdentityServer 请求的默认声明。不幸的是,由于您使用的是 Microsoft 的简约 IdentityServer 实现,因此很难找到使客户端请求声明的正确方法。但是,假设您只有一个应用程序(根据模板),您可以说客户总是 想要一些自定义声明。

非常重要的第一步:
鉴于您的自定义 IProfileService 在这些行之后被称为 CustomProfileService

services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

必须摆脱脚手架模板中使用的实现,并使用您自己的实现:

services.RemoveAll<IProfileService>();
services.AddScoped<IProfileService, CustomProfileService>();

接下来,如果您从 Microsoft 的版本开始,自定义 IProfileService 的实际实现并不难:

public class CustomProfileService : IdentityServer4.AspNetIdentity.ProfileService<ApplicationUser>
{
    public CustomProfileService(UserManager<ApplicationUser> userManager, 
        IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory) : base(userManager, claimsFactory)
    {
    }

    public CustomProfileService(UserManager<ApplicationUser> userManager,
        IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory,
        ILogger<ProfileService<ApplicationUser>> logger) : base(userManager, claimsFactory, logger)
    {
    }

    public override async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        string sub = context.Subject?.GetSubjectId();

        if (sub == null)
        {
            throw new Exception("No sub claim present");
        }

        var user = await UserManager.FindByIdAsync(sub);
        if (user == null)
        {
            Logger?.LogWarning("No user found matching subject Id: {0}", sub);
            return;
        }

        var claimsPrincipal = await ClaimsFactory.CreateAsync(user);
        if (claimsPrincipal == null)
        {
            throw new Exception("ClaimsFactory failed to create a principal");
        }

        context.AddRequestedClaims(claimsPrincipal.Claims);
    }
}

完成这两个步骤后,您可以开始根据需要调整 CustomProfileServiceGetProfileDataAsync。请注意,默认情况下 ASP.NET Core Identity 已经拥有电子邮件和用户名(您可以在 claimsPrincipal 变量中看到这些)声明,因此这是“请求”它们的问题:

// ....
// also notice that the default client in the template does not request any claim type,
// so you could just override if you want
context.RequestedClaimTypes = context.RequestedClaimTypes.Union(new[] { "email" }).ToList();
context.AddRequestedClaims(claimsPrincipal.Claims);

如果您想添加自定义数据,例如用户的名字和姓氏:

// ....
context.RequestedClaimTypes = context.RequestedClaimTypes.Union(new[] { "first_name", "last_name" }).ToList();
context.AddRequestedClaims(claimsPrincipal.Claims);
context.AddRequestedClaims(new[] 
{ 
    new Claim("first_name", user.FirstName),
    new Claim("last_name", user.LastName),
});