简体   繁体   English

如何在类库中访问JWT User.Claims

[英]How to access JWT User.Claims in class library

I've structured my project using DDD, it looks like this: 我已经使用DDD构建了项目,如下所示:

| CompanyName.API
  | MyControllerThatRequiresJwtToken << Entry point
| CompanyName.Application
| CompanyName.Data
  | EfCoreContext << I would like to get the claims here
| CompanyName.Domain
| CompanyName.IoC
| CompanyName.Test
| CompanyName.UI

I am using Z.EntityFramework.Plus.Audit.EFCore to audit all data changes. 我正在使用Z.EntityFramework.Plus.Audit.EFCore审核所有数据更改。 I added it to the CompanyName.Data project as this is where my EF Context lives. 我将其添加到CompanyName.Data项目中,因为这是我的EF上下文所在的位置。

Problem is: all requests in the API require a JWT token. 问题是: API中的所有请求都需要JWT令牌。 I'd like to set the username of the person sending the request in the Audit object that will be saved into the database, however I don't have access to the HttpContext in my data layer. 我想在将保存到数据库中的Audit对象中设置发送请求的人的用户名,但是我无权访问我的数据层中的HttpContext。

What would be the best approach to get this information? 获取此信息的最佳方法是什么? Injecting IHttpContextAccessor into the data layer perhaps? 也许将IHttpContextAccessor注入数据层? It doesn't sound like a good plan to make the data layer "Http dependent". 使数据层“依赖于HTTP”听起来不是一个好计划。

UPDATE UPDATE

I am not sure how I'd pass it from the Controller to the context. 我不确定如何将其从Controller传递到上下文。 I believe it would need to be injected somehow. 我相信它将需要以某种方式注入。

Snippet of EfCoreContext.cs EfCoreContext.cs的片段

public override int SaveChanges()
{
   var audit = new Audit();
   audit.CreatedBy = "JWT Username"; // << I'd need it here

   audit.PreSaveChanges(this);
   var rowAffecteds = base.SaveChanges();
   audit.PostSaveChanges();

   if (audit.Configuration.AutoSavePreAction != null)
   {
      audit.Configuration.AutoSavePreAction(this, audit);
      base.SaveChanges();
   }

   return rowAffecteds;
}

Create an interface called IApplicationUser for instance. 例如,创建一个名为IApplicationUser的接口。 Give it readonly properties you need like id, name and what not. 为它提供所需的只读属性,例如ID,名称和其他内容。

Create an implementation of it 创建它的实现

public class ApplicationUser : IApplicationUser
{
   private readonly IHttpContextAccessor httpContextAccessor;

   public ApplicationUser(IHttpContextAccessor httpContextAccessor)
   {
      this.httpConntextAccessor = httpContextAccessor;
   }

   public Guid Id => this.GetUserId();

   public string Name => this.GetUserName();

   private Guid GetUserId()
   {
       var subject = this.httpContextAccessor.HttpContext
                         .User.Identity.Claims
                         .FirstOrDefault(claim => claim.Type == JwtClaimTypes.Subject);

       return Guid.TryParse(subject, out var id) ? id : Guid.Empty;
   }

   private Guid GetUserId()
   {
       return this.httpContextAccessor.HttpContext
                         .User.Identity.Claims
                         .FirstOrDefault(claim => claim.Type == JwtClaimTypes.PreferredUserName);
   }
}

Now register that with your DI-Container. 现在,向您的DI容器注册。 For default MS IOC: 对于默认的MS IOC:

services.AddScoped<IApplicationUser, ApplicationUser>();

Inject IApplicationUser where ever you need and use it to get user-information. 在需要的地方注入IApplicationUser,并使用它来获取用户信息。

Edit: IHttpContextAccessor must be registered. 编辑: IHttpContextAccessor必须注册。 If it isn't the case, do it like that as well 如果不是这种情况,也要这样做

services.AddScoped<IHttpContextAccessor, HttpContextAccessor>();

Edit 2: Just to clarify. 编辑2:只是为了澄清。 This is not meant to be used in a repository or however you would like to call it. 这不是要在存储库中使用,或者您想调用它。 Rethink your logic so you can pass that information to your entity before saving data. 重新考虑您的逻辑,以便您可以在保存数据之前将该信息传递给您的实体。

I faced something like this situation. 我遇到了这种情况。 My solution steps like that: 我的解决方案是这样的:

1 - Get user information at controller and give that information to your dto (request) object. 1-在控制器上获取用户信息,并将该信息提供给您的dto(请求)对象。

I write extensions for getting user id: 我编写了用于获取用户ID的扩展程序:

public static string GetUserId(this HttpContext httpContext)
{
    return httpContext?.User?.Claims?.FirstOrDefault(claim => claim.Type == ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
}

Request object: 请求对象:

public class CreateMenuRequest
{
    public string MenuName { get; set; }

    [JsonIgnore]
    public string UpdatedBy { get; set; }
}

2- Set user information into request object 2-将用户信息设置为请求对象

Controller : 控制器:

[HttpPost, Route("")]
public IActionResult CreateMenu([FromBody] CreateMenuRequest createMenuRequest)
{
    if (createMenuRequest != null)
    {
        createMenuRequest.UpdatedBy = HttpContext.GetUserId();
    }

    CreateMenuResponse createMenuResponse = _menuService.CreateMenu(createMenuRequest);
    return StatusCode(HttpStatusCode.Created.ToInt(), createMenuResponse);
}

3 - In service layer, after validation and other business requirements, I map request to entity object. 3-在服务层中,经过验证和其他业务需求后,我将请求映射到实体对象。 Entity object like that: 像这样的实体对象:

public class Menu : IAudit, ISoftDeletable
{
    public long Id { get; set; }
    ..........

    public string UpdatedBy { get; set; }
    public DateTime UpdateDate { get; set; }
    public string CreatedBy { get; set; }
    public DateTime CreateDate { get; set; }

    public bool IsDeleted { get; set; }
}

4 - override SaveChanges for editing UpdateDate and CreatedDate, also if item is added, Updatedby information set into CreatedBy field. 4-覆盖SaveChanges以编辑UpdateDate和CreatedDate,如果添加了项,则将Updatebyby信息设置为CreatedBy字段。

public override int SaveChanges()
{
   ChangeTracker.DetectChanges();

   IEnumerable<EntityEntry> deletedEntities = ChangeTracker.Entries()
                                                           .Where(t => t.State == EntityState.Deleted && t.Entity is ISof

   foreach (EntityEntry deletedEntity in deletedEntities)
   {
       if (!(deletedEntity.Entity is ISoftDeletable item)) continue;
       item.IsDeleted = true;
       deletedEntity.State = EntityState.Modified;
   }

   IEnumerable<object> addedEntities = ChangeTracker.Entries()
                                                    .Where(t => t.State == EntityState.Added && t.Entity is IAudit)
                                                    .Select(t => t.Entity);
   IEnumerable<object> modifiedEntities = ChangeTracker.Entries()
                                                       .Where(t => t.State == EntityState.Modified && t.Entity is IAudit)
                                                       .Select(t => t.Entity);

   DateTime now = DateTime.UtcNow;

   Parallel.ForEach(addedEntities, o =>
                                   {
                                       if (!(o is IAudit item))
                                           return;
                                       item.CreateDate = now;
                                       item.UpdateDate = now;
                                       item.CreatedBy = item.UpdatedBy;
                                   });

   Parallel.ForEach(modifiedEntities, o =>
                                      {
                                          if (!(o is IAudit item))
                                              return;
                                          item.UpdateDate = now;
                                      });

   return base.SaveChanges();
}

Instead of Passing IHttpContextAccessor, Extract the username in Controller and then pass it to the methods that would need it. 而不是传递IHttpContextAccessor,而是在Controller中提取用户名,然后将其传递给需要它的方法。 I have created an extension method like this, 我创建了这样的扩展方法,

public static class UserResolverService
{
    public static Guid GetUserId(this ControllerBase controller)
    {
        var result = controller.User.FindFirstValue(ClaimTypes.NameIdentifier);
        return Guid.Parse(result);
    }
}

Then in your service method calls, 然后在您的服务方法调用中,

 entity= await DomainServices.CreateSomething(this.GetUserId(), dtoObject);

This way your services don't depend on HttpContext Directly as that exposes more things than required. 这样,您的服务就不会直接依赖于HttpContext,因为它暴露了比所需更多的东西。 However, there is an overhead of passing username everywhere. 但是,在任何地方传递用户名都会产生开销。

Another option is to create a service that depends on IHttpContext accessor and Expose a GetUserMethod. 另一种选择是创建一个依赖于IHttpContext访问器的服务,并公开一个GetUserMethod。 Then let other services depend on this one to get the current user. 然后让其他服务依赖此服务来获取当前用户。 This way only one of your business layer service will be coupled with IHttpContextAccessor. 这样,只有一个业务层服务将与IHttpContextAccessor耦合。 Best Candidate will be your UserProfile Service. 最佳候选人将是您的UserProfile服务。

Create an abstraction of the data you require and then inject into DbContext as a service with DI. 创建所需数据的抽象,然后将DbContext作为具有DI的服务注入DbContext

// interface
public interface IAppPrincipal
{
    string Name { get; }
}

// concrete class
public class AppPrincipal : IAppPrincipal
{
    public AppPrincipal(string name)
    {
        Name = name;
    }

    public string Name { get; }
}

// db context
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options, IAppPrincipal principal = null)
    {
        Principal = principal;
    }

    public IAppPrincipal Principal { get; }

    public override int SaveChanges()
    {
        var audit = new Audit();
        audit.CreatedBy = Principal?.Name;

        ...
    }
}

// service registration in API or MVC app
services.AddScoped<IAppPrincipal>(provider => 
{
    var user = provider.GetService<IHttpContextAccessor>()?.HttpContext?.User;

    return new AppPrincipal(user?.Identity?.Name);
});

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM