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

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

1. обзор

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

В этой статьеwe’ll look at the three core features of the API:

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

  2. Фирменный магазин

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

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

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

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

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

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


    
        javax
        javaee-web-api
        8.0
        provided
    

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

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


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

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


    
        org.glassfish.soteria
        javax.security.enterprise
        1.0
    

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

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

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

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

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

  1. @BasicAuthenticationMechanismDefinition

  2. @FormAuthenticationMechanismDefinition

  3. @CustomFormAuthenrticationMechanismDefinition

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

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

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

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

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

WWW-Authenticate: Basic realm="userRealm"

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

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

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

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

The @FormAuthenticationMechanismDefinition annotation triggers a form-based authentication, как определено в спецификации сервлета.

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

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

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

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

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'slogin():

//...

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

ИнтерфейсHttpAuthenticationMechanism определяет три метода. The most important is the 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 by, вызываяvalidate.

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

После действительной проверки учетных данных и возможного получения ролей пользователейthe implementation should notify the container then:

HttpMessageContext.notifyContainerAboutLogin(Principal principal, Set groups)

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

A web application can enforce security constraints by using the @ServletSecurity annotation on a Servlet implementation:

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

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

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

4. Фирменный магазин

Эта функция абстрагируетсяthe IdentityStore interface, and it’s used to validate credentials and eventually retrieve group membership. , другими словами, она может предоставлять возможности для аутентификации, авторизации или того и другого..

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

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

4.1. Встроенные магазины

Сервер, совместимый с Java EE, должен предоставлять реализации дляthe two Identity Stores: Database and LDAP.

Реализация базы данныхIdentityStore инициализируется путем передачи данных конфигурации в аннотацию@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 {
}

В качестве данных конфигурации настраиваютсяwe need a JNDI data source to an external database, два оператора JDBC для проверки вызывающего абонента и его групп и, наконец, параметр приоритета, который используется в случае кратного хранилища.

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

Как и база данных,LDAP IdentityStore implementation is initialized through the @LdapIdentityStoreDefinition путем передачи данных конфигурации:

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

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

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

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

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

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

По умолчаниюIdentityStore обрабатывает как проверку учетных данных(ValidationType.VALIDATE), так и получение группы (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;
    }
}

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

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

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

@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());
    }
}

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

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

API безопасности Java EE 8 предоставляетan access point to programmatic security through the SecurityContext interface. Это альтернатива, когда декларативной модели безопасности, обеспечиваемой контейнером, недостаточно.

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

@Inject
SecurityContext securityContext;

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

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

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

Хотя мы используем методыgetUserPrincipal() и isUserInRole() изHttpServletRequest в контейнере сервлета, аналогичные методыgetCallerPrincipal() andisCallerInRole() methods oftEJBContext используются в Контейнер EJB.

Новый API безопасности Java EE 8 стандартизировал этотby , предоставляя аналогичный метод через интерфейсSecurityContext:

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

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

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

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

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

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

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

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

withParams().credential(credential)

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

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

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

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

mvn clean package liberty:run

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

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

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

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

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