[英]How to get User at Service Layer
我使用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>();
SessionProvider
有一个Session
属性,用于保存User
, Tenant
等。
其次,我介绍SessionMiddleware
并注册它
app.UseMiddleware<SessionMiddleware>();
在Invoke
方法中,我解析了HttpContext
, SessionProvider
和UserManager
。
我取了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<ApplicationUser> 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<VocabularyDto>> GetAll()
{
List<VocabularyDto> 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中有不同的子域(租户是从子域解析的 - 数据正在被正确获取)。
使用动作过滤器可以确保在动作调用管道中调用所需的行为,以便已经实现必要的依赖关系(如HttpContext.User)
实现异步操作过滤器以避免调用.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<SessionProvider>();
var userManager = serviceProvider.GetService<MultiTenancyUserManager<ApplicationUser>>()
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
//...
}
}
在我看来,这是一个更好的解决方法 - 我们不再为每个请求进行数据库调用,我们只需从Claims检索UserID和TenantID:
请注意Session
的生命周期是Per Request - 当请求开始时我们挂钩它,解析SessionContext
实例,然后使用UserID
和TenantID
填充它 - 在我们注入Session
任何地方(给定相同的请求)之后 - 它将包含我们需要的价值观
services.AddScoped<Session>();
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<AuthenticateOutput> 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 };
}
}
你的方法似乎是正确的。 唯一的问题 - 你不应该将SessionProvider
注册为Singleton
,否则你会遇到同时请求的问题。 将其注册为Scoped
以获取每个请求的新实例。 此外,您必须在调用下一个中间件之前填充SessionInfo。 正如Nikosi所说,中间件应该用过滤器替换,以获得有关用户的正确数据。 对于过滤器实现,它使用被认为是反模式的服务定位器模式。 更好的方法是使用构造函数注入它,并且框架已经支持它。 如果您在全球范围内使用它,您只需将其注册为:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.Filters.Add<SessionFilter>();
});
}
或者如果您只需要执行某些操作,则可以应用过滤器
[ServiceFilter(typeof(SessionFilter))]
在这种情况下,过滤器也应该注册:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddScoped<SessionFilter>();
...
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.