Как создать контекстный объект, доступный для всего приложения, для хранения текущих сведений о пользователе в доступе к данным?

#c# #asp.net-mvc #security #design-patterns #audit-trail

#c# #asp.net-mvc #Безопасность #шаблоны проектирования #аудит-след

Вопрос:

У меня есть n-уровневое веб-приложение с веб-интерфейсом пользователя, BusinessLogicLayer и DataAccessLayer. Я ищу наилучшее возможное решение для передачи данных моего текущего зарегистрированного пользователя вплоть до доступа к данным, не добавляя их ко всем моим сигнатурам методов. Сведения о пользователе необходимы для целей аудита. Я полагаю, вы можете создать контекст, доступный для всего приложения, есть ли у кого-нибудь пример этого? Я ищу лучший шаблон проектирования, который разделит мои проблемы.

Ответ №1:

Вот два подхода:

Первый
Этот подход имеет больше смысла, если эти другие уровни действительно должны знать о пользователе. Например, они могут проверять разрешения или принимать какое-либо решение на основе того, кто является пользователем.

Вы можете создать абстракцию или интерфейс, чтобы описать, к чему вы хотите, чтобы эти классы могли получить доступ, что-то вроде этого:

 public interface IUserContext
{
    SomeClassContainingUserData GetCurrentUser();
}
  

Я бы определил этот класс пользовательских данных в соответствии с тем, что нужно потребительскому классу, вместо того, чтобы просто использовать какой-то существующий Customer класс, чтобы он не был тесно связан с вашим веб-приложением.

Теперь вы можете внедрить этот класс в другие свои классы:

 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 . Затем внедрите IUserContext в реализацию IUserPermissions . Во время выполнения он извлекает текущего пользователя, а затем выполняет свои собственные действия, чтобы определить, какие разрешения у пользователя.

Если у вас много классов и методов, то поддержание отдельных классов-оболочек может стать утомительным. Другой вариант — использовать перехватчик, который, вероятно, означает использование другого контейнера для внедрения зависимостей, такого как Windsor или Autofac. Они особенно хороши для ведения журнала.

Используя 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 . Перехватчик может проверить наличие атрибута во внутреннем методе, который указывает, какие разрешения необходимы, и сравнить его с разрешениями текущего пользователя.

Обычно вы можете написать один перехватчик и использовать его с множеством других классов, потому что ему не нужно ничего знать о внутреннем целевом классе. Но если вам нужно, вы также можете написать перехватчики более узкого назначения, используемые только для определенных классов.

Что приятно, так это то, что это дает вам большую гибкость, но не затрагивает фактический интерфейс или реализацию вашей бизнес-логики или классов данных. Они могут сосредоточиться на своих отдельных обязанностях, в то время как другие классы настроены для регистрации запросов, отправленных им, или проверки разрешений пользователя.

Комментарии:

1. Это было блестящее объяснение, и многое из того, что вы упомянули, было моим мышлением, но я не был уверен, как изложить свои мысли. На самом деле мне нужно будет найти промежуточное звено между обоими вариантами, поскольку мне нужно реализовать обе логики, которые требуют знать информацию о пользователе для выполнения действий, а также мне нужно войти в систему для целей аудита. Спасибо