1. Keycloakテーマの概要
Keycloakを使用すると、システムを通じてユーザーインターフェイスを完全にカスタマイズできますテーマ。各テーマは、ログインページから通知メールに至るまで、Keycloak の特定の部分のルックアンドフィールを制御します。
1.1 テーマの種類
キークロークのサポート5種類のテーマ:
| テーマの種類 | 説明する | 該当ページ |
|---|---|---|
| ログイン | ログイン、登録、パスワードリセットインターフェイス | ログイン、登録、OTP、パスワードのリセット、エラー |
| アカウント | ユーザーアカウント管理ページ | アカウント コンソール (v3 React ベース) |
| 管理者 | 管理コンソールのインターフェース | 管理コンソール (React ベース) |
| 電子メール | ユーザーに送信される電子メール テンプレート | 電子メールの確認、パスワードのリセット、イベント通知 |
| いらっしゃいませ | デフォルトのようこそページ | ルート URL にアクセスしたときのランディング ページ |
1.2 デフォルトのテーマ
Keycloakには利用可能なテーマが付属しています:
- キーマント— ログイン、アカウント、電子メールのデフォルトのテーマ
- keycloak.v2— ログイン用の新しいテーマ (Keycloak 24+)、アカウントコンソール v3
- ベース(非推奨) — 基本テーマ。親として使用しないでください。
2. テーマフォルダー構造
各テーマは、標準のフォルダー構造に従って編成されています。
themes/
└── my-custom-theme/
├── login/
│ ├── theme.properties
│ ├── resources/
│ │ ├── css/
│ │ │ └── styles.css
│ │ ├── js/
│ │ │ └── custom.js
│ │ └── img/
│ │ └── logo.png
│ ├── templates/
│ │ ├── login.ftl
│ │ ├── register.ftl
│ │ └── error.ftl
│ └── messages/
│ ├── messages_en.properties
│ └── messages_vi.properties
├── account/
│ ├── theme.properties
│ └── ...
└── email/
├── theme.properties
├── html/
│ └── email-verification.ftl
└── text/
└── email-verification.ftl
2.1 メインディレクトリ
| ディレクトリ | 目的 |
|---|---|
リソース/ | CSS、JavaScript、静的画像 |
テンプレート/ | FreeMarker テンプレート ファイル (.ftl) |
メッセージ/ | 多言語用のファイル i18n |
3. テーマプロパティファイル
ファイルテーマ.プロパティは、各テーマ タイプの中心的な構成ファイルです。
# themes/my-custom-theme/login/theme.properties
# Kế thừa từ theme keycloak (sử dụng tất cả template/resource chưa override)
parent=keycloak.v2
# Import resources từ theme khác (common resources)
import=common/keycloak
# CSS files (thêm custom CSS)
styles=css/login.css css/styles.css
# Locales hỗ trợ
locales=en,vi
# Script files
scripts=js/custom.js
# Cache control (cho production)
cacheControl=max-age=2592000
3.1 重要な特性
| 財産 | 説明する | 例えば |
|---|---|---|
親。親 | 継承する親テーマ | 親=keycloak.v2 |
輸入 | 別のテーマからリソースをインポートする | import=common/keycloak |
スタイル。スタイル | CSS ファイルのリスト (スペース区切り) | スタイル=css/login.css css/custom.css |
スクリプト | JavaScript ファイル | スクリプト=js/app.js |
ロケール | 言語サポート | ロケール=en、vi、ja |
4.FreeMarkerテンプレートエンジン
キークロークの使用Apache フリーマーカーログインおよび電子メールテーマのテンプレートエンジンとして。 FreeMarker を使用すると、変数とロジックを使用して動的な HTML を生成できます。
4.1 基本的な構文
<!-- Biến -->
${realm.displayName}
${url.loginAction}
${messagesPerField.printIfExists('username','has-error')}
<!-- Điều kiện -->
<#if realm.password>
<!-- Form đăng nhập bằng password -->
</#if>
<#if social.providers??>
<!-- Hiển thị social login buttons -->
<#list social.providers as p>
<a href="${p.loginUrl}">${p.displayName}</a>
</#list>
</#if>
<!-- Macro -->
<#macro registrationLayout>
<!-- Layout wrapper -->
</#macro>
4.2 ログインテーマで利用可能な変数
| 変数 | 説明する |
|---|---|
領域。レルム | レルム情報 (表示名、登録許可、パスワード、ソーシャル...) |
URL | URL (loginAction、registrationUrl、loginResetCredentialsUrl...) |
クライアント | クライアントがログインを要求しています (名前、クライアント ID...) |
ログイン | フォームデータ (入力されたユーザー名...) |
メッセージ。メッセージ | エラー/成功メッセージ |
フィールドごとのメッセージ | 各フィールドの検証メッセージ |
社交。社交 | ソーシャルログインプロバイダー |
ロケール | 現在のロケールとサポートされているロケールのリスト |
5. 特定のテンプレートをオーバーライドする
変更したいテンプレートをオーバーライドするだけです。残りのテンプレートは親テーマから継承されます。
5.1 ログインページの上書き
<!-- themes/my-custom-theme/login/templates/login.ftl -->
<#import "template.ftl" as layout>
<@layout.registrationLayout
displayMessage=!messagesPerField.existsError('username','password')
displayInfo=realm.password && realm.registrationAllowed
; section>
<#if section = "header">
<div class="custom-header">
<img src="${url.resourcesPath}/img/logo.png" alt="Logo" class="login-logo" />
<h1>${msg("loginAccountTitle")}</h1>
</div>
<#elseif section = "form">
<div class="custom-login-form">
<form action="${url.loginAction}" method="post">
<div class="form-group">
<label for="username">
<#if !realm.loginWithEmailAllowed>
${msg("username")}
<#elseif !realm.registrationEmailAsUsername>
${msg("usernameOrEmail")}
<#else>
${msg("email")}
</#if>
</label>
<input id="username" name="username" type="text"
value="${(login.username!'')}"
class="form-control ${messagesPerField.printIfExists('username','has-error')}"
autofocus autocomplete="username" />
<#if messagesPerField.existsError('username')>
<span class="error-message">${messagesPerField.get('username')}</span>
</#if>
</div>
<div class="form-group">
<label for="password">${msg("password")}</label>
<input id="password" name="password" type="password"
class="form-control ${messagesPerField.printIfExists('password','has-error')}"
autocomplete="current-password" />
<#if messagesPerField.existsError('password')>
<span class="error-message">${messagesPerField.get('password')}</span>
</#if>
</div>
<div class="form-actions">
<#if realm.rememberMe && !usernameHidden??>
<label class="remember-me">
<input id="rememberMe" name="rememberMe" type="checkbox"
<#if login.rememberMe??>checked</#if>>
${msg("rememberMe")}
</label>
</#if>
<button type="submit" class="btn btn-primary">
${msg("doLogIn")}
</button>
</div>
</form>
<#if realm.password && realm.registrationAllowed>
<div class="register-link">
${msg("noAccount")}
<a href="${url.registrationUrl}">${msg("doRegister")}</a>
</div>
</#if>
</div>
</#if>
</@layout.registrationLayout>
5.2 登録ページの上書き
<!-- themes/my-custom-theme/login/templates/register.ftl -->
<#import "template.ftl" as layout>
<@layout.registrationLayout
displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm')
; section>
<#if section = "header">
<h1>${msg("registerTitle")}</h1>
<#elseif section = "form">
<form action="${url.registrationAction}" method="post">
<div class="form-group">
<label for="firstName">${msg("firstName")}</label>
<input id="firstName" name="firstName" type="text"
value="${(register.formData.firstName!'')}"
class="form-control" />
</div>
<div class="form-group">
<label for="lastName">${msg("lastName")}</label>
<input id="lastName" name="lastName" type="text"
value="${(register.formData.lastName!'')}"
class="form-control" />
</div>
<div class="form-group">
<label for="email">${msg("email")}</label>
<input id="email" name="email" type="email"
value="${(register.formData.email!'')}"
class="form-control" autocomplete="email" />
</div>
<button type="submit" class="btn btn-primary">
${msg("doRegister")}
</button>
<div class="back-to-login">
<a href="${url.loginUrl}">${msg("backToLogin")}</a>
</div>
</form>
</#if>
</@layout.registrationLayout>
5.3 オーバーライドエラーページ
<!-- themes/my-custom-theme/login/templates/error.ftl -->
<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=false; section>
<#if section = "header">
${kcSanitize(msg("errorTitle"))?no_esc}
<#elseif section = "form">
<div class="error-container">
<div class="alert alert-error">
<span class="error-icon">⚠</span>
${kcSanitize(message.summary)?no_esc}
</div>
<#if skipLink??>
<#else>
<#if client?? && client.baseUrl?has_content>
<a href="${client.baseUrl}" class="btn btn-primary">
${kcSanitize(msg("backToApplication"))?no_esc}
</a>
</#if>
</#if>
</div>
</#if>
</@layout.registrationLayout>
6. カスタム CSS と JavaScript
6.1 カスタムCSSの追加
/* themes/my-custom-theme/login/resources/css/styles.css */
/* Override login page styles */
.login-pf body {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
font-family: 'Inter', -apple-system, sans-serif;
}
/* Custom login card */
#kc-login {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
max-width: 440px;
margin: 0 auto;
}
/* Logo */
.login-logo {
max-width: 180px;
margin: 0 auto 24px;
display: block;
}
/* Form inputs */
.form-control {
border: 2px solid #e0e0e0;
border-radius: 8px;
padding: 12px 16px;
font-size: 14px;
transition: border-color 0.2s;
}
.form-control:focus {
border-color: #0f3460;
box-shadow: 0 0 0 3px rgba(15, 52, 96, 0.1);
outline: none;
}
.form-control.has-error {
border-color: #e74c3c;
}
/* Primary button */
.btn-primary {
background: #0f3460;
color: white;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
width: 100%;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover {
background: #1a4a7a;
}
/* Social login buttons */
.social-links {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 16px;
}
6.2 カスタム JavaScript の追加
// themes/my-custom-theme/login/resources/js/custom.js
document.addEventListener('DOMContentLoaded', function() {
// Password visibility toggle
const passwordField = document.getElementById('password');
if (passwordField) {
const toggleBtn = document.createElement('button');
toggleBtn.type = 'button';
toggleBtn.className = 'password-toggle';
toggleBtn.textContent = '👁';
toggleBtn.addEventListener('click', function() {
passwordField.type = passwordField.type === 'password' ? 'text' : 'password';
});
passwordField.parentNode.appendChild(toggleBtn);
}
// Form validation enhancement
const form = document.querySelector('form');
if (form) {
form.addEventListener('submit', function(e) {
const submitBtn = form.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.textContent = 'Đang xử lý...';
}
});
}
});
7. 国際化 (i18n)
Keycloakはファイルを通じて複数の言語をサポートしますメッセージ_{ロケール}.properties.
7.1 ベトナム語ファイルの作成
# themes/my-custom-theme/login/messages/messages_vi.properties
# Login form
loginAccountTitle=Đăng nhập vào tài khoản
username=Tên đăng nhập
usernameOrEmail=Tên đăng nhập hoặc Email
password=Mật khẩu
rememberMe=Ghi nhớ đăng nhập
doLogIn=Đăng nhập
noAccount=Chưa có tài khoản?
doRegister=Đăng ký ngay
# Registration
registerTitle=Tạo tài khoản mới
firstName=Họ
lastName=Tên
email=Địa chỉ email
doRegister=Đăng ký
# Errors
invalidUserMessage=Tên đăng nhập hoặc mật khẩu không đúng.
accountDisabledMessage=Tài khoản đã bị vô hiệu hóa. Vui lòng liên hệ quản trị viên.
# Password reset
emailForgotTitle=Quên mật khẩu?
backToLogin=Quay lại đăng nhập
# Custom messages
welcomeMessage=Chào mừng bạn đến với hệ thống
companyName=Công ty của bạn
7.2 英語メッセージを上書きする
# themes/my-custom-theme/login/messages/messages_en.properties
# Override default messages
loginAccountTitle=Sign in to your account
welcomeMessage=Welcome to our platform
companyName=Your Company
# Custom messages cho branding
loginTitleHtml=<strong>Welcome</strong> to {0}
7.3 テンプレートでの i18n の使用
<!-- Sử dụng message key -->
<h1>${msg("loginAccountTitle")}</h1>
<!-- Message với tham số -->
<p>${msg("loginTitleHtml", realm.displayName)}</p>
<!-- Kiểm tra message tồn tại -->
<#if msg("welcomeMessage")?has_content>
<p class="welcome">${msg("welcomeMessage")}</p>
</#if>
<!-- Locale selector -->
<#if realm.internationalizationEnabled && locale.supported?size gt 1>
<div class="locale-selector">
<#list locale.supported as l>
<a href="${l.url}" class="${(l.label == locale.current)?then('active','')}">
${l.label}
</a>
</#list>
</div>
</#if>
8. 保護者と一緒にテーマを拡張する
テーマの継承により、既存のテーマに基づいて新しいテーマを作成し、変更が必要な部分のみをオーバーライドできます。
8.1 仕組み
Thứ tự tìm kiếm resource/template:
1. Current theme (my-custom-theme/login/)
2. Parent theme (keycloak.v2/login/)
3. Parent của parent (nếu có)
4. Base resources (import)
8.2 マルチレベルの継承
# themes/company-base/login/theme.properties
parent=keycloak.v2
styles=css/login.css css/company-base.css
locales=en,vi
# themes/company-product-a/login/theme.properties
parent=company-base
styles=css/login.css css/company-base.css css/product-a.css
8.3 テーマのバリエーション
Keycloak 24以降では、次のように定義できます。テーマのバリエーション同じテーマの異なるバージョンを使用するには:
# theme.properties
parent=keycloak.v2
# Variant definition (admin có thể chọn trong Realm Settings)
variant.light.styles=css/login.css css/light.css
variant.dark.styles=css/login.css css/dark.css
9. テーマの展開
9.1 スタンドアロン展開
テーマをフォルダーにコピーテーマ/キークロークの:
# Cấu trúc standalone
$KEYCLOAK_HOME/
├── themes/
│ └── my-custom-theme/
│ ├── login/
│ │ ├── theme.properties
│ │ ├── resources/
│ │ ├── templates/
│ │ └── messages/
│ └── email/
│ └── ...
9.2 Docker のデプロイメント
# Dockerfile - Simple COPY
FROM quay.io/keycloak/keycloak:26.1 AS builder
# Copy custom theme
COPY themes/my-custom-theme /opt/keycloak/themes/my-custom-theme
# Build optimized Keycloak
RUN /opt/keycloak/bin/kc.sh build
FROM quay.io/keycloak/keycloak:26.1
COPY --from=builder /opt/keycloak/ /opt/keycloak/
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
9.3 ノードを使用した Docker マルチステージ ビルド (アカウント v3)
# Dockerfile - Multi-stage build cho Account Console v3 customization
FROM node:20-alpine AS theme-builder
WORKDIR /theme
COPY account-theme/ .
RUN npm ci && npm run build
FROM quay.io/keycloak/keycloak:26.1 AS keycloak-builder
# Copy login/email themes
COPY themes/my-custom-theme /opt/keycloak/themes/my-custom-theme
# Copy built Account v3 theme
COPY --from=theme-builder /theme/dist /opt/keycloak/themes/my-custom-theme/account
RUN /opt/keycloak/bin/kc.sh build
FROM quay.io/keycloak/keycloak:26.1
COPY --from=keycloak-builder /opt/keycloak/ /opt/keycloak/
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
9.4 JAR/SPIのデプロイメント
テーマを JAR ファイルとしてパッケージ化し、SPI プロバイダーとしてデプロイします。
my-theme.jar
├── META-INF/
│ └── keycloak-themes.json
└── theme/
└── my-custom-theme/
├── login/
└── email/
// META-INF/keycloak-themes.json
{
"themes": [
{
"name": "my-custom-theme",
"types": ["login", "email"]
}
]
}
# Deploy JAR
cp my-theme.jar $KEYCLOAK_HOME/providers/
$KEYCLOAK_HOME/bin/kc.sh build
10. テーマ開発ワークフロー
10.1 開発時にキャッシュをオフにする
Keycloak キャッシュ テーマはデフォルトで、開発時にはオフにする必要があります。
# Tắt theme caching cho development
bin/kc.sh start-dev \
--spi-theme-static-max-age=-1 \
--spi-theme-cache-themes=false \
--spi-theme-cache-templates=false
10.2 開発用の Docker Compose
# docker-compose.dev.yml
services:
keycloak:
image: quay.io/keycloak/keycloak:26.1
command:
- start-dev
- --spi-theme-static-max-age=-1
- --spi-theme-cache-themes=false
- --spi-theme-cache-templates=false
volumes:
# Mount theme directory cho hot-reload
- ./themes/my-custom-theme:/opt/keycloak/themes/my-custom-theme
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: keycloak
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
ports:
- "8080:8080"
depends_on:
- postgres
postgres:
image: postgres:17
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: keycloak
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
10.3 開発ワークフロー
# 1. Khởi chạy dev environment
docker compose -f docker-compose.dev.yml up -d
# 2. Chỉnh sửa file theme (CSV, FTL, properties)
# Thay đổi sẽ tự động reflect (vì cache đã tắt)
# 3. Refresh browser để xem thay đổi
# Không cần restart Keycloak!
# 4. Áp dụng theme vào Realm
# Admin Console → Realm Settings → Themes → Login Theme: my-custom-theme
11. React アカウント コンソール v3 のカスタマイズ
アカウント コンソール v3 が使用するもの反応 + パターンフライ。カスタマイズするには、カスタム React アプリを構築する必要があります。
11.1 Keycloakify (推奨)
使用Keycloakify— React で Keycloak テーマを作成するための特殊なフレームワーク:
# Tạo project mới với Keycloakify
npx create-keycloakify-project my-keycloak-theme
cd my-keycloak-theme
# Cấu trúc project
my-keycloak-theme/
├── src/
│ ├── login/ # Login theme pages
│ │ ├── KcPage.tsx
│ │ ├── pages/
│ │ │ ├── Login.tsx
│ │ │ ├── Register.tsx
│ │ │ └── ...
│ │ └── i18n.ts
│ └── account/ # Account theme pages
├── public/
│ └── ...
├── package.json
└── vite.config.ts
# Dev mode
npm run dev
# Build JAR
npm run build-keycloak-theme
# Output: dist_keycloak/my-keycloak-theme.jar
11.2 Keycloakify ログインページの例
// src/login/pages/Login.tsx
import { type PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function Login(props: PageProps<Extract<KcContext, { pageId: "login.ftl" }>, I18n>) {
const { kcContext, i18n, Template } = props;
const { url, realm, social, login } = kcContext;
const { msg } = i18n;
return (
<Template kcContext={kcContext} i18n={i18n}>
<div className="custom-login">
<img src={`${url.resourcesPath}/img/logo.png`} alt="Logo" />
<h1>{msg("loginAccountTitle")}</h1>
<form action={url.loginAction} method="post">
<input
name="username"
defaultValue={login.username ?? ""}
placeholder={msg("usernameOrEmail")}
/>
<input
name="password"
type="password"
placeholder={msg("password")}
/>
<button type="submit">{msg("doLogIn")}</button>
</form>
{social?.providers && (
<div className="social-providers">
{social.providers.map(p => (
<a key={p.alias} href={p.loginUrl}>
{p.displayName}
</a>
))}
</div>
)}
</div>
</Template>
);
}
12. 電子メールのテーマのカスタマイズ
電子メールのテーマには、テンプレートごとに HTML とプレーン テキストの 2 つのバージョンがあります。
<!-- themes/my-custom-theme/email/html/email-verification.ftl -->
<html>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: #0f3460; padding: 20px; text-align: center;">
<img src="${url.resourcesPath}/img/logo-white.png" alt="Logo" style="max-width: 150px;" />
</div>
<div style="padding: 30px; background: #ffffff;">
<h2>${msg("emailVerificationSubject")}</h2>
<p>${msg("emailVerificationBody", linkExpiration, realmName)}</p>
<a href="${link}" style="display: inline-block; background: #0f3460; color: white; padding: 12px 30px; text-decoration: none; border-radius: 6px;">
${msg("emailVerificationLinkText")}
</a>
</div>
<div style="padding: 15px; text-align: center; color: #888; font-size: 12px;">
<p>${msg("emailFooter")}</p>
</div>
</body>
</html>
<!-- themes/my-custom-theme/email/text/email-verification.ftl -->
${msg("emailVerificationSubject")}
${msg("emailVerificationBodyPlainText", link, linkExpiration, realmName)}
13. テーマをレルムに適用する
13.1 管理コンソール経由
- 管理コンソールにログインします
- 構成するレルムを選択してください
- 入力レルム設定 → テーマ
- タイプごとにテーマを選択します。
- ログインテーマ:
私のカスタムテーマ - アカウントのテーマ:
私のカスタムテーマ - 管理コンソールのテーマ: (デフォルトのまま)
- 電子メールのテーマ:
私のカスタムテーマ
- ログインテーマ:
- プレス保存
13.2 REST API経由
# Cập nhật theme cho realm qua API
curl -X PUT "http://localhost:8080/admin/realms/my-realm" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"loginTheme": "my-custom-theme",
"accountTheme": "my-custom-theme",
"emailTheme": "my-custom-theme"
}'
14. ベストプラクティス
- 常に親テーマを使用する— から継承
keycloak.v2最初から書くのではなく。 Keycloakをアップグレードする際は互換性を確保してください。 - 変更する必要があるもののみをオーバーライドする— テンプレート全体をコピーしないでください。カスタマイズするテンプレートのファイルのみを作成します。
- クライアントごとにテーマを分ける— 複数のアプリケーションがある場合は、クライアントごとに異なるテーマを使用できます (レルム設定またはクライアントレベルのテーマのオーバーライド)。
- 複数のブラウザでテスト済み— 特に、ログイン ページはモバイルでも機能する必要があります。
- i18n を使用する— テンプレートではハードコードされたテキストではなく、常にメッセージ キーを使用します。
- バージョン管理テーマ— テーマを別の Git リポジトリまたは同じプロジェクトで管理します。
- CI/CD パイプライン— CIでJARテーマを構築し、Keycloakコンテナでテストし、自動的にデプロイします。