問題引入:
通過【ASP.NET Core[源碼分析篇] - 認證】這篇文章中,我們知道當請求通過認證模塊時,會給當前的HttpContext賦予當前用戶身份標識,我們在需要授權的控制器中打上[Authorize]授權標簽,就可以在ControllerBase的User屬性獲取到基於聲明的權限標識(ClaimsPrincipal)。
遺憾的是這只是針對Controller層面,很多場景下我們是需要在Service層乃至數據層獲直接使用用戶信息,這種情況我們就使用不了User了。
解決方案:
在Asp.net 4.x時代,我們通常的做法是通過HttpContext.Current獲取當前請求的上下文進而獲取到當前的User屬性,所以問題的切入點在於我們如何獲取當前的HttpContext上下文。
在我們的Aspnet Core應用中,系統是通過注入HttpContext的訪問器對象IHttpContextAccessor來獲取當前的HttpContext。
實現
首先我們需要在Startup的ConfigureServices方法中注冊IHttpContextAccessor的實例
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方法中,我們一樣把這個類也加入注冊中
public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddSingleton<IPrincipalAccessor, PrincipalAccessor>(); .... }
有了以上這些基礎架構提供的東西,剩下的是我們該需要如何從ClaimsPrincipal獲取到對應的Claims了,在這里我們定義一個ClaimsAccessor類負責從PrincipalAccessor把用戶的身份標志信息提取出來,比如用戶的角色,Id等業務數據,這些是需要在獲取Token時系統所提供過的信息。
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注冊進來
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給所有的業務類使用
讓我知道如果你有更好的想法或建議!