Перейти к основному содержимому

Введение в Spring Security ACL

· 9 мин. чтения

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 .