1. Обзор
Jakarta EE 8 Security API — это новый стандарт и портативный способ решения проблем безопасности в Java-контейнерах.
В этой статье мы рассмотрим три основные функции API:
- Механизм аутентификации HTTP
- Магазин удостоверений
- Контекст безопасности
Сначала мы поймем, как настроить предоставленные реализации, а затем как реализовать пользовательскую.
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, аутентификация на основе форм и аутентификация на основе настраиваемых форм.
Он также предоставляет аннотацию для запуска каждой реализации:
@BasicAuthenticationMechanismDefinition
@FormAuthenticationMechanismDefinition
@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 .