首页 > 解决方案 > How to create context object accessible application-wide to store current user details in data access?

问题描述

I have an n-tier web application with a Web UserInterface, BusinessLogicLayer and DataAccessLayer. I am looking for the best possible solution to pass my current logged in user's details all the way to the data-access without add it to all my method signatures. The user details are needed for auditing purposes. I believe you can create a context that is available application wide, is there anyone with an example of doing this? I am looking for the best design pattern that will separate my concerns.

标签: c#asp.net-mvcsecuritydesign-patternsaudit-trail

解决方案


这里有两种方法:

首先
,如果其他层确实需要了解用户,这种方法更有意义。例如,他们可能会检查权限或根据用户是谁做出一些决定。

您可以创建一个抽象或接口来描述您希望这些类能够访问的内容,如下所示:

public interface IUserContext
{
    SomeClassContainingUserData GetCurrentUser();
}

我会根据消费者类的需要来定义用户数据类,而不是仅仅使用一些现有的Customer类来防止它与您的 Web 应用程序紧密耦合。

现在您可以将该类注入到您的其他类中:

public class MyBusinessLogicClass
{
    private readonly IUserContext _userContext;

    public MyBusinessLogicClass(IUserContext userContext)
    {
        _userContext = userContext;
    }

    public void SomeOtherMethod(Whatever whatever)
    {
        var user = _userContext.GetCurrentUser();
        // do whatever you need to with that user
    }
}

这使您的其他类保持可测试性,因为很容易注入一个返回您想要的接口的模拟,因此您可以确保您的类对于不同类型的用户行为正确。

如果您的用户数据来自,HttpContext那么您的运行时实现可能大致如下所示:

public class HttpUserContext
{
    private readonly IHttpContextAccessor _contextAccessor;

    public HttpUserContext(IHttpContextAccessor contextAccessor)
    {
        _contextAccessor = contextAccessor;
    }

    public SomeClassContainingUserData GetCurrentUser()
    {
        var httpContext = _contextAccessor.HttpContext;
        // get the user data from the context and return it.
    }
}

这是一个粗略的轮廓。一个考虑因素是范围界定。如果您的所有对象都是按请求限定的,那么IUserContext实现可以一次生成用户数据并将其存储在成员变量中,而不是一遍又一遍地访问它。

缺点是必须在任何地方注入它,但如果这些类需要这些信息,这是不可避免的。

其次
,如果这些内部类实际上根本不需要用户信息怎么办?如果您只想记录哪些用户发出了由这些类处理的请求怎么办?如果您想要一个单独的对象来检查权限怎么办?

在这种情况下,选项将是拦截器或包装器。最简单的形式可能如下所示:

public class SomeBusinessClassSecurityInterceptor : ISomeBusinessClass
{
    private readonly ISomeBusinessClass _inner;
    private readonly IUserContext _userContext;

    public SomeBusinessClassSecurityInterceptor(
        ISomeBusinessClass inner, IUserContext userContext)
    {
        _inner = inner;
        _userContext = userContext;
    }

    public void UpdateSomeData(Foo data)
    {
        if(!CanUpdate()) 
            throw new YouCantUpdateThisException();
        _inner.UpdateSomeData(data);
    }

    private bool CanUpdate()
    {
        var user = _userContext.GetCurrentUser();
        // make some decision based on the user
    }   
}

如果检索用户的权限更复杂,您可能希望拥有一个IUserPermissions并注入它而不是IUserContext. 然后注入IUserContextIUserPermissions. 在运行时,它检索当前用户,然后做自己的事情来确定用户拥有什么权限。

如果你有很多类和方法,那么维护单独的包装类可能会变得乏味。另一种选择是使用拦截器,这可能意味着使用不同的依赖注入容器,如WindsorAutofac。这些对于日志记录特别有用。

以 Autofac 为例,这意味着编写这样的类:

public class LoggingInterceptor : IInterceptor
{
    private readonly IUserContext _userContext;
    private readonly ILogger _logger;

    public CallLogger(IUserContext userContext, ILogger logger)
    {
        _userContext = userContext;
        _logger = logger;
    }

    public void Intercept(IInvocation invocation)
    {
        var user = _userContext.GetCurrentUser();
        _logger.Log( 
         // some stuff about the invocation, like method name and maybe parameters)
         // and who the user was.
         // Or if you were checking permissions you could throw an exception here.

        invocation.Proceed();
    }
}

然后你会告诉容器所有对给定类的“真实”实现的调用都通过这个拦截器(在他们的文档中有很好的描述。)

如果您正在检查权限,则可以注入IUserPermissions. 拦截器可以检查内部方法的属性,该属性指定需要哪些权限,并将其与当前用户的权限进行比较。

您通常可以编写一个拦截器并将其与许多其他类一起使用,因为它不需要了解有关内部目标类的任何信息。但是,如果您需要,您还可以编写更多用途更窄的拦截器,仅用于某些类。

好的是它为您提供了很大的灵活性,但不会触及您的业务逻辑或数据类的实际接口或实现。他们可以专注于他们的单一职责,而其他类被配置为记录对他们的请求或检查用户权限。


推荐阅读