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

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

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

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

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

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

  • getAuthentication() возвращает объект Authentication, установленный в текущем потоке выполнения. Его можно использовать, чтобы получить коллекцию полномочий текущего пользователя. В стандартной реализации безопасности Jmix эта коллекция содержит объекты полномочий для каждой ресурсной и row-level роли, назначенной пользователю.

  • 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.

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

Если вам необходимо использовать некоторые значения в нескольких запросах от одного и того же подключенного пользователя, используйте бин 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.

При обработке запросов UI общие значения сохраняются в сессии HTTP.

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

Поток выполнения может быть не аутентифицирован, если был запущен внутренним планировщиком или обрабатывает запрос из 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 отправляется при любой успешной аутентификации, включая системную.
4 AuthenticationSuccessEvent содержит сущность пользователя.
5 AbstractAuthenticationFailureEvent посылается, если попытка аутентификации не удалась, например, из-за ввода неверных учетных данных.
6 AbstractAuthenticationFailureEvent содержит только имя пользователя, указанное при аутентификации.
7 LogoutSuccessEvent посылается при выходе пользователя из системы.
8 LogoutSuccessEvent содержит сущность пользователя.