OpenID Connect
Дополнение Jmix OpenID Connect предоставляет предопределенные конфигурации Spring Security и набор сервисов, которые позволяют легко реализовать в приложении следующие возможности:
-
Аутентификация пользователя с использованием внешнего поставщика OpenID (например, Keycloak).
-
Маппинг атрибутов и ролей пользователя из поставщика OpenID на пользователя Jmix.
-
Сохранение сущности пользователя и назначений ролей после успешной аутентификации пользователя поставщиком OpenID.
Дополнение использует поддержку Spring Security для OAuth2 и OpenID Connect 1.0. Вы можете прочитать об этом в документации Spring Security.
Дополнение применяет конфигурацию OidcAutoConfiguration, если ее явно не отключить, установив свойство приложения jmix.oidc.use-default-configuration=false. Данная конфигурация включает аутентификацию OpenID Connect для URL пользовательского интерфейса и REST API.
См. также руководство REST DataStore with External Authentication для ознакомления с практическими примерами применения дополнения.
Установка
Для автоматической установки через Jmix Marketplace следуйте инструкциям в разделе Add-ons.
Для ручной установки добавьте следующую зависимость в свой build.gradle:
implementation 'io.jmix.oidc:jmix-oidc-starter'
Конфигурация клиента
После включения дополнения в ваш проект и перед запуском приложения необходимо настроить "клиента". Клиент - это приложение Jmix, требующее аутентификации конечных пользователей поставщиком OpenID.
Для настройки клиента используется стандартный подход Spring Security. Например, это можно сделать, добавив следующие свойства в файл application.properties:
spring.security.oauth2.client.registration.keycloak.client-id=<client-id>
spring.security.oauth2.client.registration.keycloak.client-secret=<client-secret>
spring.security.oauth2.client.registration.keycloak.scope=openid, profile, email
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8180/realms/<realm>
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/<realm>
keycloak в ключе свойства - это идентификатор провайдера. Он может иметь любое значение, например, okta, тогда свойство будет spring.security.oauth2.client.registration.okta.client-id.
Значения идентификатора клиента (Client ID) и секрета (Client Secret) должны быть взяты из поставщика OpenID.
Свойство issuer-uri содержит путь к поставщику OpenID Configuration Endpoint.
По умолчанию конфигурация дополнения будет использовать клэйм (claim) sub в качестве имени пользователя Jmix. Если вы хотите изменить это, используйте следующее свойство приложения:
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username
Использование стандартной конфигурации дополнения
После включения дополнения в проект, настройки клиента и настройки Keycloak, приложение можно запустить. В этом случае стандартная конфигурация дополнения будет выполнять следующие действия:
-
Неаутентифицированные пользователи будут перенаправлены на страницу входа поставщика OpenID.
-
После того как пользователь пройдет аутентификацию поставщиком OpenID, будет создан экземпляр
DefaultJmixOidcUserи установлен в контекст безопасности. Атрибуты пользователя не будут отображены. Пользователь не будет сохранен в базе данных. -
Коллекция кодов ролей пользователя будет взята из клэйма "roles" токена и, затем, для каждой роли из значения клэйма, ресурсная роль и роль на уровне строк будут установлены в объекте аутентификации пользователя.
Класс DefaultJmixOidcUser реализует интерфейс JmixOidcUser. Класс пользователя должен всегда реализовывать этот интерфейс, потому что Jmix-приложения требуют интерфейс UserDetails, а Spring Security работает с интерфейсом OidcUser. JmixOidcUser просто расширяет оба эти интерфейса.
Сопоставление атрибутов и ролей пользователя
Если вам нужно работать с пользователем в памяти, но вы хотите заполнить некоторые атрибуты пользователя, создайте класс, который расширяет DefaultJmixOidcUser. В приведенном ниже примере у него есть атрибут position:
import io.jmix.oidc.user.DefaultJmixOidcUser;
public class MyUser extends DefaultJmixOidcUser {
private String position;
public String getPosition() {
return position;
}
public void setPosition(String position) {
this.position = position;
}
}
Затем вам нужно зарегистрировать экземпляр OidcUserMapper как бин Spring. Вы можете расширить BaseOidcUserMapper и переопределить его методы:
import examples.oidcex1.entity.MyUser;
import io.jmix.oidc.claimsmapper.ClaimsRolesMapper;
import io.jmix.oidc.usermapper.BaseOidcUserMapper;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Component;
import java.util.Collection;
@Component
public class MyOidcUserMapper extends BaseOidcUserMapper<MyUser> {
private ClaimsRolesMapper claimsRolesMapper;
public MyOidcUserMapper(ClaimsRolesMapper claimsRolesMapper) {
this.claimsRolesMapper = claimsRolesMapper;
}
@Override
protected String getOidcUserUsername(OidcUser oidcUser) {
return oidcUser.getName();
}
@Override
protected MyUser initJmixUser(OidcUser oidcUser) {
return new MyUser();
}
@Override
protected void populateUserAttributes(OidcUser oidcUser, MyUser jmixUser) {
jmixUser.setPosition((String) oidcUser.getClaims().get("position"));
jmixUser.setDelegate(oidcUser);
}
@Override
protected void populateUserAuthorities(OidcUser oidcUser, MyUser jmixUser) {
Collection<? extends GrantedAuthority> authorities = claimsRolesMapper.toGrantedAuthorities(oidcUser.getClaims());
jmixUser.setAuthorities(authorities);
}
}
При реализации пользовательского маппера OIDC убедитесь, что пользователь Jmix сохраняет исходные данные пользователя OIDC, включая ID токен. ID токен необходим для обработки выхода из системы. Если ваш класс пользователя расширяет DefaultJmixOidcUser или JmixOidcUserEntity, вызовите setDelegate(oidcUser), как показано в примере выше.
|
Обратите внимание, что в приведенном выше примере сопоставление клэймов от пользователя OIDC к полномочиям (granted authorities) Jmix делегируется интерфейсу ClaimsRolesMapper. Реализацией ClaimsRolesMapper по умолчанию является DefaultClaimsRolesMapper, который получает клэйм с именем "roles" из ID токена. Этот клэйм должен содержать коллекцию имен ролей. Затем для каждой роли из значения клэйма выполняется поиск ресурсной роли и роли уровня строк в Jmix. Если они найдены, соответствующие полномочия будут добавлены пользователю. Имя клэйма для ролей можно настроить с помощью следующего свойства приложения:
jmix.oidc.default-claims-roles-mapper.roles-claim-name=myRoles
При необходимости вы можете создать свой маппер клэймов в роли. Самый простой способ сделать это - расширить BaseClaimsRolesMapper и переопределить его методы getResourceRolesCodes() и/или getRowLevelRolesCodes(). Приведенный ниже пример демонстрирует, как назначать роли на основе клэйма "position":
import io.jmix.oidc.claimsmapper.BaseClaimsRolesMapper;
import io.jmix.security.role.ResourceRoleRepository;
import io.jmix.security.role.RoleGrantedAuthorityUtils;
import io.jmix.security.role.RowLevelRoleRepository;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
@Component
public class MyClaimsRoleMapper extends BaseClaimsRolesMapper {
public MyClaimsRoleMapper(ResourceRoleRepository resourceRoleRepository,
RowLevelRoleRepository rowLevelRoleRepository,
RoleGrantedAuthorityUtils roleGrantedAuthorityUtils) {
super(resourceRoleRepository, rowLevelRoleRepository, roleGrantedAuthorityUtils);
}
@Override
protected Collection<String> getResourceRolesCodes(Map<String, Object> claims) {
Collection<String> jmixRoleCodes = new HashSet<>();
String position = (String) claims.get("position");
if ("Manager".equals(position)) {
jmixRoleCodes.add("edit-contracts");
jmixRoleCodes.add("view-archive");
} else {
jmixRoleCodes.add("view-contracts");
}
return jmixRoleCodes;
}
}
Работа с JPA-сущностью пользователя
Для работы с дополнением Jmix OIDC, JPA-сущность User должна реализовывать интерфейс io.jmix.oidc.user.JmixOidcUser, который, в свою очередь, реализует org.springframework.security.oauth2.core.oidc.user.OidcUser, требуемый Spring Security.
Простейший способ сделать сущность User совместимой с дополнением OIDC - сделать этот класс подклассом абстрактного класса io.jmix.oidc.user.JmixOidcUserEntity:
@JmixEntity
@Entity
@Table(name = "USER_", indexes = {
@Index(name = "IDX_USER__ON_USERNAME", columnList = "USERNAME", unique = true)
})
public class User extends JmixOidcUserEntity implements HasTimeZone {
//...
}
|
В случае проекта на Kotlin необходимо сделать дополнительные изменения в сущности
|
Чтобы сохранять пользователей в базе данных после их входа с использованием поставщика OpenID, вам нужно зарегистрировать маппер пользователя, который расширяет класс SynchronizingOidcUserMapper. Этот суперкласс содержит поведение, которое сохраняет/обновляет пользователя в базе данных. При желании, вы также можете сохранять в базе данных и информацию о назначениях ролей.
import examples.oidcex1.entity.User;
import io.jmix.core.UnconstrainedDataManager;
import io.jmix.core.security.UserRepository;
import io.jmix.oidc.claimsmapper.ClaimsRolesMapper;
import io.jmix.oidc.usermapper.SynchronizingOidcUserMapper;
import io.jmix.security.role.RoleGrantedAuthorityUtils;
import org.springframework.context.annotation.Profile;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Component;
@Component
public class MySynchronizingOidcUserMapper extends SynchronizingOidcUserMapper<User> {
public MySynchronizingOidcUserMapper(UnconstrainedDataManager dataManager,
UserRepository userRepository,
ClaimsRolesMapper claimsRolesMapper,
RoleGrantedAuthorityUtils roleGrantedAuthorityUtils) {
super(dataManager, userRepository, claimsRolesMapper, roleGrantedAuthorityUtils);
//store role assignments in the database (false by default)
setSynchronizeRoleAssignments(true);
}
@Override
protected Class<User> getApplicationUserClass() {
return User.class;
}
@Override
protected void populateUserAttributes(OidcUser oidcUser, User jmixUser) {
jmixUser.setUsername(oidcUser.getName());
jmixUser.setFirstName(oidcUser.getGivenName());
jmixUser.setLastName(oidcUser.getFamilyName());
jmixUser.setEmail(oidcUser.getEmail());
jmixUser.setDelegate(oidcUser);
}
}
Защита API
Приложение Jmix может работать как сервер ресурсов. Чтобы указать, какой авторизационный сервер использовать, определите следующее свойство приложения:
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/<realm>
Значение свойства - это URL, содержащийся в клэйме iss для JWT-токенов, которые авторизационный сервер будет выдавать. См. документацию Spring Security для получения дополнительной информации.
По умолчанию значение клэйма sub используется в качестве имени пользователя Jmix, который устанавливается в контекст безопасности. Если вы хотите изменить это, используйте следующее свойство приложения:
jmix.oidc.jwt-authentication-converter.username-claim=preferred_username
В большинстве случаев значение свойства должно согласовываться со значением свойства spring.security.oauth2.client.provider.keycloak.user-name-attribute.
Токены доступа, полученные от поставщика OpenID, могут использоваться для доступа к защищенным конечным точкам, предоставляемым дополнением REST API.
Для локального экземпляра Keycloak токены доступа можно получить следующим образом:
curl -X POST http://localhost:8180/realms/sample1/protocol/openid-connect/token \
--user <client-id>:<client-secret> \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password&scope=openid&username=<username>&password=<password>"
Например:
curl -X POST http://localhost:8180/realms/sample1/protocol/openid-connect/token \
--user jmix-app:UONXQZf6unxVuWsxXvhMAPv5IxFz5P7D \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password&&scope=openid&username=johndoe&password=mypass"
Теперь рассмотрим, как защитить собственные контроллеры MVC, например следующий:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
@GetMapping("/authenticated/hello")
public String authenticatedHello() {
return "authenticated-hello";
}
@GetMapping("/anonymous/hi")
public String anonymousHello() {
return "anonymous-hi";
}
}
Представьте, что вам нужно защитить все URL, начинающиеся с /authenticated/, и сделать все URL, начинающиеся с /anonymous/, доступными для анонимного доступа. Это можно сделать несколькими способами. Простейший подход - использовать свойства приложения:
# All endpoints that match the given pattern will require a bearer token
jmix.resource-server.authenticated-url-patterns = /authenticated/**
# However, endpoints that match the following pattern will be accessible without a token
jmix.resource-server.anonymous-url-patterns = /anonymous/**
Свойства OIDC
jmix.oidc.filter-chain.force-api-scope-enabled
Переключатель для совместимости с версиями до 2.8. Установите данное свойство в false, чтобы игнорировать области действия ролей при аутентификации пользователей в REST API.
Значение по умолчанию: true
jmix.oidc.filter-chain.force-ui-scope-enabled
Переключатель для совместимости с версиями до 2.8. Установите данное свойство в false, чтобы игнорировать области действия ролей при аутентификации пользователей в UI.
Значение по умолчанию: true
jmix.oidc.use-default-configuration
Определяет, применять ли конфигурацию по умолчанию. По умолчанию установлено значение true. Установите это свойство в false, если вы хотите иметь доступ к бинам и интерфейсам дополнения, но не хотите использовать предварительно определенную конфигурацию Spring Security для защиты конечных точек. В этом случае вам придется написать свою собственную конфигурацию безопасности.
jmix.oidc.use-default-configuration = false
Настройка локального экземпляра Keycloak
Один из самых популярных поставщиков OpenID - Keycloak. Для ознакомления с дополнением Jmix OIDC вы можете запустить Keycloak локально с помощью Docker.
Запуск Keycloak с использованием Docker
Используйте следующую команду для запуска экземпляра Keycloak с помощью Docker на порту 8180:
docker run -p 8180:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin --name keycloak quay.io/keycloak/keycloak:22.0 start-dev
См. документацию Keycloak для получения дополнительной информации.
URL Keycloak: http://localhost:8180
Административные учетные данные:
Имя пользователя: admin
Пароль: admin
Вы можете прочитать о настройке экземпляра Keycloak в Руководстве администратора сервера.
Создание Realm
Войдите в консоль администратора Keycloak.
Откройте выпадающее окно вверху левой панели.
Нажмите Create Realm.
Дайте имя новому Realm, например, "sample".
Создание клиента
Чтобы подключить приложение Jmix к Keycloak, вам нужно создать нового клиента jmix-app с типом клиента OpenID Connect.
Включите Client authentication.
Введите Valid redirect URIs:
http://localhost:8080/*
и Web origins:
http://localhost:8080
Откройте только что созданный клиент и перейдите на вкладку Credentials. Там отображается Client secret, который вам понадобится в проекте Jmix для настройки подключения.
Параметры клиента должны использоваться в файле application.properties. См. раздел Настройка клиента.
Создание роли
Затем вам следует создать новую роль в Realm. По умолчанию имя роли должно совпадать с кодом роли Jmix. Создайте роль system-full-access:
Создание пользователя
Создайте пользователя с именем пользователя johndoe:
После сохранения пользователя появится вкладка Credentials. Там вы можете установить пароль пользователя.
На вкладке Role mappings назначьте роль system-full-access:
Если вы хотите заполнить атрибуты пользователя (например, "должность"), то можете сделать это на вкладке Attributes в редакторе пользователя.
Создание маппера ролей
Чтобы вернуть информацию о ролях Realm в токене ID, вам нужно определить маппер для клиента jmix-app. Откройте редактор клиента и перейдите на вкладку Client scopes:
Откройте редактор области jmix-app-dedicated. Добавьте предопределенный маппер для "realm roles":
Откройте только что созданный маппер "realm roles" Измените значение атрибута Token Claim Name на roles и выберите флажок Add to userinfo. Таким образом вы указываете, что клэйм, содержащий список ролей пользователя, будет называться roles.