Вы больше не можете создать сервер авторизации с помощью @EnableAuthorizationServer
, потому что Spring Security OAuth задеприкейтили, а проект Spring Authorization Server всё ещё экспериментальный? Выход есть! Напишем авторизацию своими руками... Что?.. Нет?.. Не хочется? И вообще получаются какие-то костыли и велосипеды? Ну ладно, тогда давайте возьмём уже что-то готовое. Например, Keycloak.
Что, зачем и почему?
Как-то сидя на карантине захотелось мне написать pet-проект, да не простой, а с использованием микросервисной архитектуры (ну или около того). На начальном этапе одного сервиса для фронта и одного для бэка, в принципе, будет достаточно. Если вдруг в будущем понадобятся ещё сервисы, то будем добавлять их по мере необходимости. Для бэка будем использовать Spring Boot. Для фронта - Vue.js, а точнее Vuetify, чтобы не писать свои компоненты, а использовать уже готовые.
Начнём с авторизации. В качестве протокола авторизации будем использовать OAuth2, т.к. стильно, модно, молодёжно, да и использовать токены для получения доступа к сервисам, одно удовольствие, особенно в микросервисной архитектуре.
Для авторизации пусть будет отдельный сервис. И раз уж мы решили использовать Spring Boot, то сможет ли он нам чем-то помочь в создании этого сервиса? Например, каким-нибудь готовым решением, таким как Authorization Server? Правильно, не сможет. Проект Spring Security OAuth в котором находился Authorization Server задеприкейтили, а сам проект Authorization Server стал эксперементальным и на данный момент находится в активной разработке. Что делать? Как быть? Можно написать свой сервис авторизации. Если подсматривать в исходники задеприкейченого Authorization Server, то, вероятно, задача будет не такой уж и страшной. Правда, при этом возможны ситуации когда реализацию каких-то интересных фич будет негде подсмотреть и решать вопросы о том "быть или не быть", "правильно ли так делать или чё-то фигня какая-то" придётся исходя из собственного опыта, что может привести к получению на выходе большого количества неприглядных костылей.
Что делать, если городить собственные велосипеды не хочется или хочется, но на данный момент это кажется весьма долгим процессом, а результат нужен уже вчера? Есть ли какие-то готовые решения, способные решить данную проблему? Да, есть. Давайте одно из таких решений и рассмотрим.
Keycloak
Keycloak представляет из себя сервис, который предназначен для идентификации и контроля доступа. Что он умеет:
SSO (Single-Sign On) - это когда вы логинитесь в одном едином месте входа, получаете идентификатор (например, токен), с которым можете получить доступ к различным вашим сервисам
Login Flows - различные процессы по регистрации, сбросу пароля, проверки почты и тому подобное, а так же соответствующие страницы для этих процессов
Темы - можно кастомизировать страницы для Login Flows
Social Login - можно логиниться через различные социальные сети
И всё это он умеет практически из коробки, достаточно просто настроить требуемое поведение из админки (Admin Console), которая у Keycloak тоже имеется. А если вам всего этого вдруг окажется мало, то Keycloak является open source продуктом, который распространяется по лицензии Apache License 2.0. Так что можно взять исходники Keycloak и дописать требуемый функционал, если он вам, конечно, настолько сильно нужен.
А ещё у Keycloak имеются достаточно удобные интеграции со Spring Boot и Vue.js, что значительно упрощает разработку связанную с взаимодействием с ним.
Getting Started with Keycloak
Запускать локально сторонние сервисы, требуемые для разработки своих собственных, лично я предпочитаю с помощью Docker Compose, т.к. наглядно и достаточно удобно в yml-файле описывать как и с какими параметрами требуется осуществлять запуск. А посему, Keycloak локально будем запускать с помощью Docker Compose.
В качестве докер-образа возьмём jboss/keycloak. Чтобы иметь возможность обращаться к Keycloak прокинем из контейнера порт 8080. Так же, чтобы иметь возможность заходить в админку Keycloak, требуется установить логин и пароль от админской учётки. Сделать это можно установив переменные окружения KEYCLOAK_USER
для логина и KEYCLOAK_PASSWORD
для пароля. Итоговый файл приведен ниже.
# For development
version: "3.8"
services:
keycloak:
image: jboss/keycloak:12.0.2
environment:
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: admin
ports:
- 8080:8080
Создание своих realm и client
Для того чтобы иметь возможность из своего клиентского приложения обращаться к Keycloak, например, для аутентификации или авторизации, нужно в Keycloak создать клиента (client), который будет соответствовать этому приложению. Клиента в Keycloak можно создать в определённом realm. Realm - это независимая область в которую входят пользователи, клиенты, группы, роли и много чего ещё.
По умолчанию уже создан один realm и называется он master
. В нём будет находится админская учётка логин и пароль от которой мы задали при запуске Keycloak с помощью Docker Compose. Данный realm предназначен для администрирования Keycloak и он не должен использоваться для ваших собственных приложений. Для своих приложений нужно создать свой realm.
Для начала нам нужно залогиниться в админке Keycloak, запустить который можно с помощью файла Docker Compose, описанного ранее. Для этого можно перейти по адресу http://localhost:8080/auth/ и выбрать Administration Console
.
После этого мы попадаем на страницу авторизации админки Keycloak. Здесь можно ввести логин и пароль от админской учётки для входа в Keycloak.
После входа откроется страница настроек realm master
.
Давайте создадим свой realm. Для этого необходимо навести курсор на область с названием realm, чтобы появилась кнопка Add realm
.
На странице создания realm достаточно заполнить только поле Name
.
После нажатия на кнопку Create
мы попадём на страницу редактирования этого realm. Но пока дополнительно в нашем realm ничего менять не будем.
Теперь перейдём в раздел Clients
. Как можно заметить, по умолчанию уже создано несколько технических клиентов, предназначенных для возможности администрирования через Keycloak, например, для того чтобы пользователи могли менять свои данные или чтобы можно было настраивать realm'ы с помощью REST API и много для чего ещё. Подробнее про этих клиентов можно почитать тут.
Давайте создадим своего клиента. Для этого в разделе Clients
необходимо нажать на кнопку Create
.
На странице создания клиента необходимо заполнить поля:
Client ID
- идентификатор клиента, будет использоваться в различных запросах к Keycloak для идентификации приложения.Root URL
- адрес клиентского приложения.
После нажатия на кнопку Save
мы попадём на страницу редактирования этого клиента. Настройки клиента менять не будем, оставим их такими, какими они были выставлены по умолчанию.
Интеграция со Spring Boot
В первую очередь давайте создадим проект на Spring Boot. Сделать это можно, например, с помощью Spring Initializr. В качестве системы автоматической сборки проекта будем использовать Gradle. В качестве языка пусть будет Java 15. Никаких дополнительных зависимостей в соответствующем блоке Dependencies
добавлять не требуется.
Для того чтобы в Spring Boot проекте появилась поддержка Keycloak, необходимо добавить в него Spring Boot Adapter и добавить в конфиг приложения конфигурацию для Keycloak.
Для того чтобы добавить Spring Boot Adapter, необходимо в проект подключить зависимость org.keycloak:keycloak-spring-boot-starter
и сам adapter org.keycloak.bom:keycloak-adapter-bom
. Сделать это можно изменив файл build.gradle
следующим образом:
...
dependencyManagement {
imports {
mavenBom 'org.keycloak.bom:keycloak-adapter-bom:12.0.3'
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.keycloak:keycloak-spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
...
Проблемы в Java 14+
Если запустить Spring Boot приложение на Java 14 или выше, то при обращении к вашим методам API, закрытым ролями кейклока, будут возникать ошибки видаjava.lang.NoClassDefFoundError: java/security/acl/Group
. Связано это с тем, что в Java 9 этот, а так же другие классы из этого пакета были задеприкейчины и удалены в Java 14. Исправить данную проблему, вроде как, собираются в 13-й версии Keycloak. Чтобы решить её сейчас, можно использовать Java 13 или ниже, либо, вместо сервера приложений Tomcat, который используется в Spring Boot по умолчанию, использовать, например, Undertow. Для того чтобы подключить в Spring Boot приложение Undertow, нужно добавить в build.gradle
зависимость org.springframework.boot:spring-boot-starter-undertow
и исключить зависимоситьspring-boot-starter-tomcat
.
...
dependencies {
implementation('org.springframework.boot:spring-boot-starter-web') {
exclude module: 'spring-boot-starter-tomcat'
}
implementation ('org.keycloak:keycloak-spring-boot-starter') {
exclude module: 'spring-boot-starter-tomcat'
}
implementation 'org.springframework.boot:spring-boot-starter-undertow'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
...
Теперь перейдём к конфигурации приложения. Вместо properties
файла конфигурации давайте будем использовать более удобный (на мой взгляд, конечно же) yml
. А так же, чтобы подчеркнуть, что данный конфиг предназначен для разработки, профиль dev
. Т.е. полное название файла конфигурации будет application-dev.yml
.
server:
port: 8082
keycloak:
auth-server-url: http://localhost:8080/auth
realm: "list-keep"
resource: "list-keep"
bearer-only: true
security-constraints:
- authRoles:
- uma_authorization
securityCollections:
- patterns:
- /api/*
Давайте подробнее разберём данный конфиг:
server
port
- порт на котором будет запущенно приложение
keycloak
auth-server-url
- адрес на котором запущен Keycloakrealm
- название нашего realm в Keycloakresource
- Client ID нашего клиентаbearer-only
- если выставлено true, то приложение может только проверять токены, и в приложении нельзя будет залогиниться, например, с помощью логина и пароля из браузераsecurity-constraints
- для описания ролевой политикиauthRoles
- список ролей KeycloaksecurityCollections
patterns
- URL-паттерны для методов REST API, которые требуется закрыть соответствующими ролями
В данном конкретном случае мы закрываем ролью
uma_authorization
все методы, в начале которых присутствует путь/api/
. Звёздочка в конце паттерна означает любое количество любых символов. Рольuma_authorization
добавляется автоматически ко всем созданным пользователям, т.е. по сути данная ролевая политика означает что все методы/api/*
доступны только авторизованным пользователям.
В общем-то, это все настройки которые нужно выполнить в Spring Boot приложении для интеграции с Keycloak. Давайте теперь добавим какой-нибудь тестовый контроллер.
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/current")
public User getCurrentUser(
KeycloakPrincipal<KeycloakSecurityContext> principal
) {
return new User(principal.getKeycloakSecurityContext()
.getToken().getPreferredUsername()
);
}
}
User.java
public class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
В данном контроллере есть лишь один метод /api/user/current
, который возвращает информацию по текущему юзеру, а именно Preferred Username
из токена. По умолчанию в Preferred Username
находится username
пользователя Keycloak.
Исходники проекта можно посмотреть тут.
Интеграция с Vue.js
Начнём с создания проекта. Создать проект можно, например, с помощью Vue CLI.
vue create list-keep-front
После ввода данной команды необходимо выбрать версию Vue. Т.к. в проекте будет использоваться библиотека Vuetify, которая на данный момент не поддерживает Vue 3, нужно выбрать Vue 2.
После этого нужно перейти в проект и добавить Vuetify.
vue add vuetify
После добавления Vuetify вместе с самой библиотекой в проект будут добавлены каталоги components
и assets
. В components
будет компонент HelloWorld
, с примером страницы на Vuetify, а в assets
ресурсы, использующиеся в компоненте HelloWorld
. Эти каталоги нам не пригодятся, поэтому можно их удалить.
Для удобства разработки сконфигурируем devServer следующим образом: запускать приложение будем на порту 8081, все запросы, которые начинаются с /api/
будем проксировать на адрес, на котором запущенно приложение на Spring Boot.
module.exports = {
devServer: {
port: 8081,
proxy: {
'^/api/': {
target: 'http://localhost:8082'
}
}
}
}
Перейдём к добавлению в проект поддержки Keycloak. Для начала обратимся к официальной документации. Там нам рассказывают о том, что в проект нужно добавить Keycloak JS Adapter. Сделать это можно с помощью библиотеки keycloak-js. Добавим её в проект.
yarn add keycloak-js
Далее нам предлагают добавить в src/main.js
код, который добавит в наш проект поддержку Keycloak.
// Параметры для подключения к Keycloak
let initOptions = {
url: 'http://127.0.0.1:8080/auth', // Адрес Keycloak
realm: 'keycloak-demo', // Имя нашего realm в Keycloak
clientId: 'app-vue', // Идентификатор клиента в Keycloak
// Перенаправлять неавторизованных пользователей на страницу входа
onLoad: 'login-required'
}
// Создать Keycloak JS Adapter
let keycloak = Keycloak(initOptions);
// Инициализировать Keycloak JS Adapter
keycloak.init({ onLoad: initOptions.onLoad }).then((auth) => {
if (!auth) {
// Если пользователь не авторизован - перезагрузить страницу
window.location.reload();
} else {
Vue.$log.info("Authenticated");
// Если авторизован - инициализировать приложение Vue
new Vue({
el: '#app',
render: h => h(App, { props: { keycloak: keycloak } })
})
}
// Пытаемся обновить токен каждые 6 секунд
setInterval(() => {
// Обновляем токен, если срок его действия истекает в течении 70 секунд
keycloak.updateToken(70).then((refreshed) => {
if (refreshed) {
Vue.$log.info('Token refreshed' + refreshed);
} else {
Vue.$log.warn('Token not refreshed, valid for '
+ Math.round(keycloak.tokenParsed.exp
+ keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds');
}
}).catch(() => {
Vue.$log.error('Failed to refresh token');
});
}, 6000)
}).catch(() => {
Vue.$log.error("Authenticated Failed");
});
С инициализацией Keycloak JS Adapter, вроде бы, всё понятно. А вот использование setInterval
для обновления токенов мне показалось не очень практичным и красивым решением. Как минимум, кажется, что при бездействии пользователя на странице токены всё равно продолжат обновляться, хоть это и не требуется. На мой взгляд, обновление токенов лучше сделать так, как предлагает, например, автор данной статьи. Т.е. обновлять токены когда пользователь выполняет какое-либо действие в приложении. Автор указанной статьи выделяет три таких действия:
Взаимодействие с API (бэкендом)
Навигация (переход по страницам)
Переход на вкладку с нашим приложением, например, из другой вкладки
Приступим к реализации. Для того чтобы можно было обновлять токен из различных частей приложения, нам понадобится глобальный экземпляр Keycloak JS Adapter. Для этого во Vue.js существует функционал плагинов. Создадим свой плагин для Keycloak JS Adapter в файле /plugins/keycloak.js.
import Vue from 'vue'
import Keycloak from 'keycloak-js'
const initOptions = {
url: process.env.VUE_APP_KEYCLOAK_URL,
realm: 'list-keep',
clientId: 'list-keep'
}
const keycloak = Keycloak(initOptions)
const KeycloakPlugin = {
install: Vue => {
Vue.$keycloak = keycloak
}
}
Vue.use(KeycloakPlugin)
export default KeycloakPlugin
Значение адреса Keycloak, указанное в initOptions.url
, может отличаться в зависимости от того где запущенно приложение (локально, на тесте, на проде), поэтому, чтобы иметь возможность указывать значения в зависимости от среды, будем использовать переменные окружения. Для локального запуска можно создать файл .env.local в корне проекта со следующим содержимым.
VUE_APP_KEYCLOAK_URL = http://localhost:8080/auth
Теперь нам достаточно импортировать файл с созданным нами плагином в main.js
, и мы сможем из любого места приложения обратиться к нашему Keycloak JS Adapter с помощью Vue.$keycloak
. Давайте это и сделаем, а так же создадим экземпляр Vue нашего приложения. Для этого изменим файл main.js
следующим образом.
import Vue from 'vue'
import App from './App.vue'
import vuetify from './plugins/vuetify'
import router from '@/router'
import i18n from '@/plugins/i18n'
import '@/plugins/keycloak'
import { updateToken } from '@/plugins/keycloak-util'
Vue.config.productionTip = false
Vue.$keycloak.init({ onLoad: 'login-required' }).then((auth) => {
if (!auth) {
window.location.reload();
} else {
new Vue({
vuetify,
router,
i18n,
render: h => h(App)
}).$mount('#app')
window.onfocus = () => {
updateToken()
}
}
})
Помимо инициализации Keycloak JS Adapter, здесь добавлен вызов функции updateToken()
на событие window.onfocus
, которое будет возникать при переходе пользователя на вкладку с нашим приложением. Наша функция updateToken()
вызывает функцию updateToken()
из Keycloak JS Adapter и, соответственно, обновляет токен, если срок жизни токена в секундах на данный момент меньше, чем значение TOKEN_MIN_VALIDITY_SECONDS
, после чего возвращает актуальный токен.
import Vue from 'vue'
const TOKEN_MIN_VALIDITY_SECONDS = 70
export async function updateToken () {
await Vue.$keycloak.updateToken(TOKEN_MIN_VALIDITY_SECONDS)
return Vue.$keycloak.token
}
Теперь добавим обновление токена на оставшиеся действия пользователя, а именно на взаимодействие с API и на навигацию. С API мы будем взаимодействовать с помощью axios. Помимо обновления токена нам в каждом запросе необходимо добавлять http-хидер Authorization: Bearer
с нашим токеном для авторизации в нашем Spring Boot сервисе. Так же давайте будем перенаправлять на какую-нибудь страницу с ошибками, например, /error
, если API будет возвращать нам ошибки. Для того чтобы выполнять какие-либо действие на любые запросы/ответы в axios существуют интерцепторы, добавить которые можно в App.vue
.
<template>
<v-app>
<v-main>
<router-view></router-view>
</v-main>
</v-app>
</template>
<script>
import Vue from 'vue'
import axios from 'axios'
import { updateToken } from '@/plugins/keycloak-util'
const AUTHORIZATION_HEADER = 'Authorization'
export default Vue.extend({
name: 'App',
created: function () {
axios.interceptors.request.use(async config => {
// Обновляем токен
const token = await updateToken()
// Добавляем токен в каждый запрос
config.headers.common[AUTHORIZATION_HEADER] = `Bearer ${token}`
return config
})
axios.interceptors.response.use( (response) => {
return response
}, error => {
return new Promise((resolve, reject) => {
// Если от API получена ошибка - отправляем на страницу /error
this.$router.push('/error')
reject(error)
})
})
},
// Обновляем токен при навигации
watch: {
$route() {
updateToken()
}
}
})
</script>
Помимо интерцепторов мы здесь добавили наблюдателя (watch), который будет отслеживать переход пользователя по страницам приложения и обновлять при этом токен.
Интеграция с Keycloak закончена. Давайте теперь добавим тестовую страницу /pages/Home.vue
, на которой будем вызывать с помощью axios
тестовый метод /api/user/current
, который мы ранее добавили в Spring Boot приложение, и выводить имя полученного пользователя.
<template>
<div>
<p>{{ user.name }}</p>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'Home',
data() {
return {
user: {}
}
},
mounted() {
axios.get('/api/user/current')
.then(response => {
this.user = response.data
})
}
}
</script>
Для того чтобы можно было попасть на данную страницу в нашем приложении необходимо добавить её в router.js
. Данная страница будет доступна по пути /
.
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/pages/Home'
import Error from '@/pages/Error'
import NotFound from '@/pages/NotFound'
Vue.use(VueRouter)
let router = new VueRouter({
mode: 'history',
routes: [
{
path: '/',
component: Home
},
{
path: '/error',
component: Error
},
{
path: '*',
component: NotFound
}
]
})
export default router
По умолчанию роутер работает в так называемом режиме хэша и при навигации страницы в адресной строке отображаются с символом #
. Для более естественного отображения можно включить режим history.
И ещё немного о страницах
Помимо страницы /pages/Home.vue
в роутере присутствуют страницы /pages/Error.vue
и /pages/NotFound.vue
. НаError
, как уже упоминалось ранее, происходит переход из интерцептора при получении ошибок от API. На NotFound
- если будет переход на неизвестную страницу.
Для примера давайте рассмотрим содержимое страницы Error.vue
. Содержимое NotFound.vue
практически ничем не отличается.
<template>
<v-container
class="text-center"
fill-height
style="height: calc(100vh - 58px);"
>
<v-row align="center">
<v-col>
<h1 class="display-2 primary--text">
{{ $t('oops.error.has.occurred') }}
</h1>
<p>{{ $t('please.try.again.later') }}</p>
<v-btn
href="/"
color="primary"
outlined
>
{{ $t('go.to.main.page') }}
</v-btn>
</v-col>
</v-row>
</v-container>
</template>
<script>
export default {
name: 'Error'
}
</script>
В шаблоне данной страницы используется локализация. Работает она с помощью плагина vue-i18n. Для того чтобы прикрутить локализацию своих текстовок нужно добавить переводы в виде json файлов в проект. Например, для русской локализации можно создать файл ru.json
и положить его в каталог locales
. Теперь эти текстовки необходимо загрузить в VueI18n
. Сделать это можно, например, следующим образом. Давайте код по загрузке текстовок вынесем в/plugins/i18n.js
.
import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
function loadLocaleMessages () {
const locales = require.context('@/locales', true,
/[A-Za-z0-9-_,\s]+\.json$/i)
const messages = {}
locales.keys().forEach(key => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)
if (matched && matched.length > 1) {
const locale = matched[1]
messages[locale] = locales(key)
}
})
return messages
}
export default new VueI18n({
locale: 'ru',
fallbackLocale: 'ru',
messages: loadLocaleMessages()
})
После этого к этим текстовкам можно будет обращаться из шаблона страницы с помощью $t
.
Так же привожу содержимое /plugins/vuetify.js
. В нём добавлена возможность использовать иконки Font Awesome на страницах нашего приложения.
import Vue from 'vue'
import Vuetify from 'vuetify/lib/framework'
import 'vuetify/dist/vuetify.min.css'
import '@fortawesome/fontawesome-free/css/all.css'
Vue.use(Vuetify);
const opts = {
icons: {
iconfont: 'fa'
}
}
export default new Vuetify(opts)
Немного мыслей об обработке ошибок
Функции Keycloak JS Adapter init()
и updateToken()
возвращают объект KeycloakPromise
, у которого есть возможность вызывать catch()
и в нём обрабатывать ошибки. Но лично я не понял что именно в данном случае будет считаться ошибками и когда мы попадём в этот блок, т.к., например, если Keycloak не доступен, то в этот блок мы не попадаем. Поэтому в приведённом здесь приложении, я возможные ошибки от этих двух функций не обрабатываю. Возможно, если Keycloak не работает, то в продакшене стоит делать так, чтоб и наше приложение тоже становилось недоступным и не пытаться это как-то обработать. Ну или если всё-таки нужно такие ошибки понимать именно в Vue.js приложении, то, возможно, нужно как-то доработать keycloak-js
.
Исходники проекта можно посмотреть тут.
Login Flows
Теперь давайте перейдём непосредственно к настройке процессов авторизации и регистрации, а так же соответствующих страниц. Т.к. возможностей по конфигурации Login Flows в Keycloak очень много, то давайте рассмотрим лишь некоторые из них, которые, на мой взгляд, являются наиболее важными. Предлагаю следующий список.
Авторизация и регистрация пользователей
Локализация страниц
Подтверждение email
Вход через социальные сети
Локализация страниц в Keycloak
Запустим наши Spring Boot и Vue.js приложения. При переходе в клиентское Vue.js приложение нас перенаправит на страницу логина Keycloak.
В первую очередь давайте добавим поддержку русского языка. Для этого в админке Keycloak, на вкладке Theams
, в настройки realm включаем флаг Internationalization Enabled
. В Supported Locales
убираем все локали кроме ru
, пусть наше приложение на Vue.js поддерживает только один язык. В Default Locale
выставляем ru
.
Нажимаем Save
и возвращаемся в наше клиентское приложение.
Как видим, русский язык у нас появился, правда, не все текстовки были локализованы. Это можно исправить, добавив собственные варианты перевода. Сделать это можно на вкладке Localization
, в настройках realm.
Здесь имеется возможность добавить текстовки вручную по одной, либо загрузить их из json файла. Давайте сделаем это вручную. Для начала требуется добавить локаль. Вводим ru
и нажимаем Create
. После чего попадаем на страницу Add localization text
. На этой странице нам необходимо заполнить поля Key
и Value
. Если с value всё ясно, это будет просто значение нашей текстовки, то вот где взять Key
не совсем понятно. В документации допустимые ключи нигде не описаны (либо я просто плохо искал), поэтому остаётся лишь найти их в исходниках Keycloak. Находим в ресурсах нужную нам базовую тему base
и страницу login
, а затем файл с текстовками в локали en
- messages_en.properties. В этом файле по значению определяем нужный нам ключ текстовки, добавляем его в Key
на странице Add localization text
, а так же добавляем нужное нам Value
и нажимаем Save
.
После этого на вкладке Localization
в настройках realm, при выборе локали ru
, появляется таблица, в которой можно посмотреть, отредактировать или удалить нашу добавленную текстовку.
Вернёмся в наше клиентское приложение. Теперь все текстовки на странице логина локализованы.
Регистрация пользователей
Поддержку регистрации пользователей можно добавить, включив флаг User registration
на вкладке Login
в настройках realm.
После этого на странице логина появится кнопка Регистрация
.
Нажимаем на кнопку Регистрация
и попадаем на соответствующую страницу.
Давайте немного подкрутим эту страницу. Для начала добавим отсутствующий перевод текстовки, аналогично тому, как мы делали это ранее для страницы логина. Так же давайте уберём поле Имя пользователя
. На самом деле совсем его убрать нельзя, т.к. это поля обязательно для заполнения у пользователя Keycloak, но можно сделать так, чтобы в качестве имени пользователя использовался email, при этом поле Имя пользователя
исчезнет с формы регистрации. Сделать это можно, включив флаг Email as username
на вкладке Login
в настройках realm. После этого возвращаемся на страницу регистрации и видим что поле исчезло.
Кроме этого на странице логина поле, которое ранее называлось Имя пользователя или E-mail
, теперь называется просто E-mail
. Правда, пользователи, которые, например, были созданы до выставления этого флага, и у которых email отличается от имени пользователя, могут продолжать в качестве логина использовать имя пользователя и всё будет корректно работать.
Подтверждение email
Давайте включим подтверждение email у пользователей, чтобы после регистрации они не могли зайти в наше приложение, пока не подтвердят свой email. Сделать это можно, включив флаг Verify email
на вкладке Login
в настройках realm. И нет, после этого волшебным образом всё не заработает, нужно ещё где-то добавить конфигурацию SMTP-сервера, с которого мы будем осуществлять рассылку. Сделать это можно на вкладке Email
, в настройках realm. Ниже приведён пример настроек SMTP-сервера Gmail.
Нажимаем Test connection
и получаем ошибку.
Ошибка возникает из-за того, что при нажатии на Test connection
должно отправиться письмо на адрес пользователя, под которым мы сейчас залогинены в Keycloak, но этот адрес не задан. Соответственно, если вы заранее задали этот email, ошибки не будет.
Давайте зададим email нашему пользователю Keycloak. Для этого перейдём в realm master
на страницу Users
и нажмём View all users
, чтобы отобразить всех пользователей.
Перейдём на страницу редактирования нашего пользователя и зададим ему email.
Возвращаемся на страницу конфигурации SMTP-сервера, снова пробуем Test connection
и видим что всё рабо... Нет, мы снова видим ошибку. Правда, уже другую.
Если все параметры подключения к SMTP-серверу заданы корректно, и вы тоже используете SMTP-сервер Gmail, то, возможно, вам поможет разрешение доступа к вашему аккаунту "ненадежных приложений" в настройках безопасности вашего аккаунта, с которого вы пытаетесь отправлять письма. Если не поможет, то да прибудет с вами Google. Если вы используете SMTP-сервер не от Gmail, то, возможно, у вас не будет подобной ошибки, а если будет, может быть, в настройках вашей почты тоже можно задать подобную конфигурацию для "ненадежных приложений".
Снова жмём Test connection
и, наконец-то, получаем Success
.
Содержимое письма, которое будет ждать нас на почте, представлено ниже.
Давайте зарегистрируем пользователя, чтобы проверить, что подтверждение email корректно работает.
После нажатия на кнопку Регистрация
. Мы попадём на страницу с предупреждением о том, что нужно подтвердить email. На эту страницу мы будем попадать каждый раз при логине в нашем приложении, до тех пор пока не подтвердим email.
На почту нам придёт письмо с ссылкой, по которой можно подтвердить email.
После перехода по ссылке мы попадём на нашу тестовую страницу /pages/Home.vue
, на которой просто выводится имя пользователя. Т.к. в настройках нашего realm мы указали Email as username
, то на данной странице мы увидим email нашего пользователя.
Social Login
Теперь добавим вход через социальные сети. В качестве примера давайте рассмотрим вход с помощью Google. Для того чтобы добавить данный функционал нужно в нашем realm создать соответствующий Identity Provider. Для этого нужно перейти на страницу Identity Providers
и в списке Add provider...
выбрать Google
.
После этого мы попадём на страницу создания Identity Provider
.
Здесь нам требуется задать два обязательных параметра - Client ID
и Client Secret
. Взять их можно из Google Cloud Platform.
Сказ о получении ключей из Google Cloud Platform
В первую очередь нам нужно создать в Google Cloud Platform проект.
Жмём CREATE PROJECT
и попадаем на страницу создания проекта.
Задаём имя, жмём CREATE
, ждём некоторое время, пока не будет создан наш проект, и после этого попадаем на DASHBOARD
проекта.
Выбираем в меню APIs & Services
-> Credentials
. И попадаем на страницу на которой мы можем создавать различные ключи для нашего приложения.
Жмём Create credentials
-> OAuth client ID
и попадаем на очередную страницу.
Видим, что нам так просто не хотят давать возможность создавать ключи, а сначала просят создать некий OAuth consent screen. Что ж, хорошо, жмём CONFIGURE CONSENT SCREEN
и снова новая страница.
Здесь давайте выберем External
. Ну как выберем, выбора, на самом деле, у нас нет, т.к. Internal
доступно только пользователям Google Workspace и эта штука платная и нужна, в общем-то, только организациям. Нажимаем Create
и попадаем на страницу OAuth consent screen
. Здесь заполняем название приложения и почты и жмём SAVE AND CONTINUE
.
На следующей странице можно задать так называемые области действия OAuth 2.0 для API Google. Ничего задавать не будем, жмём SAVE AND CONTINUE
.
На этой странице можно добавить тестовых пользователей. Только тестовые пользователи могут получить доступ к вашему приложением пока оно находится в статусе Testing
. Но при этом логиниться в вашем приложении можно не добавляя здесь пользователей. По крайней мере это так работает на момент написания статьи. Поэтому давайте не будем добавлять здесь никаких пользователей. В любом случае, если у вас не получится авторизоваться, вы сможете добавить тестовых пользователей позднее.
На следующей странице можно проверить все данные, которые мы заполняли и в случае чего перейти к конкретному блоку с помощью кнопки EDIT
.
Жмём BACK TO DASHBOARD
, чтобы всё это уже закончить, и попадаем на страницу, на которой мы можем редактировать все те данные, которые мы вводили на предыдущих страницах.
Жмём Credentials
, затем снова Create credentials
-> OAuth client ID
и попадаем на страницу создания OAuth client ID
. И снова нужно что-то вводить. Google, ну сколько можно?! Ниже приведены поля, которые необходимо заполнить на этой странице.
Application type
- выбираемWeb application
Name
- пишем имя нашего приложенияAuthorized redirect URIs
- сюда пишем значение из поляRedirect URI
со страницы созданияIdentity Provider
, чтобы Google редиректил пользователей на корректный адрес Keycloak после авторизации
Жмём CREATE
и, наконец-то, получаем требуемые нам Client ID
и Client Secret
, которые нам нужно указать на странице создания Identity Provider
в Keycloak.
Заполняем поля Client ID
и Client Secret
и жмём Save
, чтобы создать Identity Provider
. Теперь вернёмся на страницу логина нашего клиентского приложения. На этой странице появится нелокализованная текстовка, добавить её можно аналогично тому, как это было сделано ранее. Ниже на скрине ниже эта проблема уже устранена.
Итак, это всё что требовалось сделать, теперь мы можем входить в наше приложение с помощью Google.
Импорт и экспорт в Keycloak
В Keycloak есть возможность импортировать и экспортировать конфигурации ваших realm'ов. Это можно использовать, например, для переноса конфигураций между различными инстансами Keycloak. Или, что более вероятно, для того чтобы можно было запускать Keycloak локально с уже готовой конфигурацией и использовать его для разработки. Это может быть полезно в тех ситуациях, когда нет возможности запустить Keycloak глобально на каком-нибудь сервере либо когда до этого инстанса Keycloak по какой-либо причине нет доступа.
Для того чтобы экспортировать конфигурацию из Keycloak, нужно перейти на страницу Export
, выбрать данные, которые нужно экспортировать и нажать Export
.
После этого выгрузится файл realm-export.json
с конфигурацией того realm в котором мы сейчас находимся. При этом различные пароли и секреты в этом файле будут в виде **********
, поэтому, прежде чем куда-то импортировать этот файл, нужно заменить все такие значения на корректные. Либо сделать это после импорта через адиминку.
Импортировать данные можно на странице Import
. Либо в yml-файле Docker Compose, если вы его используете. Для этого нужно указать в переменной окружения KEYCLOAK_IMPORT
путь до ранее экспортированного файла и примонтировать этот файл в контейнер с помощью volumes. Итоговый файл приведен ниже.
# For development
version: "3.8"
services:
keycloak:
image: jboss/keycloak:12.0.2
environment:
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: admin
KEYCLOAK_IMPORT: "/tmp/realm-export.json"
volumes:
- "./keycloak/realm-export.json:/tmp/realm-export.json"
ports:
- 8080:8080
Импорт файлов локализации
Как уже упоминалось ранее, файлы локализации можно импортировать через админку. Помимо этого у Keycloak есть Admin REST API, а именно метод POST /{realm}/localization/{locale}
, с помощью которого можно это сделать. В теории это можно использовать в Docker Compose, чтобы при запуске сразу загружать все текстовки в автоматическом режиме. На практике для этого можно написать bash-скрипт и вызвать его после того как в контейнере запустится Keycloak. Пример такого скрипта приведен ниже.
#!/bin/bash
DIRECT_GRANT_RESPONSE=$(curl -i --request POST http://localhost:8080/auth/realms/master/protocol/openid-connect/token --header "Accept: application/json" --header "Content-Type: application/x-www-form-urlencoded" --data "grant_type=password&username=admin&password=admin&client_id=admin-cli");
export DIRECT_GRANT_RESPONSE
ACCESS_TOKEN=$(echo $DIRECT_GRANT_RESPONSE | grep "access_token" | sed 's/.*\"access_token\":\"\([^\"]*\)\".*/\1/g');
export ACCESS_TOKEN
curl -i --request POST http://localhost:8080/auth/admin/realms/list-keep/localization/ru -F "file=@ru.json" --header "Content-Type: multipart/form-data" --header "Authorization: Bearer $ACCESS_TOKEN";
И в докер образе jboss/keycloak даже есть возможность запускать скрипты при старте (см. раздел Running custom scripts on startup
на странице докер образа). Но запускаются они до фактического старта Keycloak. Поэтому пока я оставил данный вопрос не решенным. Если у кого-то есть идеи как это можно красиво сделать - оставляйте их в комментариях.
Заключение
Что ж. Вот и всё. Это конец. Надеюсь, мне удалось показать насколько просто и быстро можно интегрировать Keycloak с вашими приложениями. А так же насколько просто можно прикручивать различный функционал, связанный с аутентификацией и авторизацией пользователей, благодаря тому что большая часть этого функционала доступна из коробки. По крайней мере, насколько это может быть проще, чем если бы всё это приходилось писать самому.
Надеюсь, вы нашли в этой статье что-то полезное и интересное.
И ещё... Берегите там себя.
sshikov
Вы так говорите, как-будто это когда-то мешало использовать какой-либо продукт… ну да, обычно есть риск, что его больше не поддерживают, и что-то поломанное уже никогда не починят, но если в нем что-то работало — оно продолжит работать.
mamadaliev
Не просто так же задеприкейтили. Идентификационные брокеры (Keycloak и тому подобные) сейчас лучший выбор для микросервисной архитектуры.
sshikov
Не, ну это вполне может быть легаси приложение же. В общем, меня смущает формулировка «Вы больше не можете… потому что». Чей-то не могу, могу конечно. То что при этом есть более хорошие новые варианты решения — ну да, но старые при этом никуда не деваются. То что они стали deprecated — это даже не означает, что в них например нашли дырки в безопасности (хотя и такое конечно бывает).
Ну на самом деле это я так, немного к словам придираюсь.