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

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

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 могут быть разные клиенты, например, UI, GraphQL, или REST API. Каждый клиент имеет свой собственный стандартный механизм аутентификации, например:

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

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

package security.ex1.security;

import io.jmix.securityui.password.PasswordValidationContext;
import io.jmix.securityui.password.PasswordValidationException;
import io.jmix.securityui.password.PasswordValidator;
import org.springframework.stereotype.Component;
import security.ex1.entity.User;

@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
protected void onBeforeCommit(BeforeCommitChangesEvent event) {
    if (entityStates.isNew(getEditedEntity())) {
        // ...
        List<String> validationErrors = passwordValidation.validate(getEditedEntity(), passwordField.getValue());
        if (!validationErrors.isEmpty()) {
            notifications.create(Notifications.NotificationType.WARNING)
                    .withCaption(String.join("\n", validationErrors))
                    .show();
            event.preventCommit();
        }
        getEditedEntity().setPassword(passwordEncoder.encode(passwordField.getValue()));
    }
}

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

В фреймворке есть механизм защиты от взлома паролей методом перебора, которая обеспечивается свойством приложения 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 CustomerBrowse extends StandardLookup<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.

Для совместного использования атрибутов сессии в запросах REST API, аутентифицированных с помощью одного и того же токена, добавьте следующую зависимость в build.gradle:

implementation 'io.jmix.sessions:jmix-sessions-starter'

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

Поток выполнения может быть не аутентифицирован, если был запущен внутренним планировщиком или обрабатывает запрос из 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 содержит сущность пользователя.