如何在服务层获得用户

时间:2018-08-04 15:27:46

标签: c# asp.net-core asp.net-core-middleware

我使用ASP.NET Core 2.1,并希望以服务级别获取User

我已经看到了将HttpContextAccessor注入某些服务然后通过User获取当前UserManager的示例

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

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

sessionProvider.Initialise(user);

现阶段ServiceProvider的{​​{1}}对象包含我们所需的信息。

现在,我们将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);
        }
    }
}

这是任何服务的 base 类,如您所见,我们现在可以轻松获取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

就是这样。

我测试了我的代码,当打开多个选项卡时,它很好用-每个选项卡在url中都有不同的子域(租户是从子域解析的-数据已正确提取)。

3 个答案:

答案 0 :(得分:5)

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

引用Filters in ASP.NET Core

实施异步操作过滤器,以避免调用$.data[?(@.program.seriesId)][0].value 阻止调用,因为这可能会导致请求管道中出现死锁。

.Result

答案 1 :(得分:3)

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

请注意,Session的生存期是每个请求-当请求启动时,我们将其挂接到它,解析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 };
    }
}

答案 2 :(得分:2)

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