3.5.19. Процесс входа в Web Client

В данном разделе описывается, как работает аутентификация на веб-клиент и как расширить ее в проекте. Для информации об аутентификации на среднем слое см. Вход в систему.

Реализация логина в Web Client включает следующие механизмы:

  • Connection реализованный классом ConnectionImpl.

  • Реализации интерфейса LoginProvider.

  • Реализации интерфейса HttpRequestFilter.

WebLoginStructure
Рисунок 27. Механизмы логина в Web Client

Основной интерфейс подсистемы входа в Web Client - Connection, включающий следующие основные методы:

  • login() - аутентифицирует пользователя, создаёт пользовательскую сессию и изменяет состояние соединения.

  • logout() - выполняет выход из системы.

  • substituteUser() - замещает пользователя в текущей сессии. Этот метод создаёт новый объект UserSession, но с тем же ID.

  • getSession() - возвращает текущую сессию.

После успешного входа Connection устанавливает объект UserSession в атрибут VaadinSession и устанавливает SecurityContext. Объект Connection связан с VaadinSession, поэтому вы не можете использовать его из фоновых потоков, при попытке вызова login/logout из фонового потока выбрасывается исключение IllegalConcurrentAccessException.

Обычно, логин выполняется из экрана LoginScreen, который поддерживает вход при помощи логина/пароля и токена "запомнить меня".

Реализация Connection по умолчанию - ConnectionImpl, который делегирует логин цепочке объектов LoginProvider. Интерфейс LoginProvider предназначен для реализации модулей входа, которые могут обрабатывать специфичные реализации интерфейса Credentials, также этот интерфейс включает метод supports(), позволяющий проверить поддерживает ли модули определённый тип Credentials.

WebLoginProcedure
Рисунок 28. Стандартный процесс входа в Web Client

Стандартный процесс входа:

  • Пользователь вводит свой логин и пароль.

  • Web Client создаёт объект LoginPasswordCredentials, передав логи и пароль в его конструктор, и вызывает метод Connection.login() с этими данными для входа.

  • Connection использует цепочку объектов LoginProvider. Существует стандартный модуль входа LoginPasswordLoginProvider, который работает с аутентификационными данными типа LoginPasswordCredentials. В зависимости от значения свойства cuba.checkPasswordOnClient, он либо вызывает AuthenticationService.login(Credentials) передавая логин и пароль пользователя, либо загружает сущность User по логину, проверяет пароль на соответствие загруженному хэшу, и выполняет вход как доверенный клиент используя TrustedClientCredentials и cuba.trustedClientPassword.

  • Если вход выполнен успешно, то объект AuthenticationDetails с активной сессией UserSession возвращается в Connection.

  • Connection создаёт специальный класс-обёртку ClientUserSession и устанавливает его в атрибут VaadinSession.

  • Connection создаёт экземпляр SecurityContext и устанавливает его в AppContext.

  • Connection публикует событие StateChangeEvent, стандартный обработчик которого обновляет UI и инициализирует MainScreen.

Все реализации LoginProvider должны:

  • Аутентифицировать пользователя при помощи переданного объекта Credentials.

  • Создать и запустить новую пользовательскую сессию при помощи AuthenticationService или вернуть существующий объект активной сессии (например, для пользователя anonymous).

  • Вернуть данные аутентификации или null если объект Credentials не может быть обработан, например, если модуль входа отключен или не сконфигурирован.

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

HttpRequestFilter - маркерный интерфейс для бинов, которые будут автоматически добавлены в цепочку фильтров приложения в качестве HTTP фильтра: https://docs.oracle.com/javaee/6/api/javax/servlet/Filter.html. Вы можете использовать такой фильтр для реализации дополнительной аутентификации, пре- и пост-обработки запроса и ответа.

Вы можете реализовать такой Filter если создадите компонент Spring Framework и реализуете интерфейс HttpRequestFilter:

@Component
public class CustomHttpFilter implements HttpRequestFilter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain)
            throws IOException, ServletException {
        // delegate to the next filter/servlet
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
    }
}

Обратите внимание, что минимальная реализация должна делегировать исполнение объекту FilterChain, в противном случае ваше приложение будет неработоспособно. По умолчанию фильтры добавленные как бины HttpRequestFilter не будут получать запросы к каталогу VAADIN и другим путям, указанным в свойстве приложения cuba.web.cubaHttpFilterBypassUrls.

Встроенные провайдеры входа

Платформа включает следующие реализации интерфейса LoginProvider:

  • AnonymousLoginProvider - предоставляет анонимный вход для пользователей, не выполнивших вход в систему.

  • LoginPasswordLoginProvider - делегирует логин сервису AuthenticationService, передавая LoginPasswordCredentials.

  • RememberMeLoginProvider- делегирует логин сервису AuthenticationService, передавая RememberMeCredentials.

  • LdapLoginProvider - принимает LoginPasswordCredentials, выполняет аутентификацию при помощи LDAP и делегирует логин сервису AuthenticationService, передавая TrustedClientCredentials.

  • ExternalUserLoginProvider - принимает ExternalUserCredentials и делегирует логин сервису AuthenticationService, передавая TrustedClientCredentials. Тем самым позволяет выполнить вход от имени любого пользователя по его логину.

Все реализации создают активную сессию при помощи AuthenticationService.login().

Вы можете переопределить любой из провайдеров входа при помощи механизмов Spring Framework.

События

Стандартная реализация Connection - ConnectionImpl публикует следующие события во время процедуры входа:

  • BeforeLoginEvent / AfterLoginEvent

  • LoginFailureEvent

  • UserConnectedEvent / UserDisconnectedEvent

  • UserSessionStartedEvent / UserSessionFinishedEvent

  • UserSessionSubstitutedEvent

Обработчики событий BeforeLoginEvent и LoginFailureEvent могут выбросить исключение LoginException чтобы прервать процесс входа или переопределить оригинальную причину ошибки входа.

Например, при помощи обработчика BeforeLoginEvent вы можете разрешить вход в Web Client только для пользователей, логин которых включает домен компании.

@Component
public class BeforeLoginEventListener {
    @Order(10)
    @EventListener
    protected void onBeforeLogin(BeforeLoginEvent event) throws LoginException {
        if (event.getCredentials() instanceof LoginPasswordCredentials) {
            LoginPasswordCredentials loginPassword = (LoginPasswordCredentials) event.getCredentials();

            if (loginPassword.getLogin() != null
                    && !loginPassword.getLogin().contains("@company")) {
                throw new LoginException(
                        "Only users from @company are allowed to login");
            }
        }
    }
}

Дополнительно, стандартный класс приложения - DefaultApp публикует следующие события:

  • AppInitializedEvent - публикуется после инициализации объекта App, выполняется один раз для одной HTTP сессии.

  • AppStartedEvent - публикуется во время обработки первого HTTP запроса к App перед инициализацией анонимного входа. Обработчики события могут выполнить вход при помощи Connection, связанного с App.

  • AppLoggedInEvent - публикуется после инициализации UI App сразу после того, как выполнен вход в приложение.

  • AppLoggedOutEvent - публикуется после инициализации UI App сразу после того, как выполнен выход из приложения.

  • SessionHeartbeatEvent - публикуется во время запросов heartbeat от веб-браузера пользователя.

Событие AppStartedEvent может использоваться для реализации прозрачного входа и SSO со сторонними системами, такими как Jasig CAS. Обычно, используется вместе с дополнительной реализацией HttpRequestFilter, которая должна собрать и предоставить дополнительные аутентификационные данные из HTTP запроса.

Допустим, что система должна автоматически выполнять вход для пользователей, у которых есть специальный файл cookie - PROMO_USER.

@Order(10)
@Component
public class AppStartedEventListener implements ApplicationListener<AppStartedEvent> {

    private static final String PROMO_USER_COOKIE = "PROMO_USER";

    @Inject
    private Logger log;

    @Override
    public void onApplicationEvent(AppStartedEvent event) {
        String promoUserLogin = event.getApp().getCookieValue(PROMO_USER_COOKIE);
        if (promoUserLogin != null) {
            Connection connection = event.getApp().getConnection();
            if (!connection.isAuthenticated()) {
                try {
                    connection.login(new ExternalUserCredentials(promoUserLogin));
                } catch (LoginException e) {
                    log.warn("Unable to login promo user {}: {}", promoUserLogin, e.getMessage());
                } finally {
                    event.getApp().removeCookie(PROMO_USER_COOKIE);
                }
            }
        }
    }
}

Так, если веб-браузер хранит файл cookie PROMO_USER, и пользователь откроет приложение, будет выполнен вход от имени пользователя, указанного в promoUserLogin.

Если вы хотите выполнить дополнительные действия после входа в приложение и инициализации UI вы можете реализовать обработчик события AppLoggedInEvent. Обратите внимание, что вы должны проверить, аутентифицирован ли пользователь или нет в обработчиках события, поскольку все события публикуются и для пользователя anonymous, даже если пользователь не аутентифицирован.

События жизненного цикла веб-сессии

Фреймворк посылает следующие события, касающиеся жизненного цикла HTTP-сессии:

  • WebSessionInitializedEvent - публикуется после инициализации HTTP сессии.

  • WebSessionDestroyedEvent - публикуется после завершения HTTP сессии.

Эти события могут использоваться для выполнения некоторой системной логики. Обратите внимание, что в потоке, обрабатывающем событие отсутствует SecurityContext.

Точки расширения

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

  • Connection - заменить существующий ConnectionImpl.

  • HttpRequestFilter - реализовать дополнительный HttpRequestFilter.

  • LoginProvider implementations - реализовать новый или заменить существующий бин LoginProvider.

  • Events - реализовать обработчик одного из доступных событий.

Вы можете заменить существующие бины, используя механизмы Spring Framework, например, зарегистрировав новый бин в конфигурационном файле Spring XML модуля web.

<bean id="cuba_LoginPasswordLoginProvider"
      class="com.company.demo.web.CustomLoginProvider"/>