如何在服务层获取用户

Posted

技术标签:

【中文标题】如何在服务层获取用户【英文标题】:How to get User at Service Layer 【发布时间】:2019-01-12 04:45:22 【问题描述】:

我使用 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单一服务方式

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

分享我的代码,希望得到 *** 社区的建议。

思路如下:

首先,我介绍SessionProvider,它注册为Singleton。

services.AddSingleton<SessionProvider>();

SessionProvider 有一个 Session 属性,其中包含 UserTenant 等。

其次,我介绍SessionMiddleware并注册它

app.UseMiddleware<SessionMiddleware>();

Invoke 方法中,我解析了HttpContextSessionProviderUserManager

我获取User

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

sessionProvider.Initialise(user);

在这个阶段,ServiceProviderSession 对象,其中包含我们需要的信息。

现在我们将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 类,如您所见,我们现在可以轻松获取 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 中有不同的子域(租户从子域解析 - 数据被正确获取)。

【问题讨论】:

如果这是工作代码并且除了认为它不是一个好的设计之外确实没有实际问题,那么我会说这个问题对于 SO 来说是题外话,因为它更像是一个代码审查应该适合codereview.stackexchange.com @Nkosi 啊,我明白了。下次我会在codereview 上问这种问题。谢谢! 请注意,您过于关注抽象可以很好地工作的实现问题。抽象会话提供者简化了服务的依赖关系。因此,您使用IHttpContextAccessor 获取当前用户甚至都没有关系。再次,这只是我对这个问题的主题的看法.. 我也刚刚注意到,在将上下文传递到管道之后,您正在中间件中设置会话,这意味着该会话对管道中的其他处理程序不可用 嗨@Nkosi,你是绝对正确的。我刚刚尝试将ServiceProvider 注册为作用域,而Session 的所有字段都为空。 【参考方案1】:

使用动作过滤器将确保在动作调用管道中足够晚地调用所需的行为,以便已经实现了必要的依赖关系,(如 HttpContext.User)

参考Filters in ASP.NET Core

实施异步操作过滤器以避免调用 .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
        //...
    

【讨论】:

谢谢,非常有用。今天早些时候删除了.Result,但偶然从剪贴板中复制粘贴了它。修改了我的代码并进行了测试 - 工作! 完美解决方案【参考方案2】:

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

请注意Session 的生命周期是每个请求 - 当请求开始时,我们连接到它,解析 SessionContext 实例,然后用 UserIDTenantID 填充它 - 在这之后我们注入我们的 @ 987654325@(给定相同的请求) - 它将包含我们需要的值。

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 ;
    

【讨论】:

【参考方案3】:

您的方法似乎是正确的。唯一的问题 - 你不应该将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>();
    ...

【讨论】:

嗨,亚历克斯,感谢您的回答!也试过Scoped - 由于某种原因不起作用。稍后我将在 GitHub 上发布我的代码 - 您将能够更详细地查看。 顺便说一句,我们将HttpContextAccessor 注册为单例,它适用于多个请求 - 对吧?那么SessionProvider单例也不应该有问题吗? (除非我错过了什么) @AlexHerman HttpContextAccessor 没有状态,而 SessionProvider 有

以上是关于如何在服务层获取用户的主要内容,如果未能解决你的问题,请参考以下文章

如何从地理服务器中的图层获取样式

如何在服务层单元测试中模拟数据库结果?

从服务层获取 Base URL

服务层中的 JsonResult

如何在 Spring Junit 中创建会话

如何在服务层模拟方法