如何在服务层获取用户
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
作为每个服务方法中的参数
我花了几个小时思考如何最好地实施它并提出了解决方案。我只是不完全确定我的方法是否足够并且没有违反任何软件设计原则。
分享我的代码,希望得到 Whosebug 社区的推荐。
思路如下:
首先介绍SessionProvider
注册为Singleton
services.AddSingleton<SessionProvider>();
SessionProvider
有一个 Session
属性,其中包含 User
、Tenant
等
其次,我介绍SessionMiddleware
并注册
app.UseMiddleware<SessionMiddleware>();
在 Invoke
方法中,我解析 HttpContext
、SessionProvider
和 UserManager
。
我取User
然后我初始化 Session
属性 的 ServiceProvider
单例:
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;
}
}
所以这是任何服务的 base class,如您所见,我们现在可以轻松获取 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 中都有不同的子域(租户是从子域解析的 - 正在正确获取数据)。
看来你的做法是正确的。唯一的问题 - 你不应该将 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>();
...
}
使用动作过滤器将确保在动作调用管道中足够晚地调用所需的行为,从而已经实现了必要的依赖关系,(如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
的生命周期是每个请求 - 当请求开始时我们挂钩它,解析 SessionContext
实例,然后用 UserID
& [=17 填充它=] - 之后无论我们在哪里注入我们的 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 };
}
}
我使用 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
作为每个服务方法中的参数
我花了几个小时思考如何最好地实施它并提出了解决方案。我只是不完全确定我的方法是否足够并且没有违反任何软件设计原则。
分享我的代码,希望得到 Whosebug 社区的推荐。
思路如下:
首先介绍SessionProvider
注册为Singleton
services.AddSingleton<SessionProvider>();
SessionProvider
有一个 Session
属性,其中包含 User
、Tenant
等
其次,我介绍SessionMiddleware
并注册
app.UseMiddleware<SessionMiddleware>();
在 Invoke
方法中,我解析 HttpContext
、SessionProvider
和 UserManager
。
我取
User
然后我初始化
Session
属性 的ServiceProvider
单例:
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;
}
}
所以这是任何服务的 base class,如您所见,我们现在可以轻松获取 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 中都有不同的子域(租户是从子域解析的 - 正在正确获取数据)。
看来你的做法是正确的。唯一的问题 - 你不应该将 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>();
...
}
使用动作过滤器将确保在动作调用管道中足够晚地调用所需的行为,从而已经实现了必要的依赖关系,(如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
的生命周期是每个请求 - 当请求开始时我们挂钩它,解析 SessionContext
实例,然后用 UserID
& [=17 填充它=] - 之后无论我们在哪里注入我们的 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 };
}
}