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給全部的業務類使用

 

  讓我知道若是你有更好的想法或建議!

相關文章
相關標籤/搜索