Аутентификация

Аутентификация – это процесс проверки личности пользователя или процесса, который взаимодействует с системой. Например, система может аутентифицировать пользователей по их имени и паролю. Для аутентифицированных пользователей система может выполнить авторизацию, которая представляет собой проверку разрешений для определенного ресурса.

Jmix напрямую использует Servlet аутентификацию Spring Security, поэтому если вы знакомы с этой платформой, то вы можете легко расширить или переопределить стандартный механизм аутентификации, предоставляемый Jmix "из коробки".

Текущий пользователь

Чтобы определить, кто в данный момент аутентифицирован, используйте бин CurrentAuthentication. Он имеет следующие методы:

  • getUser() возвращает текущего аутентифицированного пользователя как UserDetails. Его можно привести к классу пользователя, определенному в проекте.

  • getAuthentication() возвращает объект Authentication, связанный с текущим потоком выполнения. Объект Authentication хранит имена ролей пользователя.

    Jmix использует класс Spring Security SimpleGrantedAuthority для представления ролей пользователей. Этот класс фактически хранит одну строку, представляющую роль. Формат этой строки:

    • Для ресурсных ролей: ROLE_<role-code>, например, ROLE_system-full-access.

    • Для ролей уровня строк: ROW_LEVEL_ROLE_<role-code>, например, ROW_LEVEL_ROLE_my-role.

    Granted authorities нужного Java-класса и содержания могут быть созданы из кодов ролей с использованием класса RoleGrantedAuthorityUtils.

    Вы можете настроить префикс для полномочий ресурсных ролей, используя стандартный механизм Spring, путем конфигурирования бина org.springframework.security.config.core.GrantedAuthorityDefaults.

    Аналогично, вы можете скорректировать префикс для полномочий ролей уровня строк, используя свойство приложения jmix.security.default-row-level-role-prefix.

  • getLocale() и getTimeZone() возвращают локаль и часовой пояс текущего пользователя.

  • isSet() возвращает значение true, если текущий поток выполнения аутентифицирован, то есть содержит информацию о пользователе. Если это не так, методы getUser(), getLocale() и getTimeZone(), описанные выше, выбросят исключение IllegalStateException.

Ниже приведен пример получения информации о текущем пользователе:

@Autowired
private CurrentAuthentication currentAuthentication;

private void printAuthenticationInfo() {
    UserDetails user = currentAuthentication.getUser();
    Authentication authentication = currentAuthentication.getAuthentication();
    Locale locale = currentAuthentication.getLocale();
    TimeZone timeZone = currentAuthentication.getTimeZone();

    System.out.println(
            "User: " + user.getUsername() + "\n" +
                    "Authentication: " + authentication + "\n" +
                    "Roles: " + getRoleNames(authentication) + "\n" +
                    "Locale: " + locale.getDisplayName() + "\n" +
                    "TimeZone: " + timeZone.getDisplayName()
    );
}

private String getRoleNames(Authentication authentication) {
    return authentication.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.joining(","));
}

CurrentAuthentication – это просто обёртка вокруг SecurityContextHolder, поэтому он полностью совместим со всеми механизмами Spring Security.

Например, вы можете использовать DelegatingSecurityContextRunnable для передачи контекста аутентификации в новые потоки как описано в документации Spring Security.

Аутентификация клиента

У бэкенда приложения Jmix могут быть разные клиенты, например, Jmix UI или REST API. Каждый клиент имеет свой собственный стандартный механизм аутентификации, такой как окно логина UI или токен доступа REST.

Собственная валидация паролей

Чтобы реализовать собственную валидацию паролей в приложении, достаточно создать бин (или несколько бинов), реализующий интерфейс PasswordValidator. Например:

package com.company.demo.security;

import com.company.demo.entity.User;
import io.jmix.securityflowui.password.PasswordValidationContext;
import io.jmix.securityflowui.password.PasswordValidationException;
import io.jmix.securityflowui.password.PasswordValidator;
import org.springframework.stereotype.Component;

@Component
public class MyPasswordValidator implements PasswordValidator<User> {

    @Override
    public void validate(PasswordValidationContext<User> context) throws PasswordValidationException {
        if (context.getPassword().length() < 3)
            throw new PasswordValidationException("Password is too short, must be >= 3 characters");
    }
}

Все валидаторы будут автоматически использованы в диалоге действия ChangePassword.

Для добавления кастомной валидации в экран деталей сущности User, используйте бин-помощник PasswordValidation:

@Autowired
private PasswordValidation passwordValidation;

@Subscribe
public void onValidation(final ValidationEvent event) {
    // ...
    if (entityStates.isNew(getEditedEntity())) {
        List<String> validationErrors = passwordValidation.validate(getEditedEntity(), passwordField.getValue());
        if (!validationErrors.isEmpty()) {
            event.getErrors().add(String.join("\n", validationErrors));
        }
    }

Защита от взлома методом перебора

В фреймворке есть механизм защиты от взлома паролей методом перебора, которая обеспечивается свойством приложения jmix.security.bruteforceprotection.enabled. Если она включена, комбинация логина пользователя и IP-адреса блокируется на определенный промежуток времени в случае нескольких неудачных попыток входа в систему. Максимальное количество попыток определяется свойством приложения jmix.security.bruteforceprotection.max-login-attempts-number. Интервал блокировки в секундах определяется свойством приложения jmix.security.bruteforceprotection.block-interval.

  • jmix.security.bruteforceprotection.enabled

    Включает механизм защиты от взлома пароля методом перебора. Значение по умолчанию: false.

  • jmix.security.bruteforceprotection.block-interval

    Определяет интервал блокировки в секундах после превышения максимального количества неудачных попыток входа в систему, если включено свойство jmix.security.bruteforceprotection.enabled. Значение по умолчанию: 60 seconds.

  • jmix.security.bruteforceprotection.max-login-attempts-number

    Определяет максимальное количество неудачных попыток входа в систему для комбинации логина пользователя и IP-адреса, если включено свойство jmix.security.bruteforceprotection.enabled. Значение по умолчанию: 5.

Системная аутентификация

Поток выполнения может быть не аутентифицирован, если был запущен внутренним планировщиком или обрабатывает запрос из JMX-интерфейса. Однако обычно бизнес-логике или коду доступа к данным для журналирования или авторизации требуется информация о том, кто в данный момент работает с системой.

Чтобы временно связать текущий поток выполнения с пользователем, используйте бин SystemAuthenticator. Он имеет следующие методы:

  • withSystem() - принимает lambda-выражение и выполняет его от имени системного пользователя.

  • withUser() - принимает имя обычного пользователя приложения и lambda-выражение и выполняет его от имени данного пользователя с соответствующими разрешениями.

Ниже приведен пример аутентификации операции MBean:

@Autowired
private SystemAuthenticator systemAuthenticator;
@Autowired
private CurrentAuthentication currentAuthentication;

@ManagedOperation
public String doSomething() {
    return systemAuthenticator.withSystem(() -> {
        UserDetails user = currentAuthentication.getUser();
        System.out.println("User: " + user.getUsername()); // system
        // ...
        return "Done";
    });
}

@ManagedOperation
public String doSomething2() {
    return systemAuthenticator.withUser("admin", () -> {
        UserDetails user = currentAuthentication.getUser();
        System.out.println("User: " + user.getUsername()); // admin
        // ...
        return "Done";
    });
}

Также можно использовать аннотацию @Authenticated для аутентификации всего метода бина, как выполняемого пользователем system. Например:

@Autowired
private CurrentAuthentication currentAuthentication;

@Authenticated // authenticates the entire method
@ManagedOperation
public String doSomething3() {
    UserDetails user = currentAuthentication.getUser();
    System.out.println("User: " + user.getUsername()); // system
    // ...
    return "Done";
}

События аутентификации

Фреймворк Spring посылает определенные события приложения, связанные с аутентификацией.

Studio может помочь вам создать слушателей событий аутентификации. Нажмите New (+) → Event Listener в окне инструментов Jmix и выберите Authentication Event в диалоговом окне.

Ниже приведен пример обработки событий аутентификации.

@Component
public class AuthenticationEventListener {

    private static final Logger log =
            LoggerFactory.getLogger(AuthenticationEventListener.class);

    @EventListener
    public void onInteractiveAuthenticationSuccess(
            InteractiveAuthenticationSuccessEvent event) { (1)
        User user = (User) event.getAuthentication().getPrincipal(); (2)
        log.info("User logged in: " + user.getUsername());
    }

    @EventListener
    public void onAuthenticationSuccess(
            AuthenticationSuccessEvent event) { (3)
        User user = (User) event.getAuthentication().getPrincipal(); (4)
        log.info("User authenticated " + user.getUsername());
    }

    @EventListener
    public void onAuthenticationFailure(
            AbstractAuthenticationFailureEvent event) { (5)
        String username = (String) event.getAuthentication().getPrincipal(); (6)
        log.info("User login attempt failed: " + username);
    }

    @EventListener
    public void onLogoutSuccess(LogoutSuccessEvent event) { (7)
        User user = (User) event.getAuthentication().getPrincipal(); (8)
        log.info("User logged out: " + user.getUsername());
    }
}
1 InteractiveAuthenticationSuccessEvent посылается, когда пользователь входит в систему через UI или REST API.
2 InteractiveAuthenticationSuccessEvent содержит сущность пользователя.
3 AuthenticationSuccessEvent отправляется при любой успешной аутентификации, включая системную.
Не используйте бин CurrentAuthentication для получения текущего пользователя в этом слушателе. Данное событие отправляется слишком рано в процессе аутентификации, из-за чего бин вызовет исключение или вернет предыдущий объект аутентификации. Вместо этого получайте текущего пользователя из объекта AuthenticationSuccessEvent.
4 AuthenticationSuccessEvent содержит сущность пользователя.
5 AbstractAuthenticationFailureEvent посылается, если попытка аутентификации не удалась, например, из-за ввода неверных учетных данных.
6 AbstractAuthenticationFailureEvent содержит только имя пользователя, указанное при аутентификации.
7 LogoutSuccessEvent посылается при выходе пользователя из системы.
8 LogoutSuccessEvent содержит сущность пользователя.

Пользовательская сессия

При подключении к приложению Jmix через Jmix UI или REST API, создается пользовательская сессия на основе HTTP-сессии. В этом разделе описаны детали работы пользовательских сессий для разных клиентов, управление временем жизни сессии и хранение пользовательских значений в сессии.

Вы можете отслеживать пользовательские сессии во время выполнения в экране User sessions дополнения Audit.

Максимальное количество сессий на одного пользователя можно задать с помощью свойства jmix.core.session.maximum-sessions-per-user.

Сессии в UI

По умолчанию пользовательская сессия в UI активна, пока открыта хотя бы одна вкладка браузера с приложением. Если все вкладки закрыты, сессия сохраняется в течение периода, заданного в свойстве server.servlet.session.timeout.

Свойство server.servlet.session.timeout влияет на встроенный веб-сервер, используемый при развертывании приложения в виде исполняемого JAR-файла. Если вы используете развертывание WAR во внешнем веб-сервере, настройте время жизни HTTP-сессии в его конфигурации.

На завершение сессии влияют следующие свойства приложения:

  • vaadin.heartbeatInterval — интервал в секундах между heartbeat-запросами, которые клиент отправляет на сервер, пока открыта вкладка браузера. Значение по умолчанию — 300 (5 минут). Эти запросы поддерживают сессию активной даже при отсутствии действий пользователя. Интервал между запросами должен быть меньше, чем server.servlet.session.timeout.

  • vaadin.closeIdleSessions — если установлено в true, heartbeat-запросы игнорируются, и сессия завершается после периода неактивности, заданного в server.servlet.session.timeout.

Например, следующие свойства приложения устанавливают время жизни сессии в 10 минут и интервал heartbeat-запросов в 3 минуты:

server.servlet.session.timeout=10m
vaadin.heartbeatInterval=90

Подробнее см. в документации Vaadin: User Session.

Сессии в REST API

По умолчанию пользовательские сессии в REST API создаются для каждого запроса и завершаются в соответствии со значением свойства server.servlet.session.timeout. Если клиент поддерживает cookies, сессия будет поддерживаться между всеми запросами с одинаковым cookie.

Подсистема Jmix Sessions привязывает пользовательские сессии к OAuth2-токенам, используемым для аутентификации в REST-эндпойнтах. Это позволяет поддерживать сессию между всеми запросами с одинаковым токеном. Чтобы включить Jmix Sessions в проект, добавьте следующую зависимость в файл build.gradle:

implementation 'io.jmix.sessions:jmix-sessions-starter'
Если сессия теряется при перезапуске сервера, а соответствующий токен сохраняется (например, если он хранится в базе данных), для этого токена создается новая сессия.

Атрибуты сессии

Если вам необходимо использовать некоторые значения в нескольких запросах от одного и того же подключенного пользователя, используйте бин SessionData. В нем есть методы для чтения и записи именованных значений, хранящихся в текущей сессии пользователя.

Бин SessionData можно инжектировать напрямую в экраны UI:

public class CustomerListView extends StandardListView<Customer> {

    @Autowired
    private SessionData sessionData;

В singleton-бине используйте SessionData через org.springframework.beans.factory.ObjectProvider:

@Component
public class CustomerService {

    @Autowired
    private ObjectProvider<SessionData> sessionDataProvider;

    public void saveSessionValue(String value) {
        sessionDataProvider.getObject().setAttribute("my-attribute", value);
    }
Атрибуты сессии также можно использовать в запросах JPQL.