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.
Установка
Для автоматической установки через 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"));
}
@Override
protected void populateUserAuthorities(OidcUser oidcUser, MyUser jmixUser) {
Collection<? extends GrantedAuthority> authorities = claimsRolesMapper.toGrantedAuthorities(oidcUser.getClaims());
jmixUser.setAuthorities(authorities);
}
}
Обратите внимание, что в приведенном выше примере сопоставление клэймов из пользователя OIDC в Jmix granted authorities делегируется интерфейсу ClaimsRolesMapper
. Реализация ClaimsRolesMapper
по умолчанию - это DefaultClaimsRolesMapper
, который получает клэйм с именем "roles" из токена ID. Этот клэйм должен содержать коллекцию имен ролей. Затем для каждой роли из значения клэйма будут искаться ресурсная роль и роль уровня строк в Jmix. Если они найдены, соответствующие granted authorities будут добавлены пользователю. Имя клэйма ролей можно настроить, используя следующее свойство приложения:
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 {
//...
}
Чтобы сохранять пользователей в базе данных после их входа с использованием поставщика 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());
}
}
Защита 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.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
.