Перейти к основному содержимому

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

· 10 мин. чтения

1. Обзор

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

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

  1. Механизм аутентификации HTTP
  2. Магазин удостоверений
  3. Контекст безопасности

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

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=foreach, password=foreach
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 сервер должен отправить форму клиенту:

<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 , вызывая validate .

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

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

HttpMessageContext.notifyContainerAboutLogin(Principal principal, Set groups)

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

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

@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-адреса и метода HTTP, есть ли у подключенного пользователя подходящая роль для доступа к ресурсу.

4. Магазин удостоверений

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

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

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

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

Сервер, совместимый с Jakarta EE, должен обеспечивать реализацию двух хранилищ удостоверений: базы данных и 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 {
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@Inject
SecurityContext securityContext;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

withParams().credential(credential)

Значения 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.

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