繁体   English   中英

如何在服务层获取用户

[英]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属性,用于保存UserTenant等。

其次,我介绍SessionMiddleware并注册它

app.UseMiddleware<SessionMiddleware>();

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

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的生命周期是Per Request - 当请求开始时我们挂钩它,解析SessionContext实例,然后使用UserIDTenantID填充它 - 在我们注入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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM