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

  问题引入:

  经过【ASP.NET Core[源码分析篇] - 认证】这篇文章中,咱们知道当请求经过认证模块时,会给当前的HttpContext赋予当前用户身份标识,咱们在须要受权的控制器中打上[Authorize]受权标签,就能够在ControllerBase的User属性获取到基于声明的权限标识(ClaimsPrincipal)。html

  遗憾的是这只是针对Controller层面,不少场景下咱们是须要在Service层乃至数据层获直接使用用户信息,这种状况咱们就使用不了User了。  程序员

  解决方案:

  在Asp.net 4.x时代,咱们一般的作法是经过HttpContext.Current获取当前请求的上下文进而获取到当前的User属性,因此问题的切入点在于咱们如何获取当前的HttpContext上下文。架构

  在咱们的Aspnet Core应用中,系统是经过注入HttpContext的访问器对象IHttpContextAccessor来获取当前的HttpContext。app

  实现

  首先咱们须要在Startup的ConfigureServices方法中注册IHttpContextAccessor的实例ide

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

    ....
}

   有了这个注册,咱们封装一个方法从IHttpContextAccessor 的HttpContext中获取对应的ClaimsPrincipal,以下(认证经过后,User是具备当前用户的身份标志的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; } }

   在Startup的ConfigureServices方法中,咱们同样把这个类也加入注册中ui

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

   有了以上这些基础架构提供的东西,剩下的是咱们该须要如何从ClaimsPrincipal获取到对应的Claims了,在这里咱们定义一个ClaimsAccessor类负责从PrincipalAccessor把用户的身份标志信息提取出来,好比用户的角色,Id等业务数据,这些是须要在获取Token时系统所提供过的信息。spa

  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; }
    }

   一样咱们在Startup的ConfigureServices方法中把IClaimsAccessor注册进来.net

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
            });
        }
    }

  当咱们的IClaimsAccessor解析出来时,咱们就获取到全部的Claims信息,能够基于这些信息提取访问用户的身份标志,这样咱们就不单单局限于在Controller层面才能获取用户的身份标志了,至此咱们的系统级别的标识已经完成,记得在项目的启动项中利用ASP.NET Core的容器把服务注册进来,在需通的地方解析出来便可使用。

  改进

  这时能知足咱们的业务吗?能!可是对于咱们稍微要求高点的程序员,咱们就能够发现,若是每一个服务都按照上面的写法的话,明显在实际应用中须要写不少重复的代码,每次都须要手动进行构造注入会比较繁琐,好吧,看看咱们该如何进行优雅且可复用的简化和改进

  首先咱们先定义一个ServiceProviderInstance类

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

  这个类的做用是保存IServiceProvider的一个实例,为何须要这样呢?这里的一个设计思想是,咱们如何能顺利解析出咱们所需的IClaimsAccessor对象进而获得咱们所需的信息?在ASP.NET Core的容器中,系统提供了IServiceCollection来注册服务和提供了IServiceProvider这个让咱们解析各类注册过的服务(具体可参考ASP.NET Core - 依赖注入文章所讲解的依赖注入),这时咱们的目标就是须要获取到当前应用的IServiceProvider实例,因此这个ServiceProviderInstance类的做用时为了获取IServiceProvider所设计出来的静态类。

  如何获取到应用的IServiceProvider实例?

  在应用初始化过程当中,WebHostBuilder会利用ServiceCollection来建立新的ServiceProvider来供系统使用,因此咱们在Startup类的Configure方法中,经过ApplicationBuilder的ApplicationServices属性就能获取到系统的ServiceProvider实例,在此咱们利用ServiceProviderInstance的Instance属性保存当前的IServiceProvider以供系统后面使用  

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

  在获取到了系统的IServiceProvider实例后,剩下的就是利用这个实例把咱们前面注册的基础服务IClaimsAccessor解析出来了,咱们能够利用面向对象的特色,建立基类进行继承

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

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

   好的让咱们看看改进后在一个实际环境中该如何使用

   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
            });
        }
    }

  让全部子类都继承了ServiceBase,这样在全部的业务层均可以直接获取到用户的身份信息而不用写太多的重复代码。

 

  总结

  1. 利用ASP.NET Core提供的IHttpContextAccessor来获取HttpContext的User属性

  2. 封装一系列的基础类和利用依赖注入来解析出全部的Claims

  3. 为了不过多的侵入式代码,优雅且可复用的建立ServiceBase给全部的业务类使用

 

  让我知道若是你有更好的想法或建议!

相关文章
相关标签/搜索