API безопасности Java EE 8
1. обзор
API безопасности Java EE 8 - это новый стандарт и переносимый способ решения проблем безопасности в контейнерах Java.
В этой статьеwe’ll look at the three core features of the API:
-
Механизм HTTP-аутентификации
-
Фирменный магазин
-
Контекст безопасности
Сначала мы поймем, как настроить предоставленные реализации, а затем как реализовать индивидуальную.
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, аутентификация на основе форм и настраиваемая аутентификация на основе форм.
Он также предоставляет аннотацию для запуска каждой реализации:
-
@BasicAuthenticationMechanismDefinition
-
@FormAuthenticationMechanismDefinition
-
@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.