Случай, демонстрирующий ультратонкий контроль разрешений в Spring Security!

Spring Boot Spring
Случай, демонстрирующий ультратонкий контроль разрешений в Spring Security!

Существует множество способов повысить степень детализации управления разрешениями. Эта статья продолжает вышеизложенное (Как улучшить детализацию разрешений в Spring Security?), через конкретный случай, чтобы показать друзьям управление разрешениями на основе ACL. Другие модели управления разрешениями будут введены одна за другой позже.

1. Подготовка

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

Поскольку стартера, связанного с acl, нет, нам нужно вручную добавить зависимости acl.Кроме того, acl также зависит от кеша ehcache, поэтому нам также нужно добавить зависимости кеша.

Окончательный файл pom.xml выглядит следующим образом:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-acl</artifactId>
    <version>5.3.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>2.10.4</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.23</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>

После того, как проект успешно создан, мы можем найти файл скрипта базы данных в пакете acl jar:

Выберите соответствующий скрипт для выполнения в соответствии с вашей собственной базой данных.После выполнения будет создано четыре таблицы следующим образом:

Не буду сильно объяснять смысл таблицы, для тех, кто не понял, можно обратиться к предыдущей статье:Как улучшить детализацию разрешений в Spring Security?

Наконец, настройте информацию о базе данных в файле application.properties проекта следующим образом:

spring.datasource.url=jdbc:mysql:///acls?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

На этом подготовительная работа завершена. Далее смотрим на конфигурацию.

2. Конфигурация списка контроля доступа

Этот кусок конфигурационного кода относительно большой, я сначала приведу код, а потом мы будем его анализировать по порядку:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class AclConfig {

    @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 AclCache 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()
        );
    }

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

    @Bean
    PermissionEvaluator permissionEvaluator() {
        AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(aclService());
        return permissionEvaluator;
    }
}
  1. Конфигурация аннотации @EnableGlobalMethodSecurity означает включение использования аннотаций @PreAuthorize, @PostAuthorize и @Secured в проекте. Через некоторое время мы настроим разрешения через эти аннотации.
  2. Поскольку вводится целый набор вещей базы данных и настраивается информация о соединении с базой данных, экземпляры DataSource могут быть внедрены здесь для последующего использования.
  3. Экземпляр AclAuthorizationStrategy используется для определения того, имеет ли текущий субъект аутентификации право изменять Acl.Если быть точным, существует три вида разрешений: изменение владельца Acl, изменение информации аудита Acl и изменение самого ACE. . Существует только один класс реализации этого интерфейса, AclAuthorizationStrategyImpl.При создании экземпляра мы можем передать три параметра, которые соответствуют трем разрешениям, или передать один параметр, указывающий, что эта роль может делать три вещи.
  4. Интерфейс PermissionGrantingStrategy предоставляет метод isGranted, который является последним методом для фактического сравнения разрешений Интерфейс имеет только один класс реализации DefaultPermissionGrantingStrategy, только что новый.
  5. В системе ACL, поскольку при сравнении разрешений всегда необходимо запрашивать базу данных, что вызывает проблемы с производительностью, Ehcache представлен как кеш. AclCache имеет два класса реализации: SpringCacheBasedAclCache и EhCacheBasedAclCache. Мы представили экземпляр ehcache ранее, поэтому настройте экземпляр EhCacheBasedAclCache здесь.
  6. LookupStrategy может разрешать соответствующий Acl через ObjectIdentity. LookupStrategy имеет только один класс реализации, BasicLookupStrategy, который может быть непосредственно новым.
  7. AclService, который мы уже представили выше, здесь повторяться не будем.
  8. PermissionEvaluator обеспечивает поддержку выражения hasPermission. Поскольку в этом случае используется что-то вроде@PreAuthorize("hasPermission(#noticeMessage, 'WRITE')")Такие аннотации выполняют контроль разрешений, поэтому классу необходимо настроить экземпляр PermissionEvaluator.

До сих пор вам был представлен класс конфигурации.

3. Настройка сюжета

Предположим, теперь у меня есть класс сообщений-уведомлений NoticeMessage следующим образом:

public class NoticeMessage {
    private Integer id;
    private String content;

    @Override
    public String toString() {
        return "NoticeMessage{" +
                "id=" + id +
                ", content='" + content + '\'' +
                '}';
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

Затем создал таблицу данных на основе этого класса:

CREATE TABLE `system_message` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `content` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Затем следующий элемент управления разрешениями предназначен для этогоNoticeMessage.

Создайте NoticeMessageMapper и добавьте несколько тестовых методов:

@Mapper
public interface NoticeMessageMapper {
    List<NoticeMessage> findAll();

    NoticeMessage findById(Integer id);

    void save(NoticeMessage noticeMessage);

    void update(NoticeMessage noticeMessage);
}

Содержимое NoticeMessageMapper.xml выглядит следующим образом:

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.javaboy.acls.mapper.NoticeMessageMapper">


    <select id="findAll" resultType="org.javaboy.acls.model.NoticeMessage">
        select * from system_message;
    </select>

    <select id="findById" resultType="org.javaboy.acls.model.NoticeMessage">
        select * from system_message where id=#{id};
    </select>

    <insert id="save" parameterType="org.javaboy.acls.model.NoticeMessage">
        insert into system_message (id,content) values (#{id},#{content});
    </insert>

    <update id="update" parameterType="org.javaboy.acls.model.NoticeMessage">
        update system_message set content = #{content} where id=#{id};
    </update>
</mapper>

Это должно быть легко понять, нечего сказать.

Затем создайте службу NoticeMessageService следующим образом:

@Service
public class NoticeMessageService {
    @Autowired
    NoticeMessageMapper noticeMessageMapper;

    @PostFilter("hasPermission(filterObject, 'READ')")
    public List<NoticeMessage> findAll() {
        List<NoticeMessage> all = noticeMessageMapper.findAll();
        return all;
    }

    @PostAuthorize("hasPermission(returnObject, 'READ')")
    public NoticeMessage findById(Integer id) {
        return noticeMessageMapper.findById(id);
    }

    @PreAuthorize("hasPermission(#noticeMessage, 'CREATE')")
    public NoticeMessage save(NoticeMessage noticeMessage) {
        noticeMessageMapper.save(noticeMessage);
        return noticeMessage;
    }
    
    @PreAuthorize("hasPermission(#noticeMessage, 'WRITE')")
    public void update(NoticeMessage noticeMessage) {
        noticeMessageMapper.update(noticeMessage);
    }

}

Задействованы две новые аннотации, давайте немного поговорим:

  • @PostFilter: после выполнения метода отфильтруйте возвращенную коллекцию или массив (отфильтруйте данные, для которых у текущего пользователя есть разрешение READ), а returnObject представляет возвращаемое значение метода. Есть соответствующая аннотация @PreFilter, эта аннотация разрешает вызов метода, но перед входом в метод параметры должны быть отфильтрованы.
  • @PostAuthorize: разрешает вызов метода, но создает исключение безопасности, если выражение оценивается как ложное,#noticeMessageСоответствует параметрам метода.
  • @PreAuthorize: ограничить доступ к методу на основе оценки выражения перед вызовом метода.

Поймите смысл аннотации, тогда описанный выше метод не нуждается в объяснении.

Настройка завершена, а затем мы тестируем.

4. Тест

Чтобы облегчить тест, мы сначала подготовим несколько фрагментов тестовых данных следующим образом:

INSERT INTO `acl_class` (`id`, `class`)
VALUES
	(1,'org.javaboy.acls.model.NoticeMessage');
INSERT INTO `acl_sid` (`id`, `principal`, `sid`)
VALUES
	(2,1,'hr'),
	(1,1,'manager'),
	(3,0,'ROLE_EDITOR');
INSERT INTO `system_message` (`id`, `content`)
VALUES
	(1,'111'),
	(2,'222'),
	(3,'333');

Сначала добавляется acl_class, затем три Sid, два для пользователей, один для ролей и, наконец, три экземпляра NoticeMessage.

В настоящее время ни один пользователь/роль не может получить доступ к трем частям данных в system_message. Например, выполнение следующего кода не может получить никаких данных:

@Test
@WithMockUser(roles = "EDITOR")
public void test01() {
    List<NoticeMessage> all = noticeMessageService.findAll();
    System.out.println("all = " + all);
}

@WithMockUser(roles = "EDITOR") означает доступ с использованием роли EDITOR. Песня Ge здесь для удобства. Друзья также могут настроить пользователей для Spring Security, установить соответствующие интерфейсы, а затем добавить интерфейсы в Controller для тестирования.Я не буду здесь так хлопотно.

Теперь настраиваем его.

Во-первых, я хочу установить пользователь HR для чтения записи с ID 1 в таблице System_Message следующим образом:

@Autowired
NoticeMessageService noticeMessageService;
@Autowired
JdbcMutableAclService jdbcMutableAclService;
@Test
@WithMockUser(username = "javaboy")
@Transactional
@Rollback(value = false)
public void test02() {
    ObjectIdentity objectIdentity = new ObjectIdentityImpl(NoticeMessage.class, 1);
    Permission p = BasePermission.READ;
    MutableAcl acl = jdbcMutableAclService.createAcl(objectIdentity);
    acl.insertAce(acl.getEntries().size(), p, new PrincipalSid("hr"), true);
    jdbcMutableAclService.updateAcl(acl);
}

Задаем mock пользователя javaboy, то есть после создания acl его владельцем является javaboy, но в сиде в наших предустановленных данных нет javaboy, поэтому в таблицу acl_sid будет автоматически добавлена ​​запись со значением из javaboy.

В этом процессе записи будут добавлены в три таблицы acl_entry, acl_object_identity и acl_sid соответственно, поэтому необходимо добавить транзакции, и поскольку мы выполняем модульные тесты, чтобы убедиться, что мы можем видеть изменения в данных в базу данных, нам нужно добавить аннотацию @Rollback(value = false), чтобы предотвратить автоматический откат транзакции.

Внутри метода сначала создаются объекты ObjectIdentity и Permission, а затем создается объект acl, в этом процессе javaboy будет добавлен в таблицу acl_sid.

Затем вызовите метод acl_insertAce, сохраните туз в списке контроля доступа и, наконец, вызовите метод updateAcl для обновления объекта списка контроля доступа.

После завершения настройки выполните этот метод, и после завершения выполнения в базе данных появятся соответствующие записи.

Затем используйте пользователя hr для чтения записи с идентификатором 1. следующим образом:

@Test
@WithMockUser(username = "hr")
public void test03() {
    List<NoticeMessage> all = noticeMessageService.findAll();
    assertNotNull(all);
    assertEquals(1, all.size());
    assertEquals(1, all.get(0).getId());
    NoticeMessage byId = noticeMessageService.findById(1);
    assertNotNull(byId);
    assertEquals(1, byId.getId());
}

Песня GE использовала два метода здесь, чтобы продемонстрировать вам. Во-первых, мы называем Findall, этот метод будет запрашивать все данные, а затем возвращенные результаты будут автоматически отфильтрованы, оставляя только данные, которые пользователь HR получил разрешение на чтение, то есть данные, идентификаторы которых 1; другой звонок Метод FindbyID, входящий параметр 1, который легко понять.

Если вы хотите использовать пользователя hr для изменения объекта в это время, это невозможно. Мы можем продолжить использовать приведенный выше код, чтобы позволить пользователю hr изменять запись с идентификатором 1 следующим образом:

@Test
@WithMockUser(username = "javaboy")
@Transactional
@Rollback(value = false)
public void test02() {
    ObjectIdentity objectIdentity = new ObjectIdentityImpl(NoticeMessage.class, 1);
    Permission p = BasePermission.WRITE;
    MutableAcl acl = (MutableAcl) jdbcMutableAclService.readAclById(objectIdentity);
    acl.insertAce(acl.getEntries().size(), p, new PrincipalSid("hr"), true);
    jdbcMutableAclService.updateAcl(acl);
}

Обратите внимание, что это разрешение изменено на разрешение WRITE. Поскольку этот ObjectIdentity уже существует в списке контроля доступа, существующий список контроля доступа можно напрямую прочитать с помощью метода readAclById. После выполнения метода мы проверим право записи пользователя hr:

@Test
@WithMockUser(username = "hr")
public void test04() {
    NoticeMessage msg = noticeMessageService.findById(1);
    assertNotNull(msg);
    assertEquals(1, msg.getId());
    msg.setContent("javaboy-1111");
    noticeMessageService.update(msg);
    msg = noticeMessageService.findById(1);
    assertNotNull(msg);
    assertEquals("javaboy-1111", msg.getContent());
}

На этом этапе hr может использовать разрешение WRITE для изменения объекта.

Предположим, теперь я хочу, чтобы пользователь-менеджер создал сообщение NoticeMessage с идентификатором 99. По умолчанию у менеджера нет этого разрешения. Теперь мы можем уполномочить его:

@Test
@WithMockUser(username = "javaboy")
@Transactional
@Rollback(value = false)
public void test02() {
    ObjectIdentity objectIdentity = new ObjectIdentityImpl(NoticeMessage.class, 99);
    Permission p = BasePermission.CREATE;
    MutableAcl acl = jdbcMutableAclService.createAcl(objectIdentity);
    acl.insertAce(acl.getEntries().size(), p, new PrincipalSid("manager"), true);
    jdbcMutableAclService.updateAcl(acl);
}

Обратите внимание, что разрешение здесь CREATE.

Затем используйте пользователя-менеджера для добавления данных:

@Test
@WithMockUser(username = "manager")
public void test05() {
    NoticeMessage noticeMessage = new NoticeMessage();
    noticeMessage.setId(99);
    noticeMessage.setContent("999");
    noticeMessageService.save(noticeMessage);
}

На этом этапе его можно успешно добавить. После успешного добавления пользователь-менеджер не имеет разрешения на чтение данных с идентификатором 99. Вы можете добавить его самостоятельно, обратившись к предыдущему случаю.

5. Резюме

Как вы можете видеть из приведенного выше случая, контроль разрешений в модели разрешений ACL действительно очень хорош, вплоть до CURD каждого объекта.

Излишне говорить, что преимущества достаточно хороши! При этом бизнес и власть удачно разделены. Недостатки тоже очевидны, объем данных разрешений огромен, а масштабируемость слабая.

Наконец, фон официальной учетной записи отвечает на acl, чтобы получить ссылку для загрузки дела в этой статье.