Как стать автором
Обновить

Postsharp: авторизация и аутентификация

Время на прочтение6 мин
Количество просмотров1.6K
Автор оригинала: Matthew D. Groves
Одно из самых востребованных применений аспектно-ориентированного программирования это вынос обслуживающего инфраструктурного кода, который часто повторяется в системе, в отдельный класс. Что в свою очередь является проявленем принципа единой ответственности (SRP — Single Responsibility Principle). Очень часто задачи авторизации и аутентификации разбросаны по всему коду проверяя права доступа пользователя. Следствием этого является большая трудоемкость изменений в этой немаловажной части логики, а так же ее общая проверка. Принцип единой ответственности говорит о том, что должна быть только одна причина для изменения класса, так что все что относится к авторизации и аутентификации просто просится в отдельные классы.

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



Авторизация же, напротив, является хорошим примером использования PostSharp. Слишком часто можно видеть как логика разрешения действий на основе ролей пользователя ужасно размазана по всему приложению. Как раз в таких ситуациях PostSharp может быть использован чтобы убрать «грязь»и оставить только значимый код. К тому же иногда безопасность основанная на ролях слишком обширна и нужен тонкий инструмент для регулирования доступа. Например ограничить редактирование данных всем, кроме создателя. Итак, давайте рассмотрим простое приложение, которое очень похоже на то, с которым я работал будучи консультантом и разберемся, как PostSharp может помочь.

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

Не буду приводить здесь полный код программы, ограничусь сервисным классом который предоставляет базовую функциональность для указаных действий. Он будет использовать статическую коллекцию в роли хранилища, но в настоящем приложении конечно будут использованы БД, сервисы и прочее.
public class GovtFormService : IGovtFormService{
	private static readonly IList _govtFormsDatabase = new List();

	public GovtFormService() {
		// build up some initial entries of the static list
	}

	public void SubmitForm(GovtForm form) {
		_govtFormsDatabase.Add(form);
	}

	public IEnumerable GetAllForms() {
		return _govtFormsDatabase;
	}

	public GovtForm GetFormById(Guid guid) {
		return _govtFormsDatabase.FirstOrDefault(form => form.FormId == guid);
	}
}

Здесь мы просто связываем форму с сервисом и назначаем на нажатия кнопок методы из сервиса чтобы в итоге получить базовое приложение. Однако требования четко говорят о том, что пользователь должен иметь возможность просматривать только свои запросы. GetFormById на данный момент не делает никаких проверок. Можно добавить несколько условий, но лучше сразу создадим аспект, который можно будет везде использовать.
[Serializable]
public class AuthorizeReturnValueAttribute : OnMethodBoundaryAspect{
        [NonSerialized] private IAuth Auth;

        public override void RuntimeInitialize(System.Reflection.MethodBase method) {
                Auth = new AuthService();
        }

        public override void OnSuccess(MethodExecutionArgs args) {
                var singleForm = args.ReturnValue as GovtForm;
                if (singleForm != null) {
                        if(Auth.CurrentUserHasPermission(singleForm, Permission.Read)) {
                                MessageBox.Show(
                                 "You are not authorized to view the details of that form",
                                 "Authorization Denied!");
                                args.ReturnValue = null;
                        }
                        return;
                }
        }
}

Один из способов реализации в том, чтобы проверять возвращает ли перехваченый метод тип GovtForm. Если да, то проверить принадлежит ли данная форма запроса пользователю. Заметьте, что поле типа IAuth помечено как не сериализуемое, и что оно инициализирутеся в переопределенном методе RuntimeInitialize. В данном случае использовался хардкод, но вы можете использовать IoC контейнер в качестве сервис локатора (см перевод о Dependency Injection).

Добавим еще немного кода в аспект, чтобы можно было работать с методами возвращающими коллекцию GovtForm и фильтровать доступные формы запросов для текущего пользователя.
var formCollection = args.ReturnValue as IEnumerable;
if (formCollection != null) {
        args.ReturnValue = formCollection
                        .Where(f => Auth.CurrentUserHasPermission(f, Permission.Read));
        return;
}

Класс GovtForm ничем не примечателен кроме того, что в нем реализовано свойство UserName. Вы можете добавить интерфейс ISecurable к каждому бизнес-объекту который захотите обрабатывать в соответствии с предложенным подходом. Тогда метод CurrentUserHasPermission можно будет переписать, чтобы он принимал аргумент типа ISecurable. После этого остается только добавить атрибут AuthorizedRecordsOnly ко всем сервисам и репозиториям, которые возвращают единичный бизнес-объект или их коллекцию, чтобы пользователь видел только свои формы запросов. При таком подходе вам не придется значительно менять код для UI или сервисов.

Теперь, когда вы уже знакомы с некоторыми полезными аспектами, настало время рассмотреть более комплексный пример. Представьте, что у вас есть одновременно аспект для кэширования И аспект авторизации для конкретного метода. Логично предположить, что кэширование должно идти после авторизации, иначе доступ к кэшированным данным можно получить без прав доступа. Так как же заставить аспект авторизации отработать раньше кэширования?

Вот как это можно сделать:
  1. Примените атрибут ProvideAspectRoleAttribute к интересующим аспектам. В качестве роли может быть строка или вы можете использовать перечисление StandardRoles из пространства PostSharp.
  2. Примените атрибут AspectRoleDependencyAttribute к интересующим аспектам чтобы указать тип зависимости и роль от которых они зависят.

Для решения упомянутой выше проблемы надо применить атрбуты к аспектам примерно так:
[Serializable]
[AspectRoleDependency(AspectDependencyAction.Order,
        AspectDependencyPosition.Before, StandardRoles.Caching)]
public class AuthorizeReturnValueAttribute : OnMethodBoundaryAspect{  }

[Serializable]
[ProvideAspectRole(StandardRoles.Caching)]
public class CachingAttribute : OnMethodBoundaryAspect {
        public override void OnEntry(MethodExecutionArgs args) {
                // do caching stuff
        }

        public override void OnSuccess(MethodExecutionArgs args) {
                // do caching stuff
        }
}

С помощью этого кода мы сказали PostSharp, что аспект кэширования создается с ролью Caching и мы так же явно указали, что аспект авторизации должен быть применен до того как вступит в силу любой другой аспект с ролью Caching.

А вот как будет выглядеть код метода GetAllForms после того как применятся оба аспекта (получено с помощью программы в духе Reflector):
public IEnumerable GetAllForms(){
        MethodExecutionArgs CS$0$2__aspectArgs = new MethodExecutionArgs(null, null);
        <>z__Aspects.a1.OnEntry(CS$0$2__aspectArgs);
        IEnumerable CS$1$1__returnValue = _govtFormsDatabase;
        <>z__Aspects.a1.OnSuccess(CS$0$2__aspectArgs);
        CS$0$2__aspectArgs.ReturnValue = CS$1$1__returnValue;
        <>z__Aspects.a0.OnSuccess(CS$0$2__aspectArgs);
        return (IEnumerable) CS$0$2__aspectArgs.ReturnValue;
}

И для сравнения код, если все указать верно и поменять AspectDependencyPosition на Before:
public IEnumerable GetAllForms(){
        <>z__Aspects.a1.OnEntry(null);
        MethodExecutionArgs CS$0$2__aspectArgs = new MethodExecutionArgs(null, null);
        IEnumerable CS$1$1__returnValue = _govtFormsDatabase;
        CS$0$2__aspectArgs.ReturnValue = CS$1$1__returnValue;
        <>z__Aspects.a0.OnSuccess(CS$0$2__aspectArgs);
        CS$1$1__returnValue = (IEnumerable) CS$0$2__aspectArgs.ReturnValue;
        <>z__Aspects.a1.OnSuccess(null);
        return CS$1$1__returnValue;
}

Заметьте, что а1 (кэширование) и а0 (авторизация) поменялись местами.

PostSharp предоставляет широкие возможности по настройке зависимостей аспектов. В вашем распоряжении 5 действий для разрешения зависимостей: Commute, Conflict, Order, Require, and None. Кроме того вы может создать бесконечное количество ролей, если их не хватит в перечислении StandardRoles.  К счастью, вам не часто понадобятся эти возможности, но хорошо о них знать, чтобы использовать когда придет время. Самое приятное в этом то, что можно расслабится, зная, что даже самый новый член команды не выдаст на UI данные к которым нужна авторизация, так как будет не важно в каком порядке он применяет аспекты к коду.

MatthewD. Groves инженер по разработке ПО в Telligent. Его блог mgroves.com.

PostSharp: Логирование и аудит, кэширование, отложенная загрузка зависимостей.
Теги:
Хабы:
+6
Комментарии0

Публикации

Изменить настройки темы

Истории

Работа

.NET разработчик
72 вакансии

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн