«1. Обзор

Jakarta EE 8 Security API — это новый стандарт и портативный способ решения проблем безопасности в Java-контейнерах.

В этой статье мы рассмотрим три основные функции API:

  1. HTTP Authentication Mechanism
  2. Identity Store
  3. Security Context

Сначала мы поймем, как настроить предоставленные реализации, а затем как реализовать пользовательскую.

2. Зависимости Maven

Чтобы настроить Jakarta EE 8 Security API, нам нужна либо серверная реализация, либо явная.

2.1. Использование серверной реализации

Серверы, совместимые с Jakarta EE 8, уже предоставляют реализацию Jakarta EE 8 Security API, поэтому нам нужен только артефакт Maven Jakarta EE Web Profile API:

<dependencies>
    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-web-api</artifactId>
        <version>8.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

2.2. Использование явной реализации

Сначала мы указываем артефакт Maven для Jakarta EE 8 Security API:

<dependencies>
    <dependency>
        <groupId>javax.security.enterprise</groupId>
        <artifactId>javax.security.enterprise-api</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

Затем мы добавим реализацию, например, Soteria — эталонную реализацию: ~ ~~

<dependencies>
    <dependency>
        <groupId>org.glassfish.soteria</groupId>
        <artifactId>javax.security.enterprise</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

3. Механизм HTTP-аутентификации

До выпуска Jakarta EE 8 механизмы аутентификации настраивались декларативно через файл web.xml.

В этой версии Jakarta EE 8 Security API разработал новый интерфейс HttpAuthenticationMechanism в качестве замены. Поэтому веб-приложения теперь могут настраивать механизмы аутентификации, предоставляя реализации этого интерфейса.

К счастью, контейнер уже предоставляет реализацию для каждого из трех методов аутентификации, определенных спецификацией сервлета: базовая аутентификация HTTP, аутентификация на основе форм и аутентификация на основе настраиваемых форм.

Он также предоставляет аннотацию для запуска каждой реализации:

  1. @BasicAuthenticationMechanismDefinition
  2. @FormAuthenticationMechanismDefinition
  3. @CustomFormAuthenrticationMechanismDefinition

3.1. Базовая HTTP-аутентификация

Как упоминалось выше, веб-приложение может настроить базовую HTTP-аутентификацию, просто используя аннотацию @BasicAuthenticationMechanismDefinition в компоненте CDI:

@BasicAuthenticationMechanismDefinition(
  realmName = "userRealm")
@ApplicationScoped
public class AppConfig{}

На этом этапе контейнер сервлета ищет и создает экземпляр предоставленной реализации. интерфейса HttpAuthenticationMechanism.

При получении несанкционированного запроса контейнер запрашивает у клиента предоставление подходящей аутентификационной информации через заголовок ответа WWW-Authenticate.

WWW-Authenticate: Basic realm="userRealm"

Затем клиент отправляет имя пользователя и пароль, разделенные двоеточием «:» и закодированные в Base64, через заголовок запроса авторизации:

//user=baeldung, password=baeldung
Authorization: Basic YmFlbGR1bmc6YmFlbGR1bmc=

Обратите внимание, что диалоговое окно, представленное для предоставления учетных данных идет из браузера, а не с сервера.

3.2. HTTP-аутентификация на основе форм

Аннотация @FormAuthenticationMechanismDefinition запускает аутентификацию на основе форм, как определено в спецификации сервлета.

Затем у нас есть возможность указать страницы входа и ошибок или использовать разумные страницы по умолчанию /login и /login-error:

@FormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(
    loginPage = "/login.html",
    errorPage = "/login-error.html"))
@ApplicationScoped
public class AppConfig{}

В результате вызова loginPage сервер должен отправить форму на client:

<form action="j_security_check" method="post">
    <input name="j_username" type="text"/>
    <input name="j_password" type="password"/>
    <input type="submit">
</form>

Затем клиент должен отправить форму предварительно определенному вспомогательному процессу аутентификации, предоставляемому контейнером.

3.3. Пользовательская HTTP-аутентификация на основе форм

Веб-приложение может инициировать реализацию пользовательской аутентификации на основе форм с помощью аннотации @CustomFormAuthenticationMechanismDefinition:

@CustomFormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(loginPage = "/login.xhtml"))
@ApplicationScoped
public class AppConfig {
}

Но в отличие от аутентификации на основе форм по умолчанию, мы настраиваем настраиваемый вход в систему. страницу и вызов метода SecurityContext.authenticate() в качестве вспомогательного процесса проверки подлинности.

Давайте также взглянем на вспомогательный LoginBean, который содержит логику входа в систему:

@Named
@RequestScoped
public class LoginBean {

    @Inject
    private SecurityContext securityContext;

    @NotNull private String username;

    @NotNull private String password;

    public void login() {
        Credential credential = new UsernamePasswordCredential(
          username, new Password(password));
        AuthenticationStatus status = securityContext
          .authenticate(
            getHttpRequestFromFacesContext(),
            getHttpResponseFromFacesContext(),
            withParams().credential(credential));
        // ...
    }
     
    // ...
}

В результате вызова пользовательской страницы login.xhtml клиент отправляет полученную форму в Login() LoginBean. метод:

//...
<input type="submit" value="Login" jsf:action="#{loginBean.login}"/>

3.4. Пользовательский механизм аутентификации

Интерфейс HttpAuthenticationMechanism определяет три метода. Наиболее важной является функция validateRequest(), которую мы должны реализовать.

В большинстве случаев достаточно поведения по умолчанию для двух других методов, secureResponse() и cleanSubject().

Давайте посмотрим на пример реализации:

@ApplicationScoped
public class CustomAuthentication 
  implements HttpAuthenticationMechanism {

    @Override
    public AuthenticationStatus validateRequest(
      HttpServletRequest request,
      HttpServletResponse response, 
      HttpMessageContext httpMsgContext) 
      throws AuthenticationException {
 
        String username = request.getParameter("username");
        String password = response.getParameter("password");
        // mocking UserDetail, but in real life, we can obtain it from a database
        UserDetail userDetail = findByUserNameAndPassword(username, password);
        if (userDetail != null) {
            return httpMsgContext.notifyContainerAboutLogin(
              new CustomPrincipal(userDetail),
              new HashSet<>(userDetail.getRoles()));
        }
        return httpMsgContext.responseUnauthorized();
    }
    //...
}

«

«Здесь реализация обеспечивает бизнес-логику процесса проверки, но на практике рекомендуется делегировать IdentityStore через IdentityStoreHandler, вызывая проверку.

Мы также аннотировали реализацию аннотацией @ApplicationScoped, так как нам нужно сделать ее поддерживающей CDI.

HttpMessageContext.notifyContainerAboutLogin(Principal principal, Set groups)

После действительной проверки учетных данных и возможного получения ролей пользователей реализация должна уведомить контейнер, а затем:

3.5. Обеспечение безопасности сервлета

@WebServlet("/secured")
@ServletSecurity(
  value = @HttpConstraint(rolesAllowed = {"admin_role"}),
  httpMethodConstraints = {
    @HttpMethodConstraint(
      value = "GET", 
      rolesAllowed = {"user_role"}),
    @HttpMethodConstraint(     
      value = "POST", 
      rolesAllowed = {"admin_role"})
  })
public class SecuredServlet extends HttpServlet {
}

Веб-приложение может применять ограничения безопасности, используя аннотацию @ServletSecurity в реализации сервлета:

Эта аннотация имеет два атрибута — httpMethodConstraints и value; httpMethodConstraints используется для указания одного или нескольких ограничений, каждое из которых представляет управление доступом к методу HTTP с помощью списка разрешенных ролей.

Затем контейнер будет проверять для каждого шаблона URL и метода HTTP, есть ли у подключенного пользователя подходящая роль для доступа к ресурсу.

4. Identity Store

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

IdentityStore предназначен и рекомендуется для использования HttpAuthenticationMecanism через вызываемый интерфейс IdentityStoreHandler. Реализация IdentityStoreHandler по умолчанию предоставляется контейнером Servlet.

Приложение может предоставить свою реализацию IdentityStore или использовать одну из двух встроенных реализаций, предоставляемых контейнером для базы данных и LDAP.

4.1. Встроенные хранилища удостоверений

Сервер, совместимый с Jakarta EE, должен обеспечивать реализацию двух хранилищ удостоверений: базы данных и LDAP.

@DatabaseIdentityStoreDefinition(
  dataSourceLookup = "java:comp/env/jdbc/securityDS",
  callerQuery = "select password from users where username = ?",
  groupsQuery = "select GROUPNAME from groups where username = ?",
  priority=30)
@ApplicationScoped
public class AppConfig {
}

Реализация базы данных IdentityStore инициализируется путем передачи данных конфигурации в аннотацию @DataBaseIdentityStoreDefinition:

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

IdentityStore с высоким приоритетом обрабатывается позже IdentityStoreHandler.

@LdapIdentityStoreDefinition(
  url = "ldap://localhost:10389",
  callerBaseDn = "ou=caller,dc=baeldung,dc=com",
  groupSearchBase = "ou=group,dc=baeldung,dc=com",
  groupSearchFilter = "(&(member=%s)(objectClass=groupOfNames))")
@ApplicationScoped
public class AppConfig {
}

Как и база данных, реализация LDAP IdentityStore инициализируется через @LdapIdentityStoreDefinition путем передачи данных конфигурации:

Здесь нам нужен URL-адрес внешнего сервера LDAP, как искать вызывающего абонента в каталоге LDAP и как чтобы получить его группы.

4.2. Реализация пользовательского хранилища IdentityStore

default CredentialValidationResult validate(
  Credential credential)
default Set<String> getCallerGroups(
  CredentialValidationResult validationResult)
default int priority()
default Set<ValidationType> validationTypes()

Интерфейс IdentityStore определяет четыре метода по умолчанию:

Метод priority() возвращает значение для порядка итерации, эта реализация обрабатывается IdentityStoreHandler. IdentityStore с более низким приоритетом обрабатывается первым.

По умолчанию IdentityStore обрабатывает как проверку учетных данных (ValidationType.VALIDATE), так и извлечение группы (ValidationType.PROVIDE_GROUPS). Мы можем переопределить это поведение, чтобы оно могло предоставлять только одну возможность.

@Override
public Set<ValidationType> validationTypes() {
    return EnumSet.of(ValidationType.VALIDATE);
}

Таким образом, мы можем настроить IdentityStore для использования только для проверки учетных данных:

@ApplicationScoped
public class InMemoryIdentityStore implements IdentityStore {
    // init from a file or harcoded
    private Map<String, UserDetails> users = new HashMap<>();

    @Override
    public int priority() {
        return 70;
    }

    @Override
    public Set<ValidationType> validationTypes() {
        return EnumSet.of(ValidationType.VALIDATE);
    }

    public CredentialValidationResult validate( 
      UsernamePasswordCredential credential) {
 
        UserDetails user = users.get(credential.getCaller());
        if (credential.compareTo(user.getLogin(), user.getPassword())) {
            return new CredentialValidationResult(user.getLogin());
        }
        return INVALID_RESULT;
    }
}

В этом случае мы должны предоставить реализацию для метода validate():

@Override
public Set<ValidationType> validationTypes() {
    return EnumSet.of(ValidationType.PROVIDE_GROUPS);
}

Или мы можем выбрать настроить IdentityStore так, чтобы его можно было использовать только для группового поиска:

@ApplicationScoped
public class InMemoryIdentityStore implements IdentityStore {
    // init from a file or harcoded
    private Map<String, UserDetails> users = new HashMap<>();

    @Override
    public int priority() {
        return 90;
    }

    @Override
    public Set<ValidationType> validationTypes() {
        return EnumSet.of(ValidationType.PROVIDE_GROUPS);
    }

    @Override
    public Set<String> getCallerGroups(CredentialValidationResult validationResult) {
        UserDetails user = users.get(
          validationResult.getCallerPrincipal().getName());
        return new HashSet<>(user.getRoles());
    }
}

Затем мы должны предоставить реализацию для методов getCallerGroups():

Поскольку IdentityStoreHandler ожидает, что реализация будет CDI bean-компонентом , украшаем его аннотацией ApplicationScoped.

5. Security Context API

Jakarta EE 8 Security API предоставляет точку доступа к программной безопасности через интерфейс SecurityContext. Это альтернатива, когда декларативной модели безопасности, применяемой контейнером, недостаточно.

@Inject
SecurityContext securityContext;

Реализация интерфейса SecurityContext по умолчанию должна предоставляться во время выполнения в виде компонента CDI, и поэтому нам необходимо внедрить его:

«

«На этом этапе мы можем аутентифицировать пользователя, получить аутентифицированного, проверить его принадлежность к роли и предоставить или запретить доступ к веб-ресурсу пятью доступными способами.

5.1. Получение данных вызывающего абонента

В предыдущих версиях Jakarta EE мы извлекали принципала или проверяли принадлежность к роли по-разному в каждом контейнере.

Principal getCallerPrincipal();
boolean isCallerInRole(String role);
<T extends Principal> Set<T> getPrincipalsByType(Class<T> type);

Пока мы используем методы getUserPrincipal() и isUserInRole() запроса HttpServletRequest в контейнере сервлетов, аналогичные методы getCallerPrincipal() и isCallerInRole() объекта EJBContext используются в контейнере EJB.

Новый Jakarta EE 8 Security API стандартизировал это, предоставив аналогичный метод через интерфейс SecurityContext: все принципы данного типа.

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

5.2. Тестирование доступа к веб-ресурсам

@WebServlet("/protectedServlet")
@ServletSecurity(@HttpConstraint(rolesAllowed = "USER_ROLE"))
public class ProtectedServlet extends HttpServlet {
    //...
}

Сначала нам нужно настроить защищенный ресурс:

securityContext.hasAccessToWebResource("/protectedServlet", "GET");

А затем, чтобы проверить доступ к этому защищенному ресурсу, мы должны вызвать метод hasAccessToWebResource():

В в этом случае метод возвращает true, если пользователь находится в роли USER_ROLE.

5.3. Аутентификация вызывающего объекта программно

AuthenticationStatus authenticate(
  HttpServletRequest request, 
  HttpServletResponse response,
  AuthenticationParameters parameters);

Приложение может программно инициировать процесс аутентификации, вызвав authentication():

withParams().credential(credential)

Контейнер получает уведомление и, в свою очередь, вызывает механизм аутентификации, настроенный для приложения. Параметр AuthenticationParameters предоставляет учетные данные для HttpAuthenticationMechanism:

Значения SUCCESS и SEND_FAILURE в AuthenticationStatus определяют успешную и неудачную аутентификацию, в то время как SEND_CONTINUE сигнализирует о текущем состоянии процесса аутентификации.

6. Запуск примеров

Для выделения этих примеров мы использовали последнюю разрабатываемую сборку Open Liberty Server, которая поддерживает Jakarta EE 8. Она загружается и устанавливается благодаря плагину Liberty-maven, который может также разверните приложение и запустите сервер.

mvn clean package liberty:run

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

В результате Maven загрузит сервер, создаст, развернет и запустит приложение.

7. Заключение

В этой статье мы рассмотрели настройку и реализацию основных функций нового Jakarta EE 8 Security API.

Во-первых, мы начали с демонстрации того, как настроить встроенные механизмы аутентификации по умолчанию и как реализовать собственный. Позже мы увидели, как настроить встроенное хранилище удостоверений и как реализовать собственное. И, наконец, мы увидели, как вызывать методы SecurityContext.