问题引入:
我们知道当请求通过认证模块时,会给当前的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 }); } }
这样每个子类都继承了基类的认证信息而不用写太多的重复代码。
让我知道如果你有更好的想法或建议!