Рубрики
Без рубрики

Jakarta EE 8 API безопасности

Узнайте, как использовать API Java 8 для добавления безопасности в приложение Jakarta EE.

Автор оригинала: baeldung.

1. Обзор

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

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

  1. Механизм аутентификации HTTP
  2. Хранилище идентификационных данных
  3. Контекст безопасности

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

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

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

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

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


    
        javax
        javaee-web-api
        8.0
        provided
    

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

Во-первых, мы указываем артефакт Maven для Jakarta EE 8 Security API :


    
        javax.security.enterprise
        javax.security.enterprise-api
        1.0
    

А затем мы добавим реализацию, например, Soteria – эталонная реализация:


    
        org.glassfish.soteria
        javax.security.enterprise
        1.0
    

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

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

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

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

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

  1. @BasicAuthenticationMechanismDefinition
  2. @FormAuthenticationMechanismDefinition
  3. @Customformauthenticationmechanismdefinition

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

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

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

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

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

WWW-Authenticate: Basic realm="userRealm"

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

//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{}

В результате вызова страницы входа в систему/| сервер должен отправить форму клиенту:

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

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 клиент отправляет полученную форму в метод LoginBean’ s login() :

//...

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

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

Поведение по умолчанию для двух других методов, secure Response() и clean Subject() , в большинстве случаев достаточно.

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

@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 b y вызов validate .

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

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

HttpMessageContext.notifyContainerAboutLogin(Principal principal, Set groups)

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

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

@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 {
}

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

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

4. Хранилище идентификационных данных

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

Хранилище идентификационных данных предназначено и рекомендуется для использования Механизмом аутентификации Http через вызываемый интерфейс IdentityStoreHandler . Реализация по умолчанию обработчика хранилища идентификаторов предоставляется сервлетом | контейнером.

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

4.1. Встроенные хранилища идентификационных данных

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

База данных Хранилище идентификационных данных реализация инициализируется путем передачи данных конфигурации в @DataBaseIdentityStoreDefinition аннотация:

@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 {
}

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

Хранилище идентификационных данных с высоким приоритетом обрабатывается позже обработчиком хранилища идентификационных данных|/.

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

@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 {
}

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

4.2. Реализация Пользовательского хранилища идентификационных данных

То Хранилище идентификационных данных интерфейс определяет четыре метода по умолчанию:

default CredentialValidationResult validate(
  Credential credential)
default Set getCallerGroups(
  CredentialValidationResult validationResult)
default int priority()
default Set validationTypes()

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

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

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

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

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

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

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

    @Override
    public Set 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;
    }
}

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

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

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

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

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

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

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

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

5. API Контекста безопасности

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

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

@Inject
SecurityContext securityContext;

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

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

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

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

Новый API безопасности Jakarta EE 8 стандартизировал это около предоставление аналогичного метода через Контекст безопасности интерфейс:

Principal getCallerPrincipal();
boolean isCallerInRole(String role);
 Set getPrincipalsByType(Class type);

Метод getCallerPrincipal() возвращает специфичное для контейнера представление аутентифицированного вызывающего абонента, в то время как метод getPrincipalsByType() извлекает все участники данного типа.

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

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

Во-первых, нам нужно настроить защищенный ресурс:

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

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

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

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

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

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

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

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

withParams().credential(credential)

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

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

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

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

mvn clean package liberty:run

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

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

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

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

Как всегда, примеры кода для этой статьи доступны на GitHub .