Java

       

Авторизация


Самая интересная часть системы защиты. В большинстве случаев применения ООП получаются сложные, сильно связные подсистемы, не позволяющие повторное использование в других проектах. Давайте разберемся почему.

Итак, авторизация – разрешение/запрет действий, выполняемых аутентифицированными пользователями. Вполне логично было бы разместить авторизацию в компоненте «Контроллер», но если, например, несколько контроллеров оперируют с одними и теми же объектами модели, то получается дублирование функций, поэтому логично было бы разместить авторизацию непосредственно в объектах модели. Для примера рассмотрим удаление альбома по названию (в нашей простой версии нет ID альбома):

, обрабатываемый сервлетом, который с начала находит альбом, который нужно удалить (вызов номер 2), а потом пытается его удалить из хранилища альбомов (AlbumList – класс модели). Далее, хранилище альбомов проверяет, может ли текущий пользователь удалить выбранный альбом, используя класс системы авторизации (AuthHelper) и если такое разрешение получено, то удаляет альбом. Вызов номер 6 перенаправляет запрос на слой отображения.

На даиграмме взаимодействия видно, что для проверки авторизации нам необходимо знать пользователя (вызов номер 4 имеет 2 параметра – пользователь и объект который он пытается удалить), то есть нам необходимо передать объект пользователь в методе номер 3. Следовательно, в каждый метод модели нам необходимо дополнительно передавать текущего пользователя, а если контроллер не является объектом, непосредственно получающим запрос (а как мы помним, объект User храниться в сессии), то и в каждый метод контроллера нужно добавить такой параметр.

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

Предположим, что мы рассматриваем реальную систему, в которой десятки, если не сотни объектов модели. В данном случае никто не сможет дать твёрдой гарантии, что все вызовы методов приводящих к изменению модели окружены такими проверками.

Есть один не маловажный нюанс – ссылка «Удалить» будет всё ещё отображаться напротив всех альбомов, в том числе не принадлежащих текущему пользователю. Безусловно, при нажатии на эту ссылку удаления чужых альбомов происходить не будет, но всё же ссылка на удаление не должна отображаться.


Здесь видно, что « Before advice» созданный на основе «Read access pointcut» перехватывает операцию getTitle объекта модели (вызов номер 5 уже производится аспектом) и взывает класс, инкапсулирующий правила авторизации для проверки операции чтения: boolean isAllowedRead(AnonymousUser, Object). Как видно из сигнатуры метода для проверки нам необходим текущий пользователь. Здесь нам на помощь приходит паттерн "червоточина" (Wormhole) (Статья на английском   раздел "Replace argument trickle by wormhole"). В двух словах этот паттерн позволяет нам неявно передавать параметры контекста одного из одной части системы в другую часть, находящуюся, например, в одном потоке выполнения. Реализуется он следующим образом:

В результате, мы получили pointcut, который даёт нам доступ, как к объекту текущего запроса, так и к объекту над которым производится интересующее нас действие. Обратите внимание, что перехват события будет производиться только в потоке выполнения, включающем в себя фильтр, то есть если мы, например, будем запускать unit тесты модели, тестирующие классы напрямую, то никакой проверки происходить не будет (что естественно, т.к. не откуда будет взять пользователя), что сильно упрощает тестирование.

Итак, с перехватом выполнения методов разобрались, теперь необходимо обработать исключительные ситуации, то есть когда пользователь не авторизован выполнить действие. В данном случае before advice выбросит unchecked exception AuthorizationException и тем самым не даст выполниться не разрешённому методу. Для того, что бы отобразить пользователю цивилизованное сообщение об ошибке, или обработать эту ситуацию другим способом, мы перехватываем исключение, точно так же как и в предыдущем аспекте.

Полученный код аспекта приведён ниже:

package aop.example;

import aop.example.model.*; import javax.servlet.*; import javax.servlet.http.*; import java.io.IOException;



/** * Авторизационный аспект * @author Zubairov Renat */ public aspect AuthorizationAspect {



/** * pointcut включающий в себя метод
с ServletRequest для * того что бы потом можно было
бы получить его в объединении * с другим pointcut (паттерн
"червоточина") */ pointcut requestMethod
(ServletRequest request) : execution(* aop.example.EntranceFilter.
doFilter(ServletRequest, ServletResponse,
FilterChain)) && args(request,
ServletResponse, FilterChain);

/** * pointcut определяющий метод фильтра
в котором мы будем отлавливать * исключение */ public pointcut doFilterMethod
(ServletRequest srequest, ServletResponse sresponse
, EntranceFilter filter) : execution(void aop.example.
EntranceFilter.doFilter(ServletRequest,
ServletResponse, FilterChain)) && args(srequest, sresponse,
FilterChain) && this(filter);

// все методы производящие чтение информации
объектов модели pointcut readMethods(Object object)
: execution (public * aop.example.model.*.get*(..))
&& this(object);

// все методы производящие добавление
объектов модели pointcut addMethods(Object object)
: execution (public * aop.example.model.*.add*(..))
&& this(object);

// все методы производящие удаление
объектов модели pointcut deleteMethods(Album album)
: execution (public * aop.example.model.
AlbumList.deleteAlbum(Album)) && args(album);

// Вызов методов чтения произошедшие в
потоке выполнения // следующим за вызовом метода фильтра // мы объеденили два pointcut - реализация
паттерна "червоточина" pointcut readAccess(ServletRequest request,
Object object) : cflow(requestMethod(request)) &&
readMethods(object);

// то же самое только для добавления pointcut addAccess(ServletRequest request,
Object object) : cflow(requestMethod(request)) &&
addMethods(object);

// то же самое только для удаления pointcut deleteAccess(ServletRequest
request, Album album) : cflow(requestMethod(request))
&& deleteMethods(album); /** * Before advice проверки на чтение */ before(ServletRequest request, Object object)
: readAccess(request, object) { if (!AuthHelper.isAbleToRead
(extractUser(request), object)) { throw new AuthorizationException
("Read access not allowed"); } }



/** * Before advice проверки на добавление */ before(ServletRequest request, Object object)
: addAccess(request, object) { if (!AuthHelper.isAbleToAdd(extractUser
(request), object)) { throw new AuthorizationException
("Add access not allowed"); } }

/** * Before advice проверки на удаление */ before(ServletRequest request, Album album) :
deleteAccess(request, album) { if (!AuthHelper.isAbleToDelete
(extractUser(request), album)) { throw new AuthorizationException
("Delete access not allowed"); } }

/** * Around advice отлавливающий исключение * и отправляющий запрос на страницу с ошибкой */ void around(ServletRequest srequest,
ServletResponse sresponse, EntranceFilter filter)
throws IOException, ServletException : doFilterMethod(srequest,
sresponse, filter) { try { // выполняем метод фильтра proceed(srequest, sresponse, filter); } catch (AuthorizationException e) { // ловим исключение srequest.setAttribute("error_message",
e.getMessage()); // вперёд на страницу с сообщением
об ошибке filter.getConfig().getServletContext()
.getRequestDispatcher("error.vm")
.forward(srequest, sresponse); } }

/** * Приватная фунция которая вынимает
пользователя из запроса */ private AnonymousUser extractUser
(ServletRequest request) { return (AnonymousUser)((HttpServletRequest)
request).getSession().getAttribute(EntranceFilter.USER_KEY); }

}

Обратите внимание, что для удаления альбома мы используем немного отличный от остальных pointcut, это связанно с тем, что метод удаляющий альбомы принадлежит не классу Album, а классу AlbumList, кроме того, альбом, подлежащий удалению, передаётся как параметр метода. Как видно из решения язык определения pointcut AspectJ с лёгкостью справился и с такой задачей.

Для примера попробуйте ввести в строку браузера следующий запрос:

http://localhost:8080/view?delete=Picture%20of%20%3Cb%3Euser2%3C/b%3E

Будучи не зарегистрированным в системе (должна отобразиться страница с логином), или под пользователем User1 (должна отобразиться страница с ошибкой, т.к. производится попытка удалить альбом, не принадлежащий текущему пользователю).



Пред-проверка

Для того, что бы реализовать оповещение слоя отображения о действиях которые разрешено производить с объектом модели, мы создадим дополнительный интерфейс который будет содержать 3 метода - boolean isReadable(),boolean isDeletable() и boolean isAddable() после чего создадим в классе Album методы реализующие данный интерфейс и возвращающие всегда true. Слой отображения будет отображать ссылку «удалить» только если метод isDeletable() даст добро. А в нашем авторизационном аспекте создадим pointuct перехватывающие соответствующие методы всех классов реализующих данный интерфейс. После чего создадим around advice, который будет возвращать результат вызова системы авторизации. Захват параметров производится точно так же, как это было сделано при пост-проверке.

Изменённый аспект:

package aop.example;

import aop.example.model.*; import javax.servlet.*; import javax.servlet.http.*; import java.io.IOException;

/** * Авторизационный аспект * @author Zubairov Renat */ public aspect AuthorizationAspect {

/** * pointcut включающий в себя метод
с ServletRequest для * того что бы потом можно было
бы получить его в объединении * с другим pointcut (паттерн "червоточина") */ pointcut requestMethod
(ServletRequest request) : execution(* aop.example.EntranceFilter.
doFilter(ServletRequest, ServletResponse,
FilterChain)) && args(request, ServletResponse,
FilterChain);

/** * pointcut определяющий метод фильтра
в котором мы будем отлавливать * исключение */ public pointcut doFilterMethod
(ServletRequest srequest, ServletResponse
sresponse, EntranceFilter filter) : execution(void aop.example.
EntranceFilter.doFilter(ServletRequest,
ServletResponse, FilterChain)) && args(srequest,
sresponse, FilterChain) && this(filter);

// все методы производящие чтение информации
объектов модели pointcut readMethods(Object object)
: execution (public * aop.example.model.
*.get*(..)) && this(object);

// все методы производящие добавление
объектов модели pointcut addMethods(Object object)
: execution (public * aop.example.model.
*.add*(..)) && this(object);



// все методы производящие удаление
объектов модели pointcut deleteMethods(Album album)
: execution (public * aop.example.
model.AlbumList.deleteAlbum(Album)) && args(album);

// методы проверки на доступность
чтения (пред-проверка) pointcut controlledRead(Object object)
: execution(public boolean aop.example.model.
Controlled+.isReadable()) && this(object);

// методы проверки на доступность
добавления (пред-проверка) pointcut controlledAdd(Object object)
: execution(public boolean aop.example.model.Controlled+.
isAddable()) && this(object);

// методы проверки на доступность
удаления (пред-проверка) pointcut controlledDelete(Object object)
: execution(public boolean aop.example.model.
Controlled+.isDeletable()) && this(object);

// Вызов методов чтения произошедшие в потоке
выполнения // следующим за вызовом метода фильтра // мы объеденили два pointcut - реализация
паттерна "червоточина" pointcut readAccess(ServletRequest request,
Object object) : cflow(requestMethod(request))
&& readMethods(object);

// то же самое только для добавления pointcut addAccess(ServletRequest request,
Object object) : cflow(requestMethod(request)) &&
addMethods(object);

// то же самое только для удаления pointcut deleteAccess(ServletRequest request,
Album album) : cflow(requestMethod(request)) &&
deleteMethods(album);

// пред-проверка на чтение pointcut readCheck(ServletRequest request,
Object object) : cflow(requestMethod(request)) &&
controlledRead(object);

// пред-проверка на добавление pointcut addCheck(ServletRequest request,
Object object) : cflow(requestMethod(request)) &&
controlledAdd(object);

// пред-проверка на удаление pointcut deleteCheck(ServletRequest request,
Object object) : cflow(requestMethod(request)) &&
controlledDelete(object);

/** * Around advice отлавливающий исключение * и отправляющий запрос на страницу с ошибкой */ void around(ServletRequest srequest,
ServletResponse sresponse, EntranceFilter filter)
throws IOException, ServletException :
doFilterMethod(srequest, sresponse, filter) { try { // выполняем метод фильтра proceed(srequest, sresponse, filter); } catch (AuthorizationException e) { // ловим исключение srequest.setAttribute("error_message",
e.getMessage()); // вперёд на страницу с сообщением
об ошибке filter.getConfig().getServletContext()
.getRequestDispatcher("error.vm").
forward(srequest, sresponse); } }



/** * Before advice проверки на чтение */ before(ServletRequest request, Object object)
: readAccess(request, object) { if (!AuthHelper.isAbleToRead
(extractUser(request), object)) { throw new AuthorizationException
("Read access not allowed"); } }

/** * Before advice проверки на добавление */ before(ServletRequest request, Object object)
: addAccess(request, object) { if (!AuthHelper.isAbleToAdd
(extractUser(request), object)) { throw new AuthorizationException
("Add access not allowed"); } }

/** * Before advice проверки на удаление */ before(ServletRequest request, Album album)
: deleteAccess(request, album) { if (!AuthHelper.isAbleToDelete
(extractUser(request), album)) { throw new AuthorizationException
("Delete access not allowed"); } }

/** * Around advice пред-проверки, здесь
мы игнорируем возвращаемое методом * значение, и всё время возвращаем
то которое удовлетворяет правилам * авторизации * Мы не обрабатываем остальные пред-проверки
т.к. по умолчанию любой может * читать, и все аутентифицированные
пользователи могут добавлять */ boolean around(ServletRequest request,
Object object) : deleteCheck(request, object) { return AuthHelper.isAbleToDelete
(extractUser(request), object); }

/** * Приватная фунция которая вынимает
пользователя из запроса */ private AnonymousUser extractUser
(ServletRequest request) { return (AnonymousUser)((HttpServletRequest)
request).getSession().getAttribute
(EntranceFilter.USER_KEY); }

}

Достоинства АОП решения:

Как видно из определения аспектов readMethods, addMethods, controlledRead, controlledAdd и controlledDelete мы не ссылаемся на определённые классы, следовательно, авторизация будет автоматически распространяться на все новые классы модели помещённые в пакет model и реализующие интерфейс Controllable.

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

Все вызовы методов классов модели (созданные согласно правилам) будут объектом для применения системы авторизации.

Слой отображения не зависит от системы авторизации.

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

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


Содержание раздела