1. Обзор
API безопасности Jakarta EE 8 – это новый стандарт и переносимый способ решения проблем безопасности в контейнерах Java.
В этой статье мы рассмотрим три основные функции API:
- Механизм аутентификации HTTP
- Хранилище идентификационных данных
- Контекст безопасности
Сначала мы поймем, как настроить предоставленные реализации, а затем как реализовать пользовательскую.
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, проверка подлинности на основе форм и пользовательская проверка подлинности на основе форм.
Он также предоставляет аннотацию для запуска каждой реализации:
- @BasicAuthenticationMechanismDefinition
- @FormAuthenticationMechanismDefinition
- @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 SetgetCallerGroups( CredentialValidationResult validationResult) default int priority() default Set validationTypes()
Метод priority() возвращает значение для порядка итерации, в котором эта реализация обрабатывается IdentityStoreHandler. Сначала обрабатывается Хранилище идентификационных данных с более низким приоритетом.
По умолчанию хранилище идентификаторов обрабатывает как проверку учетных данных (ValidationType.VALIDATOR) , так и групповое извлечение( ValidationType.PROVIDE_GROUPS ). Мы можем переопределить это поведение, чтобы оно обеспечивало только одну возможность.
Таким образом, мы можем настроить IdentityStore для использования только для проверки учетных данных:
@Override public SetvalidationTypes() { 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 SetvalidationTypes() { 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 .