Привет! Меня зовут Валерия, я java-разработчик компании SimbirSoft. В этой статье я хочу рассказать об одном из способов реализации ролевого контроля над действиями пользователей в системе. Механизм ролевого контроля позволяет сделать бизнес-процессы надежными с точки зрения информационной безопасности и привести их в соответствие с внутренними регламентами организации. Задачи подобного рода так или иначе возникают на любом проекте.
Есть несколько способов решения. Они зависят от проблематики, требований, доменной области, пожеланий заказчика и т.д. Возможные варианты реализации:
Контроль доступа к методам через ldap группы из AD, проверяемые в приложении: если пользователь входит в какую-то ldap-группу, то ему разрешены соответствующие действия.
Контроль доступа к методам через группы ldap, рассматриваемые как роли пользователя, которые помещаются в SecureContext и становятся доступны механизмам SpringSecurity.
Контроль доступа к эндпоинтам на основе внутренних ролей приложения (не обязательно завязанные на AD-группы).
Контроль доступа (с ролями и без) к объектам приложения на базе AOP.
Контроль доступа (с ролями и без) к объектам приложения на базе Spring ACLs.
Однако довольно часто встречается комбинация методов, когда какие-то действия ограничиваются на уровне контроллера, какие-то на сервисном слое, а какие-то — в зависимости от объекта, который необходимо обработать. Мы рассмотрим такой вариант: роль приложения + ldap группа с ограничениями на уровнях контроллера и сервиса.
Для начала опишем общую проблематику. Допустим, на предприятии есть система по управлению определенной документацией. В интересующем нас срезе доменной модели Документы, Заявки, Статьи. Просматривать их может любой человек в организации, но отредактировать документ, согласовать заявку или опубликовать статью могут только пользователи с правами администратора, причем тех подразделений, к которым эти заявки или документы или статьи относятся. При этом пользователь может работать в одном подразделении (например, бухгалтерия), но иметь доступ к объектам других подразделений (например, склад, охрана, кадры).
Авторизация в разрабатываемой системе идет через ldap, а права пользователя определяются через принадлежность к ldap-группе. То есть, сотрудник может работать в подразделении Управление, а иметь доступ к Складу, Охране и Бухгалтерии, если в ldap он состоит в соответствующих группах.
Архитектура разрабатываемой системы — MVC, то есть, созданы:
репозиторий-сервис-контроллер для управления документами;
репозиторий-сервис-контроллер для заявок;
репозиторий-сервис-контроллер для статей.
На уровне контроллеров разделения по подразделениям нет, однако есть разделение по наличию роли администратора (роль приложения). Ограничение на доступ к эндпоинтам редактирования по роли стоит на уровне фильтров Spring Security через AuthorizationManagerRequestMatcherRegistry:
requestMatchers(HttpMethod.PUT, "/papers/**").hasAnyAuthority(“ADMIN”)
То есть put-запрос, выполняющий редактирование статей, доступен только пользователям с ролью администратора. Обратите внимание, здесь (и далее) используются именно authorities, хотя называются ролью.
Используется стандартный rest-подход: для создания документа посылаем Post-запрос на конечную точку /docs, редактирование – put-запрос на /docs/{guid}, удаление – delete на /docs/{guid}. Со статьями и заявками то же самое. Со стороны интерфейса, естественно, стоит защита, что пользователь не может передать на обработку тот объект, к которому у него нет доступа. Однако по внутренним требованиям безопасности защита должна быть и от curl-запросов. То есть, ролевой контроль нужно сделать именно на сервисном слое.
Описанную проблематику можно нагляднее представить следующей схемой (на примере объекта Документ):


С точки зрения модели мы должны ввести некоторое поле, которое поможет сопоставить принадлежность документа и роли пользователя.
Назовем это поле ldap-name и рассмотрим на примере DTO Документа, как это можно сделать:
DocumentDTO { @NotNull DepartmentDTO department;}
DepartmentDTO { String ldap-name; }
Общая логика такова: метод вызывается авторизованным пользователем (то есть находящемся в SecureContext, а значит потенциально имеющим GrantedAuthority). В метод приходит DocumentDto с обязательно заполненным DepartmentDto. Нам необходимо, до попадания в метод, проверить, есть ли ldap-name из DepartmentDto Документа в списке ролей вызвавшего метод пользователя. Общая схема приведена на рисунке:

Мы видим классическую иллюстрацию для аспектного подхода: у нас есть валидационный метод, который мы привязываем к проверяемому перед его вызовом. В Spring Security для этой цели используется аннотация @PreAuthorize.
Для реализации описанной схемы нам необходимо сделать следующее:
Первое. Реализовать IAuthService с методом получения ролей текущего пользователя (организацию перехода от полного наименования ldap-групп к ldap-name оставим за скобками, так как это отдельная тема). Приведем класс-реализацию сервиса:
@Service
public class AuthUserServiceImpl implements IAuthService {
@Override
public Set<String> getMySecureRoles() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
AuthUserDetails authDitails = (AuthUserDetails) authentication.getPrincipal();
authDitails.getAuthorities();
return authDitails.getAuthorities().stream().map(e -> e.toString()).collect(Collectors.toSet());
}
}
Возможный ответ метода:
[
"MONEY",
"SKLAD",
"CONTROL"
]
Второе. Сделать интерфейс и его реализацию для проверки возможности действия (приведем сразу второе).
@Component
@RequiredArgsConstructor
@Component("checkerBean")
public class ActionCheckPermissionImpl implements IActionCheckPermission {
private final IAuthService authService;
private final DocRepo docRepo;
private final DoMapper docMapper;
@Override
public void checkPermission(Object object) {
String ldapName = "";
if (object instanceof DocumentDto doc) {
ldapName = doc.getDepartment).getLdapName0;
}
if (object instanceof Document doc) {
ldapName = doc.getDepartment.getLdapName;
}
if (lauthService.getMySecureRoles().contains(ldapName)) {
throw new AccessDeniedException("Access denied: you have not necessary role");
}
// do something ...
}
}
Третье. В сервисном слое пометить аннотацией необходимые методы
public interface DocumentService {
@PreAuthorize("isAuthenticated()")
List<DocumentDto> getDocuments();
@PreAuthorize("hasAuthority('ADMIN')")
List<DocumentDto> getDocumentsForReview();
@PreAuthorize(“@checkerBean.checkPermission(#dto)”)
DocumentDto addDocument(@P(“dto”) DocumentDto dto);
@PreAuthorize(“@checkerBean.checkPermission(#dto)”)
List<DocumentDto> updateDocument(@P(“dto”) DocumentDto dto);
@PreAuthorize(“@checkerBean.checkPermission(@documentRepo.findById(id).get())”)
void deleteDocument(@P(“id”)UUID id);
}
То есть, dto, пришедшее в метод может быть таким:
{ department : { ldap-name: “SKLAD”}}
Если getMySecureRoles() вернет пользователю вот такой список: ["MONEY", "SKLAD", "CONTROL"], проверка будет успешно пройдена. Если же вот такой - ["MONEY", "CONTROL"] возникнет AccessDeniedException.
За скобками осталось несколько моментов использования @PreAuthorize, которые необходимо подсветить.
Во-первых, не забыть поставить аннотацию @EnbleWebSecurity над основным приложением.
Во-вторых, явно указать имя бина, который осуществляет проверку. В нашем случае это выполнялось так:
@Component(“checkerBean”)
public class ActionCheckPermissionImpl implements IActionCheckPermission {
…
}
В-третьих, через аннотацию @P явно указать имена используемых параметров.
В этой статье мы рассмотрели реализацию ролевого контроля действий над объектами (через @PreAuthorize). Из приведенного примера видно, что @PreAuthorize предназначен для простой и эффективной проверки доступа на основе аспектного подхода. Если доступ отклоняется, он просто предотвращает выполнение метода, передавая управление обработчику ошибок. Если же вам нужно более детальное управление валидацией с возможностью выбрасывания специфических исключений, можно использовать самостоятельную привязку валидационных методов через чистые аспекты.
Спасибо за внимание!
Больше авторских материалов для backend-разработчиков от моих коллег читайте в соцсетях SimbirSoft – ВКонтакте и Telegram.