ASP.NET Core - 基于IHttpContextAccessor实现系统级别身份标识

  问题引入:

  我们知道当请求通过认证模块时,会给当前的HttpContext赋予当前用户身份标识,我们在需要授权的控制器中打上[Authorize]授权标签,就可以在ControllerBase的User属性获取到基于声明的权限标识(ClaimsPrincipal)。这只是针对Controller层面,遗憾的是很多场景下我们是需要在Service层乃至数据层获直接使用用户信息,这种情况我们就使用不了User了。

  在Asp.net 4.x时代,我们通常通过HttpContext.Current获取当前请求的上下文进而获取到当前的User属性,而Aspnet Core则是通过注入HttpContext的访问器对象IHttpContextAccessor来获取当前的HttpContext。

 

  解决方案:

  问题的解决在于我们如何获取当前的HttpContext上下文,首先我们需要注册IHttpContextAccessor的实例

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

    ....
}

   有了这个注册,我们封装一个方法从IHttpContextAccessor 的HttpContext中获取对应的ClaimsPrincipal,如下:

public class PrincipalAccessor : IPrincipalAccessor
{
  //没有通过认证的,User会为空 public ClaimsPrincipal Principal => _httpContextAccessor.HttpContext?.User; private readonly IHttpContextAccessor _httpContextAccessor; public PrincipalAccessor(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } } public interface IPrincipalAccessor { ClaimsPrincipal Principal { get; } }

   我们一样把这个类也加入注册中

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    services.AddSingleton<IPrincipalAccessor, PrincipalAccessor>();
    ....
}

   有了这些东西,那我们需要的是如何从ClaimsPrincipal获取到对应的Claims了

  public class ClaimsAccessor : IClaimsAccessor
    {
        protected IPrincipalAccessor PrincipalAccessor { get; }

        public ClaimsAccessor(IPrincipalAccessor principalAccessor)
        {
            PrincipalAccessor = principalAccessor;
        }

        /// <summary>
        /// 登录用户ID
        /// </summary>
        public int? ApiUserId
        {
            get
            {
                var userId = PrincipalAccessor.Principal?.Claims.FirstOrDefault(c => c.Type == SystemClaimTypes.UserId)?.Value;
                if (userId != null)
                {
                    int id = 0;
                    int.TryParse(userId, out id);
                    return id;
                }

                return null;
            }
        }

       /// <summary>
        /// 用户角色Id
        /// </summary>
        public string RoleIds
        {
            get
            {
                var roleIds = PrincipalAccessor.Principal?.Claims.FirstOrDefault(c => c.Type == SystemClaimTypes.RoleIds)?.Value;
                if (string.IsNullOrWhiteSpace(roleIds))
                {                    
                    return string.Empty;
                }

                return roleIds;
            }
        }
  }

    public interface IClaimsAccessor
    {
        /// <summary>
        /// 登录用户ID
        /// </summary>
        int? ApiUserId { get; }
       
        /// <summary>
        /// 用户角色Id
        /// </summary>
        string RoleIds{ get; }
    }

   同样我们把它注册进来

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    services.AddSingleton<IPrincipalAccessor, PrincipalAccessor>();
    services.AddSingleton<IClaimsAccessor, ClaimsAccessor>();
    ....
}

  好的让我们看看该如何使用  

    public class TestService : ITestService
    {
        private readonly IClaimsAccessor _claims;
        private readonly IRepository<Product> _productRepository;

        public TestService(IClaimsAccessor claims, IRepository<Product> productRepository)
        {
            _claims = claims;
            _productRepository = productRepository;
        }


        public Result AddProduct(ProductDto dto)
        {
            _productRepository.Insert(new Product
            {
                Name = dto.Name,
                ...
                CreatorUserId = _claims.ApiUserId
            });
        }
    }

  至此我们的系统级别的标识已经完成,只需通过注入的方式即可。

  改进

  我们发现了如果每个服务都按照上面的写法的话,每次都需要手动进行构造注入会比较繁琐,可以创建基类进行继承

    public abstract class ServiceBase 
    {
        /// <summary>
        /// 身份信息
        /// </summary>
        protected IClaimsAccessor Claims { get; set; }

        /// <summary>
        /// cotr
        /// </summary>
        protected ServiceBase ()
        {
            Claims = ServiceProviderInstance.Instance.GetRequiredService<IClaimsAccessor>();
        }
    }

  上面用到了ServiceProviderInstance,这里主要是利用了

    public class ServiceProviderInstance
    {
        public static IServiceProvider Instance { get; set; }
    }  

  ServiceProviderInstance的实例是在应用启动的时候把当前的IServiceProvider保存起来

   public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
     {
            ...
            ServiceProviderInstance.Instance = app.ApplicationServices;
    }

   好的让我们看看改进后该如何使用

   public class TestService : ServiceBase,ITestService
    {
        private readonly IRepository<Product> _productRepository;

        public TestService(IRepository<Product> productRepository)
        {
            _productRepository = productRepository;
        }


        public Result AddProduct(ProductDto dto)
        {
            _productRepository.Insert(new Product
            {
                Name = dto.Name,
                ...
                CreatorUserId = Claims.ApiUserId
            });
        }
    }

  这样每个子类都继承了基类的认证信息而不用写太多的重复代码。

 

  让我知道如果你有更好的想法或建议!

上一篇:C#开发微信门户及应用(1)--开始使用微信接口


下一篇:Asp.NetCore 请求日志拦截