ASP.NET Core - 基于IHttpContextAccessor实现系统级别身份标识
问题引入:
我们知道当请求通过认证模块时,会给当前的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给所有的业务类使用
让我知道如果你有更好的想法或建议!