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

Posted

技术标签:

【中文标题】从使用“dotnet new react -au Individual”创建的项目中通过“IHttpContextAccessor”访问用户信息?【英文标题】:Accessing user information via `IHttpContextAccessor` from project created with `dotnet new react -au Individual`? 【发布时间】:2020-12-31 14:15:44 【问题描述】:

背景

我一直在关注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 应用程序提供了基于令牌的身份验证,并且没有任何痛苦! 在我的 ReactJS 应用程序中,我可以访问由 oidc-client npm package 填充的用户信息,例如用户名。

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

问题

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

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() 自动生成的资源和范围的名称。

...此方法向 IdentityServer 注册一个 >API API 资源,默认范围为 >API,并配置 JWT Bearer 令牌中间件以验证 IdentityServer 为应用程序发出的令牌。

研究

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

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

从 IdentityServer 文档中,我认为我不需要添加客户端,因为访问令牌已经存在。客户端 ReactJS 应用程序使用来自 oidc-clientaccess_token 对我的 Web API 进行授权调用。

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

范围集包括 openid、配置文件以及为应用中的 API 定义的每个范围。

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

问题

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

【问题讨论】:

不是一个答案,因为我已经有一段时间没有接触 IdSrv4,但是:用户名之类的东西是用户声明。您需要创建这些用户声明,为每个用户分配值(IdSrv4 DB 中的多对多关系),然后客户端必须在授权时专门请求这些用户声明(因此必须允许客户端请求这些声明,如果您说这些声明属于profile 范围,则最后一步可以视为“完成”) 这会在IProfileService 的实现中完成吗? According to the documentation,这是我要添加其他声明的地方。我认为我不必这样做,因为我认为用户名是基本的,但看起来我错了。我不太确定如何实现IsActiveAsync。我的实现会覆盖脚手架设置的默认实现吗?这样做是否存在安全风险? 请记住:IdSrv4 是一个开源框架,如果您有疑问,可以随时查看源代码。默认配置文件是 github.com/IdentityServer/IdentityServer4/blob/main/src/… 和 ASP.NET Core Identity 是 github.com/IdentityServer/IdentityServer4/blob/main/src/… - 你可以很容易地说 IsActiveAsync 在两个版本中总是返回 true,你可能想要改变它 感谢@CamiloTerevinto!有趣的。我还找到了一个替代方案并将其发布为下面的答案,但您的解决方案更好。您介意将其发布为答案吗?如果不是,我会在编写IProfileService 的玩具实现后继续添加第二个答案。 在尝试实现IProfileServices 时,我遇到了一个问题。默认实现当前用于填充 ProfileDataRequestContext.RequestedClaimTypes 对象请求的声明,并且该机制已经起作用,因为这是 ReactJS 客户端代码接收它们的方式。这意味着当我尝试从 ASP.NET-Core Identity 获取用户声明时,它没有正确填充 ProfileDataRequestContext.RequestedClaimTypes 或者甚至根本没有调用 IProfileServices.GetProfileDataAsync 【参考方案1】:

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

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

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

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

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

接下来,如果你从微软的版本开始,自定义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),
);

【讨论】:

【参考方案2】:

可以通过项目模板设置的范围UserManager&lt;ApplicationUser&gt; 服务检索用户信息。用户的声明包含"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" (ClaimTypes.NameIdentifier),其值为用户标识符。然后,UserManager&lt;&gt;.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&lt;ApplicationUser&gt; 可以直接在 Razor 页面和控制器中访问,因为它们已经在作用域内。

【讨论】:

这种方法的问题是你总是在联系数据库,你只能在服务器端获得值,而如果你在声明中包含用户名,你会已经在内存和客户端中具有值(前提是您允许每个客户端访问此类声明)

以上是关于从使用“dotnet new react -au Individual”创建的项目中通过“IHttpContextAccessor”访问用户信息?的主要内容,如果未能解决你的问题,请参考以下文章

使用存储过程从视图中检索或过滤数据是不是比使用存储过程从表中获取或过滤数据更快?

如何使用 graphql 从 Firebase 使用 Flutter 从 Cloud Firestore 获取数据?

使用 RTSP 从 Opencv 处理后,视频从 PC 流式传输到 Android

从哪里 clickonce 从 regedit 获取 DisplayName 中使用的值?

如何使用自适应卡操作从自适应卡获取用户响应。使用 Microsoft Bot Framework 从 MS Teams 频道提交操作?

无法从使用推送从苹果开发者帐户创建的 aps_production 创建 p12