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 Mapusers = 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 Mapusers = 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 .