1. Введение
Список контроля доступа
( ACL)
— это список разрешений, прикрепленных к объекту. ACL указывает, какие
удостоверения предоставляются, какие операции над данным объектом.
Spring Security Access Control List
— это компонент Spring
, поддерживающий безопасность объектов домена.
Проще говоря, Spring ACL помогает определить разрешения для конкретного пользователя/роли в одном объекте домена, а не повсеместно, на типичном уровне для каждой операции.
Например, пользователь с ролью администратора
может видеть ( ЧИТАТЬ)
и редактировать ( ЗАПИСАТЬ)
все сообщения в центральном ящике уведомлений
, но обычный пользователь может только видеть сообщения, относиться к ним и не может редактировать. Между тем, другие пользователи с ролью Редактор
могут видеть и редактировать определенные сообщения.
Следовательно, разные пользователи/роли имеют разные разрешения для каждого конкретного объекта. В этом случае Spring ACL
способен выполнить эту задачу. В этой статье мы рассмотрим, как настроить базовую проверку разрешений с помощью Spring ACL .
2. Конфигурация
2.1. База данных ACL
Чтобы использовать Spring Security ACL
, нам нужно создать четыре обязательные таблицы в нашей базе данных.
Первая таблица — это ACL_CLASS
, в которой хранится имя класса объекта домена, столбцы включают:
Я БЫ
CLASS:
имя класса защищенных объектов домена, например:com.foreach.acl.persistence.entity.NoticeMessage
``
Во-вторых, нам нужна таблица ACL_SID
, которая позволяет нам универсально идентифицировать любой принцип или орган власти в системе. В стол нужно:
Я БЫ
SID:
имя пользователя или имя роли.SID
расшифровывается какSecurity Identity
PRINCIPAL:
0
или1
, чтобы указать, что соответствующийSID
является принципалом (пользователь, например,mary, mike, jack…
) или органом власти (роль, напримерROLE_ADMIN, ROLE_USER, ROLE_EDITOR…
)
Следующая таблица — ACL_OBJECT_IDENTITY, в
которой хранится информация для каждого уникального объекта домена:
Я БЫ
OBJECT_ID_CLASS:
определить класс объекта домена, `ссылки на таблицу
ACL_CLASS`OBJECT_ID_IDENTITY:
объекты домена могут храниться во многих таблицах в зависимости от класса. Следовательно, в этом поле хранится первичный ключ целевого объекта.PARENT_OBJECT:
укажите родителя этогоидентификатора объекта
в этой таблице .OWNER_SID:
ID
владельца объекта, ссылки на таблицуACL_SID
ENTRIES_INHERITING: наследуются
лизаписи ACL
этого объекта от родительского объекта (записи ACL
определены в таблицеACL_ENTRY
)
Наконец, ACL_ENTRY
хранит отдельные разрешения, назначенные каждому SID
в идентификаторе объекта
:
Я БЫ
ACL_OBJECT_IDENTITY:
указать идентификатор объекта, ссылки на таблицуACL_OBJECT_IDENTITY
ACE_ORDER:
порядок текущей записи в спискезаписей ACL соответствующего
идентификатора объекта .
SID:
целевойSID
, которому предоставляется или запрещается разрешение, ссылается на таблицуACL_SID.
MASK:
целочисленная битовая маска, которая представляет фактическое предоставление или отказ в разрешении.ПРЕДОСТАВЛЕНИЕ:
значение 1 означает предоставление, значение0
означает отказAUDIT_SUCCESS
иAUDIT_FAILURE
: для целей аудита
2.2. Зависимость
Чтобы иметь возможность использовать Spring ACL
в нашем проекте, давайте сначала определим наши зависимости:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-acl</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.11</version>
</dependency>
Spring ACL
требует кэша для хранения Object Identity
и записей ACL
, поэтому здесь мы будем использовать Ehcache
. И для поддержки Ehcache
в Spring
нам также нужна поддержка spring-context.
Когда мы не работаем с Spring Boot, нам нужно явно добавлять версии. Их можно проверить на Maven Central: spring-security-acl , spring-security-config , spring-context-support , ehcache-core .
2.3. Конфигурация, связанная с ACL
Нам нужно защитить все методы, которые возвращают защищенные объекты домена или вносят изменения в объект, включив глобальную безопасность методов:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class AclMethodSecurityConfiguration
extends GlobalMethodSecurityConfiguration {
@Autowired
MethodSecurityExpressionHandler
defaultMethodSecurityExpressionHandler;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return defaultMethodSecurityExpressionHandler;
}
}
Давайте также включим управление доступом
на основе выражений, установив для параметра prePostEnabled
значение true
, чтобы использовать Spring Expression Language (SpEL)
.
Более того ,
нам нужен обработчик выражений с поддержкой ACL
:
@Bean
public MethodSecurityExpressionHandler
defaultMethodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler
= new DefaultMethodSecurityExpressionHandler();
AclPermissionEvaluator permissionEvaluator
= new AclPermissionEvaluator(aclService());
expressionHandler.setPermissionEvaluator(permissionEvaluator);
return expressionHandler;
}
Следовательно ,
мы назначаем AclPermissionEvaluator
DefaultMethodSecurityExpressionHandler
. Оценщику требуется MutableAclService
для загрузки настроек разрешений и определений объектов домена из базы данных.
Для простоты мы используем предоставленный JdbcMutableAclService
:
@Bean
public JdbcMutableAclService aclService() {
return new JdbcMutableAclService(
dataSource, lookupStrategy(), aclCache());
}
В своем названии JdbcMutableAclService
использует JDBCTemplate
для упрощения доступа к базе данных. Ему нужны DataSource (
для JDBCTemplate)
, LookupStrategy
(обеспечивает оптимизированный поиск при запросе к базе данных) и AclCache (
кэширование записей
ACL
и идентификатора объекта)
.
Опять же, для простоты мы используем предоставленные BasicLookupStrategy
и EhCacheBasedAclCache
.
@Autowired
DataSource dataSource;
@Bean
public AclAuthorizationStrategy aclAuthorizationStrategy() {
return new AclAuthorizationStrategyImpl(
new SimpleGrantedAuthority("ROLE_ADMIN"));
}
@Bean
public PermissionGrantingStrategy permissionGrantingStrategy() {
return new DefaultPermissionGrantingStrategy(
new ConsoleAuditLogger());
}
@Bean
public EhCacheBasedAclCache aclCache() {
return new EhCacheBasedAclCache(
aclEhCacheFactoryBean().getObject(),
permissionGrantingStrategy(),
aclAuthorizationStrategy()
);
}
@Bean
public EhCacheFactoryBean aclEhCacheFactoryBean() {
EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean();
ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject());
ehCacheFactoryBean.setCacheName("aclCache");
return ehCacheFactoryBean;
}
@Bean
public EhCacheManagerFactoryBean aclCacheManager() {
return new EhCacheManagerFactoryBean();
}
@Bean
public LookupStrategy lookupStrategy() {
return new BasicLookupStrategy(
dataSource,
aclCache(),
aclAuthorizationStrategy(),
new ConsoleAuditLogger()
);
}
Здесь AclAuthorizationStrategy
отвечает за вывод о том, обладает ли текущий пользователь всеми необходимыми разрешениями на определенные объекты или нет.
Ему нужна поддержка PermissionGrantingStrategy,
которая определяет логику определения того, предоставлено ли разрешение конкретному SID
.
3. Безопасность метода с помощью Spring ACL
На данный момент мы сделали все необходимые настройки .
Теперь мы можем установить требуемое правило проверки на наши защищенные методы.
По умолчанию Spring ACL
обращается к классу BasePermission
для всех доступных разрешений. По сути, у нас есть разрешения на ЧТЕНИЕ, ЗАПИСЬ, СОЗДАНИЕ, УДАЛЕНИЕ
и АДМИНИСТРИРОВАНИЕ .
Давайте попробуем определить некоторые правила безопасности:
@PostFilter("hasPermission(filterObject, 'READ')")
List<NoticeMessage> findAll();
@PostAuthorize("hasPermission(returnObject, 'READ')")
NoticeMessage findById(Integer id);
@PreAuthorize("hasPermission(#noticeMessage, 'WRITE')")
NoticeMessage save(@Param("noticeMessage")NoticeMessage noticeMessage);
После выполнения метода findAll
()
сработает @PostFilter . Требуемое правило hasPermission(filterObject, 'READ')
означает возврат только тех уведомлений
, на которые у текущего пользователя есть разрешение READ .
Точно так же @PostAuthorize
срабатывает после выполнения метода findById()
, убедитесь, что объект NoticeMessage
возвращается только в том случае, если у текущего пользователя есть разрешение READ на него.
В противном случае система выдаст исключение AccessDeniedException
.
С другой стороны, система запускает аннотацию @PreAuthorize перед вызовом метода
save()
. Он решит, разрешено ли выполнение соответствующего метода или нет. В противном случае будет выброшено исключение AccessDeniedException .
4. В действии
Теперь мы собираемся протестировать все эти конфигурации с помощью JUnit
. Мы будем использовать базу данных H2
, чтобы максимально упростить настройку.
Нам нужно будет добавить:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
4.1. Сценарий
В этом сценарии у нас будет два пользователя ( manager, hr)
и одна роль пользователя ( ROLE_EDITOR),
поэтому наш acl_sid
будет:
INSERT INTO acl_sid (id, principal, sid) VALUES
(1, 1, 'manager'),
(2, 1, 'hr'),
(3, 0, 'ROLE_EDITOR');
Затем нам нужно объявить классNoticeMessage
в acl_class
. И три экземпляра класса NoticeMessage
будут вставлены в system_message.
Кроме того, соответствующие записи для этих трех экземпляров должны быть объявлены в acl_object_identity
:
INSERT INTO acl_class (id, class) VALUES
(1, 'com.foreach.acl.persistence.entity.NoticeMessage');
INSERT INTO system_message(id,content) VALUES
(1,'First Level Message'),
(2,'Second Level Message'),
(3,'Third Level Message');
INSERT INTO acl_object_identity
(id, object_id_class, object_id_identity,
parent_object, owner_sid, entries_inheriting)
VALUES
(1, 1, 1, NULL, 3, 0),
(2, 1, 2, NULL, 3, 0),
(3, 1, 3, NULL, 3, 0);
Первоначально мы предоставляем разрешения READ
и WRITE
для первого объекта ( id=1
) менеджеру
пользователей . Между тем, любой пользователь с ROLE_EDITOR
будет иметь разрешение READ
для всех трех объектов, но только разрешение WRITE
для третьего объекта ( id=3
). Кроме того, пользователь hr
будет иметь только право READ
для второго объекта.
Здесь, поскольку мы используем класс Spring ACL
BasePermission
по умолчанию для проверки разрешений, значение маски разрешения READ
будет равно 1, а значение маски разрешения WRITE
будет равно 2. Наши данные в acl_entry
будут:
INSERT INTO acl_entry
(id, acl_object_identity, ace_order,
sid, mask, granting, audit_success, audit_failure)
VALUES
(1, 1, 1, 1, 1, 1, 1, 1),
(2, 1, 2, 1, 2, 1, 1, 1),
(3, 1, 3, 3, 1, 1, 1, 1),
(4, 2, 1, 2, 1, 1, 1, 1),
(5, 2, 2, 3, 1, 1, 1, 1),
(6, 3, 1, 3, 1, 1, 1, 1),
(7, 3, 2, 3, 2, 1, 1, 1);
4.2. Прецедент
В первую очередь пробуем вызвать метод findAll
.
В соответствии с нашей конфигурацией метод возвращает только те уведомления
, на которые у пользователя есть разрешение READ .
Следовательно, мы ожидаем, что список результатов будет содержать только первое сообщение:
@Test
@WithMockUser(username = "manager")
public void
givenUserManager_whenFindAllMessage_thenReturnFirstMessage(){
List<NoticeMessage> details = repo.findAll();
assertNotNull(details);
assertEquals(1,details.size());
assertEquals(FIRST_MESSAGE_ID,details.get(0).getId());
}
Затем мы пытаемся вызвать этот же метод с любым пользователем, у которого есть роль — ROLE_EDITOR
. Обратите внимание, что в этом случае у этих пользователей есть разрешение READ
на все три объекта.
Следовательно, мы ожидаем, что список результатов будет содержать все три сообщения:
@Test
@WithMockUser(roles = {"EDITOR"})
public void
givenRoleEditor_whenFindAllMessage_thenReturn3Message(){
List<NoticeMessage> details = repo.findAll();
assertNotNull(details);
assertEquals(3,details.size());
}
Далее, используя пользователя manager
, мы попробуем получить первое сообщение по id и обновить его содержимое — все должно работать нормально:
@Test
@WithMockUser(username = "manager")
public void
givenUserManager_whenFind1stMessageByIdAndUpdateItsContent_thenOK(){
NoticeMessage firstMessage = repo.findById(FIRST_MESSAGE_ID);
assertNotNull(firstMessage);
assertEquals(FIRST_MESSAGE_ID,firstMessage.getId());
firstMessage.setContent(EDITTED_CONTENT);
repo.save(firstMessage);
NoticeMessage editedFirstMessage = repo.findById(FIRST_MESSAGE_ID);
assertNotNull(editedFirstMessage);
assertEquals(FIRST_MESSAGE_ID,editedFirstMessage.getId());
assertEquals(EDITTED_CONTENT,editedFirstMessage.getContent());
}
Но если какой-либо пользователь с ролью ROLE_EDITOR
обновит содержимое первого сообщения — наша система выдаст AccessDeniedException
:
@Test(expected = AccessDeniedException.class)
@WithMockUser(roles = {"EDITOR"})
public void
givenRoleEditor_whenFind1stMessageByIdAndUpdateContent_thenFail(){
NoticeMessage firstMessage = repo.findById(FIRST_MESSAGE_ID);
assertNotNull(firstMessage);
assertEquals(FIRST_MESSAGE_ID,firstMessage.getId());
firstMessage.setContent(EDITTED_CONTENT);
repo.save(firstMessage);
}
Точно так же пользователь hr
может найти второе сообщение по id, но не сможет его обновить:
@Test
@WithMockUser(username = "hr")
public void givenUsernameHr_whenFindMessageById2_thenOK(){
NoticeMessage secondMessage = repo.findById(SECOND_MESSAGE_ID);
assertNotNull(secondMessage);
assertEquals(SECOND_MESSAGE_ID,secondMessage.getId());
}
@Test(expected = AccessDeniedException.class)
@WithMockUser(username = "hr")
public void givenUsernameHr_whenUpdateMessageWithId2_thenFail(){
NoticeMessage secondMessage = new NoticeMessage();
secondMessage.setId(SECOND_MESSAGE_ID);
secondMessage.setContent(EDITTED_CONTENT);
repo.save(secondMessage);
}
5. Вывод
В этой статье мы рассмотрели базовую настройку и использование Spring ACL .
Как мы знаем, Spring ACL
требует определенных таблиц для управления объектами, принципами/полномочиями и настройками разрешений. Все взаимодействия с этими таблицами, особенно действия по обновлению, должны проходить через AclService.
Мы рассмотрим этот сервис для основных действий CRUD
в следующей статье.
По умолчанию мы ограничены предопределенным разрешением в классе BasePermissio
.
Наконец, реализацию этого туториала можно найти на Github .