如何在服务层获取用户

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);

问题:

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

分享我的代码,希望得到 Whosebug 社区的推荐。

思路如下:

首先介绍SessionProvider注册为Singleton

services.AddSingleton<SessionProvider>();

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

其次,我介绍SessionMiddleware并注册

app.UseMiddleware<SessionMiddleware>();

Invoke 方法中,我解析 HttpContextSessionProviderUserManager

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 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)

引用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
        //...
    }
}

我认为这是一个更好的解决方法 - 我们不再对每个请求进行数据库调用,我们只是从 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 };
    }
}