如何在服务层获取用户

我使用ASP.NET Core 2.1并希望在服务级别获取User

我看到HttpContextAccessor被注入某个服务然后我们通过UserManager获取当前User例子

 var user = await _userManager.GetUserAsync(accessor.HttpContext.User); 

或在控制器中

 var user = await _userManager.GetUserAsync(User); 

问题:

  • HttpContextAccessor注入服务似乎是错误的 – 仅仅因为我们违反了SRP并且服务层没有被隔离(它依赖于http上下文 )。

  • 我们当然可以在控制器中获取用户 (一种更好的方法),但我们面临两难 – 我们根本不想在每个服务方法中将User作为参数传递

我花了几个小时思考如何最好地实现它,并提出了一个解决方案。 我不完全确定我的方法是否足够,并且不违反任何软件设计原则。

共享我的代码希望从StackOverflow社区获得建议。

这个想法如下:

首先,我介绍注册为Singleton的SessionProvider

 services.AddSingleton(); 

SessionProvider有一个Session属性,用于保存UserTenant等。

其次,我介绍SessionMiddleware并注册它

 app.UseMiddleware(); 

Invoke方法中,我解析了HttpContextSessionProviderUserManager

  • 我取了User

  • 然后我初始化ServiceProvider单例的Session属性:

sessionProvider.Initialise(user);

在此阶段, ServiceProvider具有包含我们需要的信息的Session对象。

现在我们将SessionProvider注入到任何服务中,并且其Session对象已准备好使用。


码:

SessionProvider

 public class SessionProvider { public Session Session; public SessionProvider() { Session = new Session(); } public void Initialise(ApplicationUser user) { Session.User = user; Session.UserId = user.Id; Session.Tenant = user.Tenant; Session.TenantId = user.TenantId; Session.Subdomain = user.Tenant.HostName; } } 

Session

 public class Session { public ApplicationUser User { get; set; } public Tenant Tenant { get; set; } public long? UserId { get; set; } public int? TenantId { get; set; } public string Subdomain { get; set; } } 

SessionMiddleware

 public class SessionMiddleware { private readonly RequestDelegate next; public SessionMiddleware(RequestDelegate next) { this.next = next ?? throw new ArgumentNullException(nameof(next)); } public async Task Invoke( HttpContext context, SessionProvider sessionProvider, MultiTenancyUserManager userManager ) { await next(context); var user = await userManager.GetUserAsync(context.User); if (user != null) { sessionProvider.Initialise(user); } } } 

现在服务层代码:

 public class BaseService { public readonly AppDbContext Context; public Session Session; public BaseService( AppDbContext context, SessionProvider sessionProvider ) { Context = context; Session = sessionProvider.Session; } } 

所以这是任何服务的类,你可以看到我们现在可以轻松获取Session对象并且可以使用了:

 public class VocabularyService : BaseService, IVocabularyService { private readonly IVocabularyHighPerformanceService _vocabularyHighPerformanceService; private readonly IMapper _mapper; public VocabularyService( AppDbContext context, IVocabularyHighPerformanceService vocabularyHighPerformanceService, SessionProvider sessionProvider, IMapper mapper ) : base( context, sessionProvider ) { _vocabularyHighPerformanceService = vocabularyHighPerformanceService; _mapper = mapper; } public async Task<List> GetAll() { List dtos = _vocabularyHighPerformanceService.GetAll(Session.TenantId.Value); dtos = dtos.OrderBy(x => x.Name).ToList(); return await Task.FromResult(dtos); } } 

关注以下几点:

 .GetAll(Session.TenantId.Value); 

另外,我们可以轻松获得当前用户

 Session.UserId.Value 

要么

 Session.User 

就是这样了。

我测试了我的代码,当打开几个选项卡时它运行良好 – 每个选项卡在url中都有不同的子域(租户是从子域解析的 – 数据正在被正确获取)。

使用动作filter可确保在动作调用管道中调用所需行为,以便已实现必要的依赖关系(如HttpContext.User)

ASP.NET Core中的参考filter

实现异步操作filter以避免调用.Result阻塞调用,因为它可能导致请求管道中的死锁。

 public class SessionFilter : IAsyncActionFilter { public async Task OnActionExecutionAsync( ActionExecutingContext context, ActionExecutionDelegate next) { // do something before the action executes var serviceProvider = context.HttpContext.RequestServices; var sessionProvider = serviceProvider.GetService(); var userManager = serviceProvider.GetService>() var user = await userManager.GetUserAsync(context.HttpContext.User); if (user != null) { sessionProvider.Initialise(user); } //execute action var resultContext = await next(); // do something after the action executes; resultContext.Result will be set //... } } 

你的方法似乎是正确的。 唯一的问题 – 你不应该将SessionProvider注册为Singleton ,否则你会遇到同时请求的问题。 将其注册为Scoped以获取每个请求的新实例。 此外,您必须在调用下一个中间件之前填充SessionInfo。 正如Nikosi所说,应该用filter替换中间件,以获得有关用户的正确数据。 对于filter实现,它使用被认为是反模式的服务定位器模式。 更好的方法是使用构造函数注入它,框架已经支持它。 如果您在全球范围内使用它,您只需将其注册为:

 public void ConfigureServices(IServiceCollection services) { services.AddMvc(options => { options.Filters.Add(); }); } 

或者如果只需要一些动作就可以使用它,你可以使用filter

 [ServiceFilter(typeof(SessionFilter))] 

在这种情况下,filter也应该注册:

 public void ConfigureServices(IServiceCollection services) { ... services.AddScoped(); ... } 

在我看来,这是一个更好的解决方法 – 我们不再为每个请求进行数据库调用,我们只需从Claims中检索UserID和TenantID:

请注意Session的生命周期是Per Request – 当请求开始时我们挂钩它,解析SessionContext实例,然后使用UserIDTenantID填充它 – 在我们注入Session任何地方(给定相同的请求)之后 – 它将包含我们需要的价值观

 services.AddScoped(); 

Session.cs

 public class Session { public long? UserId { get; set; } public int? TenantId { get; set; } public string Subdomain { get; set; } } 

AppInitializationFilter.cs

 public class AppInitializationFilter : IAsyncActionFilter { private Session _session; private DBContextWithUserAuditing _dbContext; private ITenantService _tenantService; public AppInitializationFilter( Session session, DBContextWithUserAuditing dbContext, ITenantService tenantService ) { _session = session; _dbContext = dbContext; _tenantService = tenantService; } public async Task OnActionExecutionAsync( ActionExecutingContext context, ActionExecutionDelegate next ) { string userId = null; int? tenantId = null; var claimsIdentity = (ClaimsIdentity)context.HttpContext.User.Identity; var userIdClaim = claimsIdentity.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier); if (userIdClaim != null) { userId = userIdClaim.Value; } var tenantIdClaim = claimsIdentity.Claims.SingleOrDefault(c => c.Type == CustomClaims.TenantId); if (tenantIdClaim != null) { tenantId = !string.IsNullOrEmpty(tenantIdClaim.Value) ? int.Parse(tenantIdClaim.Value) : (int?)null; } _dbContext.UserId = userId; _dbContext.TenantId = tenantId; string subdomain = context.HttpContext.Request.GetSubDomain(); _session.UserId = userId; _session.TenantId = tenantId; _session.Subdomain = subdomain; _tenantService.SetSubDomain(subdomain); var resultContext = await next(); } } 

AuthController.cs

 [Route("api/[controller]/[action]")] [ApiController] public class AuthController : Controller { public IConfigurationRoot Config { get; set; } public IUserService UserService { get; set; } public ITenantService TenantService { get; set; } [AllowAnonymous] [HttpPost] public async Task Authenticate([FromBody] AuthenticateInput input) { var expires = input.RememberMe ? DateTime.UtcNow.AddDays(5) : DateTime.UtcNow.AddMinutes(20); var user = await UserService.Authenticate(input.UserName, input.Password); if (user == null) { throw new Exception("Unauthorised"); } int? tenantId = TenantService.GetTenantId(); string strTenantId = tenantId.HasValue ? tenantId.ToString() : string.Empty; var tokenHandler = new JwtSecurityTokenHandler(); var tokenDescriptor = new SecurityTokenDescriptor { Expires = expires, Issuer = Config.GetValidIssuer(), Audience = Config.GetValidAudience(), SigningCredentials = new SigningCredentials(Config.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256), Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, user.UserName), new Claim(ClaimTypes.NameIdentifier, user.Id), new Claim(CustomClaims.TenantId, strTenantId) }) }; var token = tokenHandler.CreateToken(tokenDescriptor); string tokenString = tokenHandler.WriteToken(token); return new AuthenticateOutput() { Token = tokenString }; } }