本記事の概要、目的
本記事は、OSSのIDプロバイダであるKeycloakを、ソースコード上から機能カスタマイズする際に重要となるポイントを押さえた解説を行います。
Keycloakの特徴として、OSSであり、拡張性高く設計されている点が挙げられるため、認証サーバを選定する際、拡張性を重視する際には有力な候補となるかと思います。
実際にKeycloakで安全に機能拡張する際、基礎的な構造を理解した上でコーディングを行う必要があります。
しかし、コンポーネント設計に関してKeycloakは独自のアーキテクチャを採用しており、若干取っ付きづらい面があります。
本記事は
- これからKeycloakのカスタマイズを行う
- Keycloakのカスタマイズを行ってみたものの、イマイチ仕組みがよくわからない
という人向けに、土台を理解した上で納得のいくコーディングが行えることを目標としています。
本記事で扱わないもの
以下は、前述した趣旨に沿わないため割愛させて頂きます。
- IDプロバイダとしてのKeycloakの基本的な理解
- Javaコード以外のKeycloakカスタマイズ方法
- 一般的な認証認可の仕組み
- Keycloakを理解する上での前提知識(Java SE・EEなど)
Keycloakの構造
Keycloakには膨大な数の機能が備わっていますが、これらを分割することで保守性・拡張性を高めるための独自構造を持っています。
この構造を理解する上でのキーワードが以下になります。
- Provider
- ProviderFactory
- SPI(Service Provider Interface)
- ServiceLoader(META-INF/services/*)
- KeycloakSession
まずは、これらについて掘り下げて解説します。
なお、一般的なDI(Dependency Injection)の概念を知っていると、より理解が捗るかと思います。上記のキーワードは、要するDIの仕組みを独自に構築するために必要なもの、と筆者は理解しています。
Provider
Providerは、個別機能の実際の振る舞いを定義(実装)するインターフェース(クラス)です。
すべてのProviderは、インターフェースProvider
を継承することで作成されます。
基本となるProvider
インターフェースそのものはごくシンプルなものです。
public interface Provider {
void clone();
}
個別機能向けに、このProvderを継承したインターフェースが作成され、その実装クラスで実際のロジックが記述されます。
例えば、UserProviderというインターフェースがあり、これはユーザ(認証サービスの利用者)の取得・追加メソッドが宣言されています。
UserProviderの実装クラスは以下の通り複数存在します。
- JpaUserProvider
- UserCacheSession
- UserStorageManager
ProviderFactory
名前の通り、Providerのインスタンスを生成するためのファクトリを定義(実装)するインターフェース(クラス)です。
ProviderFactoryもシンプルなインターフェースです。
public interface ProviderFactory<T extends Provider> {
T create(KeycloakSession session);
void init(Config.Scope config);
void postInit(KeycloakSessionFactory factory);
void close();
String getId();
default int order() { return 0; }
default List<ProviderConfigProperty> getConfigMetadata() {
return Collections.emptyList();
}
}
個別機能向けに、このProvderFactoryを継承したインターフェースが作成され、その実装クラスでProvider生成処理が記述されます。
例えば、UserProviderに対応するファクトリUserProviderFactoryも、ごくシンプルです。多くのケースにおいて、ProviderFactoryはこのようにメソッド等の追加なしで定義されています。
public interface UserProviderFactory<T extends UserProvider> extends ProviderFactory<T> {
}
Provider実装クラスに対するProviderFactory実装クラスは、基本的に一対一対応します(制約があるわけではなく、自然とそのような作りになります)。
前述したUserProviderの実装クラスに対するファクトリは以下のものになります。
- JpaUserProvider -> JpaUserProviderFactory
- UserCacheSession -> UserCacheSessionFactory
- UserStorageManager -> UserStorageManagerFactory
SPI(Service Provider Interface)
Providerインターフェースだけでは、コンポーネント間を疎結合にする仕組みとしては不十分です。
なぜならば、Providerから別のProviderの処理を呼び出す際、その実装クラスを直接指定せずにインスタンスを取得する仕組みが必要となります。
その仕組みのカギとなるのがSPIとなります。
SPIそのものはごくシンプルなインターフェースとなります。
public interface Spi {
boolean isInternal();
String getName();
Class<? extends Provider> getProviderClass();
Class<? extends ProviderFactory> getProviderFactoryClass();
default boolean isEnabled() { return true; }
}
getProviderClass()
、getProviderFactoryClass()
が重要で、この情報を利用して、ProviderクラスとProviderFactoryクラスの紐付けが行われます。
前述の、UserProviderを得るためのSPIが、UserSpiというものになります。こちらも実装はごくシンプルです。
public class UserSpi implements Spi {
@Override public boolean isInternal() { return true; }
@Override public String getName() { return "user"; }
@Override public Class<? extends Provider> getProviderClass() {
return UserProvider.class;
}
@Override public Class<? extends ProviderFactory> getProviderFactoryClass() {
return UserProviderFactory.class;
}
}
ServiceLoader(META-INF/services/*)
SPIの実装クラスを作成することで、Providerとそのファクトリの紐付け情報が用意されることになります。しかし、単純にクラスを用意するだけでは認識されず、宙ぶらりの状態となってしまいます。
そこで、KeycloakはServiceLoaderの機能を使い、SPIの実装クラスを認識し、さらにそこからProviderFactoryの実装クラスを引っ張ってきます。
ServiceLoaderは、Java標準機能であり、これを利用することで、インターフェースからその実装クラスのインスタンスを生成することが出来ます。
方法としては、src/main/resources/META-INF/services
下に、インターフェース名(パッケージ名のついたフルネーム)と同じファイルを作成します。
そのファイル内に、実装クラス名(こちらもパッケージ名のついたフルネーム)を記述する形となります。
例えば、UserSpiは、ファイルsrc/main/resources/META-INF/services/org.keycloak.provider.Spi
に、以下のように記載されています(他にも多数SPI実装クラスが記述されています)。
# 中略
org.keycloak.models.UserSpi
# 中略
逆に言えば、作成したProviderFactory、SPIをKeycloakに認識させるためには、META-INF/servicesに適切なファイルを作成する必要があります。
参考:「Java SE 6完全攻略」第11回 コンポーネントのロードを行うServiceLoader
※2006年の記事ですが、ServiceLoader仕様は特に変わっていない様で、十分役立ちます。
KeycloakSession
Providerを疎結合にする仕組みの仕上げとして、Provider同士の橋渡し役を担うKeycloakSessionが登場します。この名前は、その役割上ソースコードで散見されることになります。
Providerは基本的に、このKeycloakSessionをプロパティやメソッド引数などで持ち回し、以下のメソッドを呼び出すことで別のProviderを取得して処理を呼び出すことができます。
<T extends Provider> T getProvider(Class<T> clazz);
ここでclazz
に指定する対象は、実装クラスではなく、抽象インターフェースとなります(Spi.getProviderClass
で取得されるもの)。
よくある使用例が、JpaConnectionProviderの取得です。このProviderを取得するには、以下のようにします。
JpaConnectionProvider provider = session.getProvider(JpaConnectionProvider.class);
JpaConnectionProviderは実装クラスではなく、抽象インターフェースです。定義はごくシンプルです。
public interface JpaConnectionProvider extends Provider {
EntityManager getEntityManager();
}
なお、KeycloakSessionを利用すると、登録されている任意のProviderを取得できるため、使い方を誤るとクラス間の依存関係がごちゃごちゃになってしまう懸念があり、設計時に注意が必要です(個人的に、この辺はそもそもKeycloakSessionが作りとして適切か、疑問を持ってしまいます)。
補足:セッション(トランザクション)処理
KeycloakSessionは、名前の通り、セッション処理の役割も担います。
そのため、例えばKeycloakSessionオブジェクトをstatic変数で持ち回す様なことは禁止すべきです。
なお、ここでの「セッション」は、ユーザセッションではなくサーバ側のセッションです。1トランザクションと捉えて良いかと思います。
実際、org.keycloak.models.utils.KeycloakModelUtils
に、1つのトランザクションを発生させて処理を行うためのメソッドが用意されています。
/**
* Wrap given runnable job into KeycloakTransaction.
* @param factory The session factory to use
* @param task The task to execute
*/
public static void runJobInTransaction(KeycloakSessionFactory factory, KeycloakSessionTask task) {
runJobInTransactionWithResult(factory, null, session -> {
task.run(session);
return null;
});
}
/**
* Wrap given runnable job into KeycloakTransaction.
* @param factory The session factory to use
* @param context The context from the previous session
* @param task The task to execute
*/
public static void runJobInTransaction(KeycloakSessionFactory factory, KeycloakContext context, KeycloakSessionTask task) {
runJobInTransactionWithResult(factory, context, session -> {
task.run(session);
return null;
});
}
処理フロー
おおまかに、以下のフローとなります(厳密に正しくない箇所もあるかもしれませんがご容赦ください)
アプリ起動時の準備
- ServiceLoaderからSPIインスタンスが生成されます
- 各SPIインスタンスから、ProviderFactoryインターフェースのclassオブジェクトが取得されます
- 再びServiceLoaderから、ProviderFactoryインスタンスが生成されます
- 各SPIインスタンスから、Providerインターフェースのclassオブジェクトが取得されます
- Providerインターフェースのclassオブジェクトと、ProviderFactoryインスタンスのマッピングを作成します
- 生成したSPI、ProviderFactoryインスタンスは以降使い回されます
何かしらのトリガー(例えばAPIコール)
- Providerは、要求があった際にセッション単位でインスタンス生成されます
- Providerの生成は、ProviderFactory.createによって行われます
- ProviderFactoryのインスタンスは、アプリ起動時の準備処理中に作成されるマッピングから検索します