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

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

1. Вступление

Access Control List (ACL) - это список разрешений, прикрепленных к объекту. ACL указывает, какие идентификаторы предоставляются каким операциям над данным объектом.

Spring Security _Access Control List_ - этоa Spring component which supports Domain Object Security.. Проще говоря, Spring ACL помогает в определении разрешений для конкретного пользователя / роли в одном доменном объекте, а не для всех, на типичном уровне для каждой операции.

Например, пользователь с рольюAdmin может видеть (READ) и редактировать (WRITE) все сообщения наCentral Notice Box), но обычный пользователь может видеть только сообщения, относящиеся к их и редактировать нельзя. Между тем, другие пользователи с рольюEditor могут просматривать и редактировать некоторые конкретные сообщения.

Следовательно, разные пользователи / роли имеют разные разрешения для каждого конкретного объекта. В этом случаеSpring ACL способен выполнить задачу. В этой статье мы узнаем, как настроить базовую проверку разрешений с помощьюSpring ACL.

2. конфигурация

2.1. База данных ACL

Чтобы использоватьSpring Security ACL, нам нужно создать четыре обязательных таблицы в нашей базе данных.

Первая таблица -ACL_CLASS, в которой хранится имя класса объекта домена, столбцы включают:

  • ID

  • CLASS: имя класса защищенных объектов домена, например:org.example.acl.persistence.entity.NoticeMessage

Во-вторых, нам нужна таблицаACL_SID, которая позволяет нам универсально идентифицировать любой принцип или авторитет в системе. Стол нуждается в:

  • ID

  • SID: - имя пользователя или имя роли. SID означаетSecurity Identity

  • PRINCIPAL:0 или1, чтобы указать, что соответствующийSID является принципалом (пользователем, напримерmary, mike, jack…) или органом (ролью, напримерROLE_ADMIN, ROLE_USER, ROLE_EDITOR…с)

Следующая таблица -ACL_OBJECT_IDENTITY,, в которой хранится информация для каждого уникального объекта домена:

  • ID

  • OBJECT_ID_CLASS: определяют класс объекта предметной области, таблицу_ links to _ACL_CLASS

  • Объекты доменаOBJECT_ID_IDENTITY: могут храниться во многих таблицах в зависимости от класса. Следовательно, в этом поле хранится первичный ключ целевого объекта.

  • PARENT_OBJECT: указывает родительский элемент этогоObject Identity в этой таблице

  • OWNER_SID:ID владельца объекта, ссылки на таблицуACL_SID

  • ENTRIES_INHERITTING:, наследуется лиACL Entries этого объекта от родительского объекта (ACL Entries определены в таблицеACL_ENTRY)

Наконец, индивидуальное разрешение хранилищаACL_ENTRY назначается каждомуSID наObject Identity:

  • ID

  • ACL_OBJECT_IDENTITY: указывает идентификатор объекта, ссылается на таблицуACL_OBJECT_IDENTITY

  • ACE_ORDER: порядок текущей записи в спискеACL entries соответствующегоObject Identity

  • SID: целевойSID, которому предоставлено или отказано в разрешении, ссылается на таблицуACL_SID

  • MASK: целочисленная битовая маска, которая представляет фактическое разрешение, предоставленное или отклоненное

  • GRANTING: значение 1 означает предоставление, значение0 означает отказ

  • AUDIT_SUCCESS иAUDIT_FAILURE: для целей аудита

2.2. зависимость

Чтобы иметь возможность использоватьSpring ACL в нашем проекте, давайте сначала определим наши зависимости:


    org.springframework.security
    spring-security-acl


    org.springframework.security
    spring-security-config


    org.springframework
    spring-context-support


    net.sf.ehcache
    ehcache-core
    2.6.11

Spring ACL требует кеширования для храненияObject Identity иACL entries, поэтому мы воспользуемся здесьEhcache. И для поддержкиEhcache вSpring, нам также понадобитсяspring-context-support.

Когда Spring Boot не работает, нам нужно явно добавлять версии. Их можно проверить в Maven Central:spring-security-acl,spring-security-config,spring-context-support,ehcache-core.

Нам необходимо защитить все методы, которые возвращают защищенные объекты домена или вносят изменения в объект, включивGlobal Method Security:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class AclMethodSecurityConfiguration
  extends GlobalMethodSecurityConfiguration {

    @Autowired
    MethodSecurityExpressionHandler
      defaultMethodSecurityExpressionHandler;

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return defaultMethodSecurityExpressionHandler;
    }
}

Давайте также включимExpression-Based Access Control, установив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;
}

Следовательно,, мы присваиваемAclPermissionEvaluatorDefaultMethodSecurityExpressionHandler. Оценщику нуженMutableAclService для загрузки настроек разрешений и определений объекта домена из базы данных.

Для простоты мы используем предоставленныйJdbcMutableAclService:

@Bean
public JdbcMutableAclService aclService() {
    return new JdbcMutableAclService(
      dataSource, lookupStrategy(), aclCache());
}

В названииJdbcMutableAclService используетсяJDBCTemplate для упрощения доступа к базе данных. Ему нужныDataSource ( дляJDBCTemplate),LookupStrategy (обеспечивает оптимизированный поиск при запросе базы данных) иAclCache (cachingACLEntries иObject Identity)с.

Опять же, для простоты мы используем предоставленные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 для всех доступных разрешений. По сути, у нас есть разрешенияREAD, WRITE, CREATE, DELETE иADMINISTRATION.

Попробуем определить некоторые правила безопасности:

@PostFilter("hasPermission(filterObject, 'READ')")
List findAll();

@PostAuthorize("hasPermission(returnObject, 'READ')")
NoticeMessage findById(Integer id);

@PreAuthorize("hasPermission(#noticeMessage, 'WRITE')")
NoticeMessage save(@Param("noticeMessage")NoticeMessage noticeMessage);

После выполнения методаfindAll() будет запущен@PostFilter. Требуемое правилоhasPermission(filterObject, ‘READ'), означает возврат только техNoticeMessage, на которые текущий пользователь имеет разрешениеREAD.

Аналогично,@PostAuthorize запускается после выполнения методаfindById(), убедитесь, что объектNoticeMessage возвращается только в том случае, если текущий пользователь имеет на него разрешениеREAD. Если нет, система выдастAccessDeniedException.

С другой стороны, система запускает аннотацию@PreAuthorize перед вызовом методаsave(). Это решит, где соответствующий метод разрешено выполнять или нет. Если нет, будет брошенAccessDeniedException.

4. В бою

Теперь мы протестируем все эти конфигурации, используяJUnit. Мы будем использовать базу данныхH2, чтобы упростить настройку.

Нам нужно будет добавить:


  com.h2database
  h2



  org.springframework
  spring-test
  test



  org.springframework.security
  spring-security-test
  test

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, 'org.example.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) пользователюmanager. Между тем, любой пользователь сROLE_EDITOR будет иметь разрешениеREAD на все три объекта, но будет иметь разрешениеWRITE только на третий объект (id=3). Кроме того, у пользователяhr будет толькоREAD разрешение на второй объект.

Здесь, поскольку мы используем классSpring ACLBasePermission по умолчанию для проверки разрешений, значение маски разрешения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.

В нашей конфигурации метод возвращает только теNoticeMessage, на которые у пользователя есть разрешениеREAD.

Следовательно, мы ожидаем, что список результатов содержит только первое сообщение:

@Test
@WithMockUser(username = "manager")
public void
  givenUserManager_whenFindAllMessage_thenReturnFirstMessage(){
    List 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 details = repo.findAll();

    assertNotNull(details);
    assertEquals(3,details.size());
}

Затем, используя пользователяmanager, мы попытаемся получить первое сообщение по идентификатору и обновить его содержимое - все должно работать нормально:

@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 может найти второе сообщение по идентификатору, но не сможет его обновить:

@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 в следующей статье.

По умолчанию мы ограничены предопределенным разрешением в классеBasePermission.

Наконец, реализацию этого руководства можно найтиover on Github.