はじめに
複数プロジェクトでマイクロサービスを利用するためにKeycloakを利用してマルチテナントを実現してみた。
試したこと
本家マニュアルを見ると、KeycloakConfigResolverインターフェースのresolve()メソッド使えばできそうであるが、複数プロジェクトのKeycloakコンフィグをJsonファイルで用意するのは勘弁して欲しいと思い、さらに調べるとAdapterConfigを引数にしてKeycloakコンフィグを動的に生成できることが分かった。auth_server_urlとresourceは共通なのでapplication.ymlで設定し、リクエストヘッダーに埋め込まれたプロジェクトIDをKeycloakのレルム名にすればマルチテナントを実現できる。
しかし、リスエストごとにKeycloakコンフィグを生成するのもパフォーマンス上問題になると思い、GuavaのCacheBuilderで期限付きでキャッシュすることにした。
ソースコード
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
public class RequestBasedKeycloakConfigResolver implements KeycloakConfigResolver {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final LoadingCache<String, KeycloakDeployment> keycloakDeploymentCache;
public RequestBasedKeycloakConfigResolver() {
keycloakDeploymentCache = CacheBuilder
.newBuilder()
.refreshAfterWrite(10, TimeUnit.MINUTES)
.build(
new CacheLoader<String, KeycloakDeployment>(){
@Override
public KeycloakDeployment load(String realm) throws Exception {
return loadKeycloakDeployment(realm);
}
}
);
}
@Value("${keycloak.auth-server-url}")
private String authServerUrl;
@Value("${keycloak.resource}")
private String resource;
@Override
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
String realm = request.getHeader("project-id");
try {
return keycloakDeploymentCache.get(realm);
} catch (ExecutionException ex) {
logger.error(ex.getMessage());
return loadKeycloakDeployment(realm);
}
}
private KeycloakDeployment loadKeycloakDeployment(String realm) {
AdapterConfig cfg = new AdapterConfig();
cfg.setRealm(realm);
cfg.setAuthServerUrl(authServerUrl);
cfg.setResource(resource);
return KeycloakDeploymentBuilder.build(cfg);
}
}
###まとめ
Keycloakは、認証方法などをレルム単位で定義することが可能であるので、リクエストヘッダーにレルム名を埋め込んでKeycloakConfigResolverでリクエスト毎にレルムを切り替えることにより複数プロジェクトに対応することができた。リクエストごとにKeycloakのセキュリティ設定を切り替えるのはパフォーマンス上の心配があったので期限付きのキャッシュでとりあえず10分間保持することにした。
###参考資料