Введение
Данная стать является инструкцией для новичков, которые хотели бы использовать Keycloak в своих проектах в качестве безопасности. В статье будет рассказано:
-
Что такое Keycloak и для чего он нужен.
-
Как запустить Keycloak.
-
Как создать свой первый realm.
-
Как настроить Keycloak.
-
Как интегрировать Keycloak в свое приложение на Spring.
В данной статье не будет подробного изучения данного сервера. Я разберу основные моменты, которые нужно знать, чтобы начать использование.
Keycloak
Для начала разберемся для чего нужен keycloak.
Keycloak — это система управления доступом с открытым исходным кодом, которая позволяет добавлять аутентификацию и авторизацию в приложения и сервисы. Она предоставляет функции единого входа (SSO), а также поддерживает различные протоколы аутентификации, такие как OpenID Connect, OAuth 2.0 и SAML.
Основные возможности Keycloak:
-
Единый вход (SSO): позволяет пользователям входить в систему один раз и получать доступ ко всем связанным приложениям без повторной аутентификации.
-
Социальная аутентификация: поддержка интеграции с социальными сетями, такими как Google, Facebook, Twitter, для аутентификации пользователей.
-
Централизованное управление пользователями: администраторы могут управлять пользователями, ролями и разрешениями из единого интерфейса.
-
Поддержка различных протоколов: как упоминалось ранее, Keycloak поддерживает множество стандартных протоколов аутентификации и авторизации.
-
Настраиваемая и расширяемая: возможности платформы можно расширить за счет использования различных плагинов и адаптеров.
-
Интеграция с LDAP и Active Directory: возможность интеграции с существующими системами управления пользователями.
Keycloak часто используется в больших организациях и проектах, требующих надежной и масштабируемой системы управления доступом.
Запуск Keycloak
Для начала создадим наш проект Spring, для этого можно использовать открытый для всех spring initializer - https://start.spring.io/ или же, если есть IntelliJ IDEA Ultimate, то можно создать проект Spring прям в ней.
Добавим нужные зависимости и создадим проект:
После того, как наш проект сформируется, у нас появится файл pom.xml, в котором будут прописаны все наши зависимости:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>OauthTest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>OauthTest</name>
<description>OauthTest</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Для дальнейшего использования Keycloak нам нужно прописать еще одну зависимость. Для того чтобы найти нужную зависимость и актуальную версию, можно использовать сайт - https://mvnrepository.com/
<!-- https://mvnrepository.com/artifact/org.keycloak/keycloak-admin-client -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
<version>26.0.2</version>
</dependency>
Чтобы запустить keycloak, я буду использовать Docker. Для этого создадим в корневой папке нашего проекта файл docker-compose.yml и пропишем в нем загрузку образов и создание контейнеров. Также я буду использовать вместо базы данных H2, которая установлена по-умолчанию в keycloak, базу данных posgreSQL, которую также запустил в Docker и подключил к keycloak.
docker-compose.yml
services:
postgres:
container_name: Postgres
image: postgres
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: root
POSTGRES_DB: keycloak
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- 5435:5432
networks:
- keycloak-network
restart: unless-stopped
keycloak:
container_name: Keycloak
image: quay.io/keycloak/keycloak:latest
ports:
- 9090:8080
environment:
DB_VENDOR: POSTGRES
DB_ADDR: postgres
DB_DATABASE: keycloak
DB_SCHEMA: public
DB_USER: root
DB_PASSWORD: root
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
networks:
- keycloak-network
depends_on:
- postgres
command:
- "start-dev"
networks:
keycloak-network:
driver: bridge
volumes:
postgres_data:
driver: local
keycloak:
driver: local
Наш keycloak будет работать на порту 9090. После запуска контейнера мы можем перейти на наш сервер по ссылке http://localhost:9090. Для того, чтобы войти в настройки keycloak, нам нужно вести данные, которые мы указали при создании контейнера.
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
Keycloak запущен, можно приступать к настройкам.
Настройка Keycloak
После входа, нам нужно создать наш realm. Realm (область) — это пространство, которое управляет набором пользователей, учетных данных и ролей. Реалмы позволяют изолировать данные и настройки, чтобы различные приложения и пользователи могли существовать в одном экземпляре Keycloak без пересечения данных.
Создание нового realm
После создания нашего realm нам нужно создать нового client. Для этого переходим в раздел Clients и нажимаем кнопку Create client.
Создание своего Client
-
Даём имя нашему client.
-
Устанавливаем в Client authentication флаг на On.
-
Прописываем наши URL.
Данный URL будет использовать наш бэк. В дальнейшем мы это все пропишем.
После того как мы создали нашего client, создадим нового пользователя (User). Для этого перейдем во вкладку Users. Здесь нам нужно создать пользователя, указав логин и пароль.
Создание нового пользователя
Пропишем данные в полях.
После создания, данный пользователь не будет иметь пароля, для это установим его во вкладке Credentials.
Теперь у нас есть пользователь в базе данных. Давайте проверим можно ли войти под этими данными. Для этого создадим файл scratch.http и пропишем следующее:
POST http://localhost:9090/realms/OauthTests/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
client_id=myclient&client_secret=SECRET&username=kudzip&password=test&grant_type=password
client_id - это название, созданного нами client.
client_secret - это секретный ключ нашего client, его можно скопировать следуя инструкциям в пункте Копирование client secret. Данный ключ нужно вставить вместо слова SECRET.
Копирование client secret
username и password - это username и password, созданного нами пользователя.
После отправления запроса нам в ответ генерируется access_token и refresh_token:
Данный запрос можно выполнить в postman.
Как видно из изображения, все работает исправно. Проверим данный токен на сайте - JWT.IO.
Данный токен несет в себе основную информацию о пользователе. В поле "полезная нагрузка" можно увидеть все нужные данные. (email, role, name, family, username и тд.)
Во вкладке Clients можно посмотреть какую информацию у нас будет хранить JWT. Давайте на это посмотрим. Для этого нужно перейти во вкладку Clients и нажать на созданный нами myclient.
Просмотр токена
В поле Users нужно выбрать интересующего нас пользователя.
{
"exp": 1733502948,
"iat": 1733502648,
"jti": "85cd30db-8b48-49a0-bf91-45a8e649e20a",
"iss": "http://localhost:9090/realms/OauthTests",
"aud": "account",
"sub": "4b65107a-99f2-49dc-a161-e12bf2fdc1f3",
"typ": "Bearer",
"azp": "myclient",
"sid": "70a67a84-f849-4b43-a3f8-b5d42b1f4daa",
"acr": "1",
"allowed-origins": [
"http://localhost:8083"
],
"realm_access": {
"roles": [
"default-roles-oauthtests",
"offline_access",
"uma_authorization"
]
},
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "openid profile email",
"email_verified": true,
"name": "ivan Storozhev",
"spring_sec_roles": [
"default-roles-oauthtests",
"offline_access",
"uma_authorization"
],
"preferred_username": "kudzip",
"given_name": "ivan",
"family_name": "Storozhev",
"email": "test@test.ru"
}
Такую же информацию мы получили, когда расшифровали наш JWT на сайте.
Для чего нужны Access и Refresh токены?
Они служат для управления доступом к ресурсам и поддержания сеансов пользователей.
Access-токен
-
Предоставление доступа: Access-токен предоставляет клиентскому приложению права доступа к защищённым ресурсам от имени пользователя или сервиса. Он содержит информацию о пользователе и правах доступа.
-
Краткосрочный срок действия: Обычно имеют короткий срок действия (несколько минут или часов) для снижения риска компрометации.
-
Использование в API-запросах: Access-токены передаются в заголовках запросов к API, чтобы сервер мог подтвердить, что запрос авторизован.
-
JSON Web Tokens (JWT): Часто реализуются в формате JWT, что позволяет серверам быстро проверять подпись и данные токена без дополнительного обращения к серверу авторизации.
Refresh-токен
-
Обновление access-токена: Refresh-токен используется для получения нового access-токена, когда старый истекает, без необходимости повторной аутентификации пользователя.
-
Долгосрочный срок действия: Имеют более длительный срок действия по сравнению с access-токенами (дни или недели), что позволяет поддерживать долгосрочные сеансы.
-
Хранение и безопасность: Должны храниться надежно, так как компрометация refresh-токена может позволить злоумышленнику получать новые access-токены без ведома пользователя.
-
Не передаются с каждым запросом: В отличие от access-токенов, refresh-токены не используются в обычных API-запросах, а только для обновления access-токенов.
Вместе эти токены обеспечивают баланс между безопасностью и удобством, позволяя пользователям оставаться авторизованными в течение длительного времени, но минимизируя риски, связанные с компрометацией токенов.
Иногда нужно добавить определенную роль, чтобы доступ к странице был только для ограниченных пользователей. Такое можно сделать во вкладке Realm roles. Я добавлю роль менеджера "ROLE_MANAGER".
Создание новой роли
Теперь у нас есть отдельная роль для менеджеров. В конфиге spring security мы пропишем безопасность с учетом ролей.
Подключение авторизации и регистрации через сторонние API. Технология Oauth достаточно удобна и популярна, ее используют сейчас практически везде и с помощью Keycloak, можно подключить авторизацию и регистрацию через Google, GitHub и др. сервисы. Я воспользуюсь API только Google и GitHub. Давайте посмотрим как это сделать.
Чтобы подключить Google нужно перейти на сайт с API. Здесь нам нужно создать новый Oauth client ID.
Откроется окно по созданию client. Первым делом нам нужно выбрать тип приложения (я выбрал Web application). Далее нужно дать имя нашему client. После нужно указать Redirect URI. Как получить Redirect URI описано в пункте Получение Redirect URI.
Получение Redirect URI
После нажатия на "Add provider" будет выбор из нескольких провайдеров, нам нужно выбрать Google.
Когда мы создали наш Oauth client ID, нужно зайти в него, чтобы узнать Client ID и Client secret.
Нам нужно скопировать эти 2 поля и перейти к Keycloak. Здесь нам нужно во вкладке, где мы копировали Redirect URI вставить недостающие поля.
Client ID - скопированный Client ID с API Google.
Client Secret - скопированный Client Secret с API Google.
GitHub
Чтобы подключить GitHub нужно перейти на сайт Developer settings. Здесь нам нужно создать new Oauth app.
Далее заполняем поля.
Получение Redirect URI
После нажатия на "Add provider" будет выбор из нескольких провайдеров, нам нужно выбрать GitHub.
Заходим в созданный нами Oauth app, чтобы узнать Client ID и Client secret.
Нам нужно скопировать эти 2 поля и перейти к Keycloak. Здесь нам нужно во вкладке, где мы копировали Redirect URI вставить недостающие поля.
Client ID - скопированный Client ID с GitHub.
Client Secret - скопированный Client Secret с GitHub.
Настройка Keycloak в Spring
Этот модуль посвящен интеграции Keycloak в Spring приложение.
Для начала нужно прописать свойства в application.yml. Здесь будет указываться, что мы используем в качестве ресурс сервера и клиента наш Keycloak.
application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:9090/realms/OauthTests
client:
provider:
keycloak:
issuer-uri: http://localhost:9090/realms/OauthTests
user-name-attribute: preferred_username
registration:
keycloak:
client-id: myclient
client-secret: YOUR SECRET
scope: openid
server:
port: 8083
logging:
level:
org.springframework.security: TRACE
client-id - это client id, созданного нами client в Keycloak.
client-secret - это секретный ключ, созданного нами client в Keycloak. (Мы его использовали выше, для теста пользователя)
Далее нам нужно создать класс SecurityConfig, чтобы настроить доступ к URI и реализовать oauth2 Resource Server. Это прописывается в методе securityFilterChain.
SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/error").permitAll()
.requestMatchers("/manager.html").hasRole("MANAGER")
.anyRequest().authenticated()
)
.oauth2ResourceServer((oauth2) -> oauth2
.jwt(Customizer.withDefaults())
)
.oauth2Login(Customizer.withDefaults());
return http.build();
}
}
@Configuration: Указывает, что класс содержит определения бинов и может быть использован контейнером Spring для генерации бинов.
@EnableWebSecurity: Включает поддержку безопасности веб-приложений в Spring Security.
authorizeHttpRequests: Определяет правила авторизации для HTTP-запросов.
.requestMatchers("/error").permitAll(): Позволяет доступ ко всем запросам на /error
без аутентификации.
.requestMatchers("/manager.html").hasRole("MANAGER"): Разрешает доступ к /manager.html только пользователям с ролью MANAGER.
.anyRequest().authenticated(): Все остальные запросы требуют аутентификации.
oauth2ResourceServer: Настраивает приложение как OAuth2 ресурсный сервер, используя JWT для проверки токенов.
.jwt(Customizer.withDefaults()): Указывает на использование JWT с настройками по умолчанию.
oauth2Login: Включает OAuth2 логин с настройками по умолчанию, позволяя пользователям входить в систему с помощью провайдера OAuth2.
Далее создадим метод jwtAuthenticationConverter. Он конвертирует JWT в объект аутентификации, позволяя извлекать и преобразовывать роли из токена.
jwtAuthenticationConverter()
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtAuthenticationConverter.setPrincipalClaimName("preferred_username");
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> {
var authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
var roles =jwt.getClaimAsStringList("spring_sec_roles");
return Stream.concat(authorities.stream(),
roles.stream()
.filter(role -> role.startsWith("ROLE_"))
.map(SimpleGrantedAuthority::new)
.map(GrantedAuthority.class::cast))
.toList();
});
return jwtAuthenticationConverter;
}
.setPrincipalClaimName("preferred_username"): Указывает, что preferred_username
будет использоваться как имя пользователя.
jwtGrantedAuthoritiesConverter: Конвертирует роли из JWT в объекты GrantedAuthority
.
roles: Извлекает роли из токена, находящегося в поле spring_sec_roles
.
Stream.concat: Объединяет роли из токена и стандартные авторитеты, преобразуя их в список авторитетов.
oAuth2UserService()
@Bean
public OAuth2UserService<OidcUserRequest, OidcUser> oAuth2UserService() {
var oidcUserService = new OidcUserService();
return userRequest -> {
var oidcUser = oidcUserService.loadUser(userRequest);
var roles = oidcUser.getClaimAsStringList("spring_sec_roles");
var authorities = Stream.concat(oidcUser.getAuthorities().stream(),
roles.stream()
.filter(role -> role.startsWith("ROLE_"))
.map(SimpleGrantedAuthority::new)
.map(GrantedAuthority.class::cast))
.toList();
return new DefaultOidcUser(authorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
};
}
OidcUserService: Сервис для обработки аутентификации OpenID Connect.
roles: Извлекает роли из аутентификационного ответа OpenID Connect.
Stream.concat: Объединяет роли из OpenID Connect и стандартные авторитеты, создавая новый объект DefaultOidcUser
с обновленным списком авторитетов.
Целый код класса SecurityConfiig.
SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/error").permitAll()
.requestMatchers("/manager.html").hasRole("MANAGER")
.anyRequest().authenticated()
)
.oauth2ResourceServer((oauth2) -> oauth2
.jwt(Customizer.withDefaults())
)
.oauth2Login(Customizer.withDefaults());
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtAuthenticationConverter.setPrincipalClaimName("preferred_username");
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> {
var authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
var roles =jwt.getClaimAsStringList("spring_sec_roles");
return Stream.concat(authorities.stream(),
roles.stream()
.filter(role -> role.startsWith("ROLE_"))
.map(SimpleGrantedAuthority::new)
.map(GrantedAuthority.class::cast))
.toList();
});
return jwtAuthenticationConverter;
}
@Bean
public OAuth2UserService<OidcUserRequest, OidcUser> oAuth2UserService() {
var oidcUserService = new OidcUserService();
return userRequest -> {
var oidcUser = oidcUserService.loadUser(userRequest);
var roles = oidcUser.getClaimAsStringList("spring_sec_roles");
var authorities = Stream.concat(oidcUser.getAuthorities().stream(),
roles.stream()
.filter(role -> role.startsWith("ROLE_"))
.map(SimpleGrantedAuthority::new)
.map(GrantedAuthority.class::cast))
.toList();
return new DefaultOidcUser(authorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
};
}
}
На этом самая базовая настройка Keycloak закончена. Теперь мы можем перейти к тестам.
Тестирование
Для проверки создадим две html страницы. Первая страница будет доступна для всех авторизированных пользователей authenticated.html, а вторая будет видна только менеджерам manager.html. Это сделано для того, чтобы протестировать, работает ли разделение по ролям.
authenticated.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<h2>Привет, аутентифицированный пользователь</h2>
</body>
</html>
manager.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<h2>Hi manager</h2>
</body>
</html>
Далее запустим наш проект и перейдем на наш хост - http://localhost:8083.
Как видно, нас сразу же перебрасывает на форму авторизации Keycloak. В этой форме есть вход через username/email, а также через GitHub и Google. Попробуем войти под нашим пользователем.
Мы смогли зайти под нашим пользователем. Так как мы создали authenticated страничку, то давайте попробуем туда зайти, указав такой URI - http://localhost:8083/authenticated.html.
Попробуем зайти на страничку manager.
Как видно, мы не можем туда зайти из-за ограничений в доступе, так как у нашего пользователя нет роли менеджер.
Давайте присвоим ему эту роль. Для этого в Keycloak зайдем во вкладку Users и выберем нашего пользователя.
Далее выбираем фильтр "by realm roles" и тут выбираем нашу роль, которую мы создали ранее. Все, новая роль присвоена пользователю. Перезапустим приложение и попробуем войти заново на страничку менеджера.
Как видно, все работает!
Теперь проверим вход и регистрацию через Google и GitHub.
Так как мы не сделали страничку выхода, то сессию нужно завершить в самом Keycloak и перезапустить наше приложение. В Keycloak переходим во вкладку Users, выбираем нашего пользователя, переходим в сессии и завершаем их все.
Далее вновь заходим на наш хост и в форме авторизации выбираем Google.
Далее выбираем наш аккаунт.
Как видно, все работает! Если зайти в Keycloak во вкладку Users, то мы увидим, что у нас добавился новый пользователь.
GitHub
Далее вновь заходим на наш хост и в форме авторизации выбираем GitHub.
Далее вводим наши данные от аккаунта.
Как видно, все работает! Если зайти в Keycloak во вкладку Users, то мы увидим, что у нас добавился новый пользователь.
Из тестирования видно, что все работает. Авторизация проходит по логину и паролю, а также через Google и GitHub.
Вывод
В данной статье я описал, как начать свою работу с Keycloak и интегрировать его в свой проект Spring. Я разобрал главные моменты:
-
Создание своего realm.
-
Создание нового client.
-
Создание своей роли.
-
Подключение сторонних API.
Данная инструкция поможет освоить Keycloak.
Автор: ivan_storozhev