Сегодня поговорим об секьюрити в web (да, наверное, и не только) приложениях. Прежде чем описывать подходы и фреймворки расскажу небольшую предысторию.


Предыстория


За много лет работы в IT приходилось сталкиваться с проектами в самых разных сферах. У каждого проекта были свои требования к безопасности. Если в части аутентификации все было более-менее одинаково с точки зрения требований, то способы реализации механизма авторизации получались довольно разными от проекта к проекту. Каждый раз авторизацию приходилось писать практически с нуля под конкретные цели проекта, разрабатывать архитектурное решение, потом дорабатывать с изменением требований, тестировать, и т.д. — все это обычный процесс, которого не избежать в разработке. С каждой реализацией очередного такого архитектурного подхода все больше складывалось ощущение, что можно придумать какой-то общий подход, который будет покрывать основные цели авторизации и который можно будет использовать повторно в других приложениях. В данной статье будет рассмотрен обобщенный архитектурный подход к авторизации на примере разработанного фреймворка.


Подходы к созданию фреймворка


Как обычно, прежде чем разрабатывать что-то новое нужно определиться с тем, какие проблемы будут решаться, чем фреймворк будет удобен и полезен и, возможно, уже есть готовое решение (об этом поговорим после).


Всем известны два стиля написания кода — императивный и декларативный. Императивный стиль описывает то, как получить результат, декларативный — что требуется получить в результате.


Декларативный стиль удобен тем, что нужно минимум усилий и времени, чтобы обозначить желаемый результат. Например, для авторизации это может быть сделано в виде описания ролей пользователя для доступа к ресурсу, разрешений (permissions) и т.д.
Однако все задачи (как минимум для целей авторизации) декларативный стиль не решает, да и не может решить. Императивный же стиль удобен тем, что он дает необходимую гибкость в реализации. Например (в авторизации), как будет реализован механизм назначения пермишенов пользователям — статически или динамически, от чего они будут зависеть.


Фреймворк для решения всех целей и задач, очевидно, построить не получится. Необходимо выделить архитектурный каркас, который будет общим для всех, и оставить реализацию логики авторизации на усмотрение пользователя — очень похоже на концепцию абстракции в сфере разработки. Всегда была дилемма насчет уровня абстракции: слишком абстрактный фреймворк с одной стороны является очень гибким, но с другой требует много дополнительной реализации, менее абстрактный — не столь гибок, но требует минимум дополнительной реализации.


Создание фреймворка для авторизации


При создании фреймворка было решено преследовать следующие цели:


  1. Простота в использовании — чтобы избавить пользователей от чтения многостраничного мануала, настроек и т.д.
  2. Гибкость — фреймворк должен адаптироваться под различные цели и приложения
  3. Надежная обработка ошибок

Декларативный стиль


Реализацию авторизации декларативно можно выполнить различными способами: при помощи файлов конфигурации (например xml, yaml, properties), используя java annotations.
Было решено остановиться на последнем, в силу того, что:


  1. Java annotations это инструмент как самого языка java, так и JVM, что позволяет обрабатывать аннотации как во время runtime, так и во время compile time.
  2. Аннотации удобны в использовании, т.к. сразу наглядно видно какой ресурс чем ограничен.
  3. Аннотации достаточно гибки в конфигурации, т.к. являются частью языка java.

Способ реализации авторизации


Есть много способов реализации:


  • это может быть авторизация, основанная на ролях пользователя (очень удобна в приложениях с небольшой гранулярностью ролей, например это может быть Admin, Viewer, Editor)
  • может быть авторизация, основанная на правах (permissions) пользователя (удобна в приложениях с более гранулярным распределением прав, т.е. когда обычного набора ролей не хватает)
  • может быть авторизация, основанная на действия (или actions) пользователя (также удобна в случае гранулярного распределением прав), т.е. вместо того, чтобы декларативно обозначать какие права (роли или пермишены) нужны для доступа к ресурсу, обозначается действие, которое выполняет пользователя с ресурсом (например create, modify, delete). Действия могут быть самыми разными, на сколько хватит фантазии и требований. Также action-based авторизация удобна тем, что впоследствии нет необходимости менять права доступа — права декларативно описываются действием, а действие с ресурсом обычно не меняется, меняются права, который необходимы для совершения действия.

Конфигурация и обработка ошибок


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


Как уже говорилось, было решено использовать java annotations для реализации авторизации в декларативном стиле. Еще одним преимуществом такого выбора является обработка ошибок конфигурации во время компиляции — т.е. на более ранней стадии. Java предоставляет механизм Annotation Processing, который позволяет обрабатывать аннотации во время компиляции программы.


Тут еще можно в пример привести Java Module System, разработанную Oracle, которая вышла вместе с JDK 9, одним из важных достоинств которой является именно обработка ошибок во время компиляции.


Уровень абстракции


Абстракция во фреймворке выполнена следующим образом:


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

Easy-ABAC Framework


Фреймворк был разработан на основе вышеописанных поставленных целей и способов.


Давайте рассмотрим данный фреймворк в использовании на простом Spring Boot проекте.
Для начала добавим зависимость в проект (будем использовать maven):


<dependency>
  <groupId>com.exadel.security</groupId>
  <artifactId>easy-abac</artifactId>
  <version>1.1</version>
</dependency>

На момент написания статьи последней версией является 1.1.


Добавляем конфигурацию, это нужно для подключения аспектов фреймворка:


@SpringBootApplication
@Import(AbacConfiguration.class)
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Предположим у нас есть ресурс Project к которому нужно ограничить доступ. Создадим весь необходимый каркас, как описано в документации.


1. Описание необходимых действий


Предположим, что в нашем приложении есть следующие роли пользователей:


  • Админ
  • Разработчик
  • Владелец проекта

Определим возможные действия с проектом:


  • Просмотреть
  • Редактировать
  • Закрыть
  • Удалить

(отмечу, что действия могут быть самыми различными, например редактировать только открытые проекты, просматривать только свои проекты и т.д. — сколько хватит фантазии и требования к авторизации в приложении).


Опишем это средствами фреймворка:


import com.exadel.easyabac.model.core.Action;

public enum ProjectAction implements Action {
    VIEW,
    UPDATE,
    CLOSE,
    DELETE
}

Здесь единственным условием является реализация интерфейса-маркера com.exadel.easyabac.model.core.Action. Все остальное в enum — на усмотрение разработчика.
Сразу отмечу, что именно через данный enum удобно делать привязку к роли пользователя и (или) пермишенам пользователя, любым способом — можно статически, можно динамически.


2. Создание аннотаций для управления доступом


Создадим аннотацию-идентификатор проекта:


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface ProjectId {
}

она понадобится для определения идентификатора проекта среди параметров метода.


Создадим аннотацию для управления доступа к проектам:


import com.exadel.easyabac.model.annotation.Access;
import com.exadel.easyabac.model.validation.EntityAccessValidator;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Access(identifier = ProjectId.class)
public @interface ProjectAccess {

    ProjectAction[] actions();

    Class<? extends EntityAccessValidator> validator();
}

Аннотация должна содержать actions и validator методы, в противном случае получим ошибки компиляции:


Error:(13, 9) java: value() method is missing for @com.example.abac.model.ProjectAccess
Error:(13, 9) java: validator() method is missing for @com.example.abac.model.ProjectAccess

Также тут стоить обратить внимание на такую вещь как Target:


@Target({ElementType.METHOD, ElementType.TYPE})

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


3. Создание валидатора для проверки прав


Остается добавить валидатор:


import com.exadel.easyabac.model.validation.EntityAccessValidator;
import com.exadel.easyabac.model.validation.ExecutionContext;
import com.example.abac.model.ProjectAction;
import org.springframework.stereotype.Component;

@Component
public class ProjectValidator implements EntityAccessValidator<ProjectAction> {

    @Override
    public void validate(ExecutionContext<ProjectAction> context) {
        // here get current user actions
        // and compare them with context.getRequiredActions()
    }
}

Валидатор можно сделать либо дефолтным (чтобы каждый раз явно не указывать в аннотации):


@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Access(identifier = ProjectId.class)
public @interface ProjectAccess {

    ProjectAction[] value();

    Class<? extends EntityAccessValidator> validator() default ProjectValidator.class;
}

либо же каждый раз указывать в аннотации явно:


@ProjectAccess(value = ProjectAction.VIEW, validator = ProjectValidator.class)

4. Ограничение доступа


Теперь, чтобы ограничить доступ к ресурсу остается расставить аннотации:


import com.exadel.easyabac.model.annotation.ProtectedResource;
import com.example.abac.Project;
import com.example.abac.model.ProjectAccess;
import com.example.abac.model.ProjectAction;
import com.example.abac.model.ProjectId;
import org.springframework.web.bind.annotation.*;

@RestController
@ProtectedResource
@RequestMapping("/project/{projectId}")
public class ProjectController {

    @GetMapping
    @ProjectAccess(ProjectAction.VIEW)
    public Project getProject(@ProjectId @PathVariable("projectId") Long projectId) {
        Project project = ...; // get project here
        return project;
    }

    @PostMapping
    @ProjectAccess({ProjectAction.VIEW, ProjectAction.UPDATE})
    public Project updateProject(@ProjectId @PathVariable("projectId") Long projectId) {
        Project project = ...; // update project here
        return project;
    }

    @PostMapping("/close")
    @ProjectAccess(ProjectAction.CLOSE)
    public Project updateProject(@ProjectId @PathVariable("projectId") Long projectId) {
        Project project = ...; // close project here
        return project;
    }

    @DeleteMapping
    @ProjectAccess(ProjectAction.DELETE)
    public Project updateProject(@ProjectId @PathVariable("projectId") Long projectId) {
        Project project = ...; // delete project here
        return project;
    }
}

@ProtectedResource аннотация используется для обозначения ресурсов, для которых нужна авторизации — в данном случае все instance-методы класса должны содержать как минимум одну @Access-based аннотацию, если это требования не выполняется — будет ошибки компиляции.


@PublicResource аннотация используется для обозначения метода который не требует авторизации, в том случае, если класс, содержащий метод помечен как @ProtectedResource


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


5. Реализация валидатора


Давайте рассмотрим более подробно как это работает. Фреймворк предоставляет каркас для построения архитектуры авторизации в приложении. Написание логики авторизации является задачей пользователя, это сделано для того, чтобы обеспечить достаточный уровень гибкости — обработка в приложениях может осуществляться по-разному.


Проверка прав выполняется в валидаторе, который должен реализовывать интерфейс EntityAccessValidator, а именно метод validate:


public void validate(ExecutionContext<Action> context);

ExecutionContext содержит необходимую информацию о требуемых правах доступа к ресурсу и мета-информацию о контексте вызова: context.getRequiredActions() вернет список Action, которые необходимо иметь пользователю.


Далее нужно получить список Action доступных текущему залогиненному пользователю — как это сделать — опять же задача разработчика приложения. Action(s) можно привязать к пользователю различными способами: статически привязав к роли пользователя, динамически через базу данных и т.д..


В итоге у нас есть 2 списка Actions — текущие и требуемые, остается сравнить их — если хотя бы одного Action не хватает — пользователь не может быть авторизован. Можно создать свой exception, например, AccessDeniedException и обработав его в ExceptionHandler вернуть HTTP status 403 — это уже на усмотрение разработчика приложения.


Пример реализации валидатора можно посмотреть здесь.



Диаграмма последовательности работы фреймворка


Сравнительный анализ


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


Рассмотрены следующие инструменты для авторизации: Apache Shiro, JAAS, Spring Security.
Apache Shiro и JAAS не предоставляют достаточной гибкости, имеют не очень удобный интерфейс конфигурации, JAAS вообще не использует декларативный стиль, Apache Shiro — только через файл конфигурации — несомненно, данные фреймворки удобны для решения некоторых задач, но
Что же касается Spring Security — это довольно мощный механизм и к тому же очень гибкий (как и положено быть фреймворку такого уровня), использует декларативный стиль для авторизации, однако не имеет встроенного механизма проверки конфигурации во время compile-time. Также конфигурация через аннотации в случае сложной авторизации получается довольно громоздкой. Та гибкость, которой обладает спринг требует дополнительных затрат на реализацию нужного механизма.


В заключение можно сказать что Easy-ABAC Framework с одной стороны решает проблемы которые есть у других фреймворков, дополняет их, с другой — ...


Дальнейшее развитие фреймворка


На данный момент фреймворк включает базовый механизм авторизации, причем довольно гибкий. Рассматривается возможность встроенной реализации валидаторов "из коробки" по умолчанию.
Также на данный момент фреймворк можеть быть использован только в spring-based приложениях. В будущем планируется избавиться от привязки к Spring.
В планах и разработка более удобной и гибкой конфигурации.


Cферы использования


  1. Java приложения с гранулярной авторизаций
  2. Мультитенантные приложения
  3. Приложения с динамическими правами доступа
  4. Spring-based приложения

Заключение


В статье рассмотрены архитектурные подходы к авторизации, предствлен Easy-ABAC Framework.
Среди преимуществ разработанного фреймворка можно отметить:


  1. Декларативный стиль авторизации
  2. Обработка ошибок конфигурации во время компиляции
  3. Простая и понятная конфигурация
  4. Гибкость