Keycloakでマルチテナント

### はじめに
複数プロジェクトでマイクロサービスを利用するためにKeycloakを利用してマルチテナントを実現してみた。
### 試したこと
[本家マニュアル](https://www.keycloak.org/docs/2.5/securing_apps/topics/oidc/java/multi-tenancy.html)を見ると、KeycloakConfigResolverインターフェースのresolve()メソッド使えばできそうであるが、複数プロジェクトのKeycloakコンフィグをJsonファイルで用意するのは勘弁して欲しいと思い、さらに調べるとAdapterConfigを引数にしてKeycloakコンフィグを動的に生成できることが分かった。auth_server_urlとresourceは共通なのでapplication.ymlで設定し、リクエストヘッダーに埋め込まれたプロジェクトIDをKeycloakのレルム名にすればマルチテナントを実現できる。
しかし、リスエストごとにKeycloakコンフィグを生成するのもパフォーマンス上問題になると思い、[GuavaのCacheBuilder](https://github.com/google/guava/wiki/CachesExplained)で期限付きでキャッシュすることにした。
### ソースコード
```java:RequestBasedKeycloakConfigResolver.java
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分間保持することにした。

###参考資料

* [Keycloakマニュアル](https://www.keycloak.org/docs/1.9/securing_apps_guide/topics/oidc/java/spring-security-adapter.html)
* [Guava期限付きキャシュ](https://github.com/google/guava/wiki/CachesExplained)