Help us understand the problem. What is going on with this article?

【2019年10月版】Quarkus の REST API を KeyCloak で SSO するデモを docker-compose で動かしてみた。

More than 1 year has passed since last update.

Quarkus + KeyCloakの SSO 連携が簡単らしい。

Quarkus は JBoss 御謹製のK8sネイティブなマイクロサービスが素早く作れるフレームワークでございまして GraalVM でのビルドが簡単にできるらしいのでちょっと注目をしておりましたところ、以下のような記事を見つけました。

Quarkus は RedHat 社が開発しており、同社製または同社がスポンサーの OSS 製品(KeyCloak、Hibernate)との連携を前提としている。

あらそうなの?KeyCloakの代替になる SSO の OSS はほぼないのでKeyCloakと接続前提なのは全然、問題なしでございますよ~、ということで早速ググってみたところ、あっさりドンピシャなサンプルを発見いたしました。

ただしこちらの手順では JDK やら maven などの物騒なブツが必要なので、ちょっと Docker さんところでお願いしたいなぁ・・・というわけで、いつもの docker-compose を使ってさくっと起動するように調整してみました。

0. 準備

dockerdocker-compose を使用します。こちらのご用意をお願いいたします。

続いてソースコードを git clone してきます。

$ git clone git@github.com:yuhaibohotmail/quarkus-keycloak-demo.git
...
$ cd quarkus-keycloak-demo

クローンが終わったら中に入ります。

以降の手順では quarkus-keycloak-demo 内での作業となります。

1. Quarkus サンプルのビルド用イメージを準備

readme.md の "Build" および "Run with Remote Debug" の記述が Quarkus 側のサンプルプログラムのビルド & 起動コマンドとなっております。
まず、ビルドするための環境を Dockerイメージで用意します。

Dockerfile
FROM maven

WORKDIR /tmp/build
ADD . /tmp/build

RUN mvn clean package -DskipTests

これでこのイメージの中にはビルドした jar が入っている、ということになります。
続いてこちらを以下のように docker-compose.yml から起動するようにします。

docker-compose.yml
version : "3"
services:
  quarkus:
    build:
      context: .
    ports:
      - 8787:8787
      - 8082:8082
    entrypoint: >
      java
      -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:8787
      -jar target/quarkus-keycloak-demo-1.0.0-SNAPSHOT-runner.jar
      ./target/classes
      ./target/wiring-devmode
      ./target/transformer-cache
    tty: true

この Quarkus のサーバーは 8082 で動くように設定されているので、リモートデバッグ用の 8787 と 8082 は外向けにポートを開けておきます。(今回のデモでは必要ないですが。。。)

まだ、ビルドしてはいけません! clone してきたこのプロジェクトには ".dockerignore" ファイルがあります!これをチェックしてからでないと危険です!(・・・はい、数時間ハマりました。)

.dockerignore
*                     # <- これを削除するべし!!
!target/*-runner
!target/*-runner.jar
!target/lib/*

というわけで、".dockerignore" の * は削除しておいてください。

そしてビルドはまだ!です。

2. KeyCloak サービスの追加

続いて、上記の docker-compose.yml に KeyCloak 用のサービスを追加します。
readme.md の Run Keycloak の箇所も Docker コマンドで KeyCloak を起動する手順ですのでこちらをそのまま docker-compose 用に書き換えます。

docker-compose.yml
...(上記の続き)
  keycloak:
    image: jboss/keycloak:5.0.0
    container_name: keycloak
    restart: always
    ports:
      - 8180:8180
    environment:
      - KEYCLOAK_USER=admin
      - KEYCLOAK_PASSWORD=admin
    volumes:
      - ./quarkus-quickstart-realm.json:/config/quarkus-quickstart-realm.json
    command: >
      -b 0.0.0.0
      -Djboss.http.port=8180
      -Dkeycloak.migration.action=import
      -Dkeycloak.migration.provider=singleFile
      -Dkeycloak.migration.file=/config/quarkus-quickstart-realm.json
      -Dkeycloak.migration.strategy=OVERWRITE_EXISTING
    tty: true

はい、こちらは簡単ですね~。

3. Quarkus、KeyCloak の連携設定を修正

さて、双方の連携する設定を見直す必要があります。元の手順では Quarkus を実機で動かしているため localhost で問題なく動いておりますが、Quarkus をコンテナ化してしまったので、Quarkus -> KeyCloak のリクエストが localhost じゃ名前解決できなくなっております。

また、docker-compose.yml で設定したサービス名で名前解決したいので、docker-compose のネットワークに参加したコンテナ内でデモを動かしてみたいと思います。

というわけで、すべて localhost で指定していた箇所をそれぞれのサービス名に変更する必要があります。

3-1. Quarkus の設定

まずは、Quarkus から Keycloak を参照する設定は以下です。

src/main/resources/application.properties
# Configuration file
quarkus.http.port=8082

# MP-JWT Config
mp.jwt.verify.publickey.location=http://keycloak:8180/auth/realms/quarkus-quickstart/protocol/openid-connect/certs
mp.jwt.verify.issuer=http://keycloak:8180/auth/realms/quarkus-quickstart
quarkus.smallrye-jwt.auth-mechanism=MP-JWT
quarkus.smallrye-jwt.realmName=quarkus-keycloak-demo
quarkus.smallrye-jwt.enabled=true

上記ファイルの localhost:8180keycloak:8180 に修正いたしました。

3-2. KeyCloak の設定

KeyCloak の設定を quarkus-quickstart-realm.json で投入していますが、ここでは Quarkus 側のアドレスが、localhost:8080 となっています。(ポートも違うし。。。)
これを修正していきます。

quarkus-quickstart-realm.json
...
      "clientId": "quarkus-front",
      "rootUrl": "http://quarkus:8082",
      "adminUrl": "http://quarkus:8082",
      "surrogateAuthRequired": false,
      "enabled": true,
      "clientAuthenticatorType": "client-secret",
      "secret": "**********",
      "redirectUris": [
        "http://quarkus:8082/*"
      ],
      "webOrigins": [
        "http://quarkus:8082"
      ],
      "notBefore": 0,
...

localhost:8080quarkus:8082 に修正いたしました。

3. デモ実行用コンテナの追加

最後にデモを実行するための空っぽのコンテナを追加いたします。

docker-compose.yml
... (上記の続き)
  demo:
    image: ubuntu
    volumes:
      - ./retrieve.sh:/retrieve.sh

で、"retrieve.sh" には readme.md の "Retrieve Tokens" の手順をそのままコピペしておきます。が、ここも localhost はそれぞれのサービス名に修正します。

retrieve.sh
#!/bin/bash

KC_CLIENT_ID=quarkus-front
KC_ISSUER=http://keycloak:8180/auth/realms/quarkus-quickstart

# Simple test user
KC_USERNAME=test
KC_PASSWORD=test

KC_RESPONSE=$( \
curl \
  -d "client_id=$KC_CLIENT_ID" \
  -d "username=$KC_USERNAME" \
  -d "password=$KC_PASSWORD" \
  -d "grant_type=password" \
  "$KC_ISSUER/protocol/openid-connect/token" \
)
echo $KC_RESPONSE | jq -C .

KC_ACCESS_TOKEN=$(echo $KC_RESPONSE | jq -r .access_token)

# Try to call endpoints - /data/user should work, /data/admin should fail
curl -v -H "Authorization: Bearer $KC_ACCESS_TOKEN" http://quarkus:8082/data/user
curl -v -H "Authorization: Bearer $KC_ACCESS_TOKEN" http://quarkus:8082/data/admin

# Simple admin user
KC_USERNAME=admin
KC_PASSWORD=test

KC_RESPONSE=$( \
curl \
  -d "client_id=$KC_CLIENT_ID" \
  -d "username=$KC_USERNAME" \
  -d "password=$KC_PASSWORD" \
  -d "grant_type=password" \
  "$KC_ISSUER/protocol/openid-connect/token" \
)
echo $KC_RESPONSE | jq -C .

KC_ACCESS_TOKEN=$(echo $KC_RESPONSE | jq -r .access_token)

# Try to call endpoints - /data/user and /data/admin should work both
curl -v -H "Authorization: Bearer $KC_ACCESS_TOKEN" http://quarkus:8082/data/admin
curl -v -H "Authorization: Bearer $KC_ACCESS_TOKEN" http://quarkus:8082/data/user

4. ビルド&実行

さて、準備が整ったところで docker-compose を起動します。初回は自動的にビルドも実行されるので up だけでOKです。

$ docker-compose up -d
...

ビルドを再度、実行したい場合は build します。

$ docker-compose build
...

5. デモ用コンテナに入って準備・・・

デモ実行用の demo コンテナはエントリーポイントを置いてないのでさっさと終わってしまいますので、exec コマンドではなく、直接 run で中に入ります。

$ docker-compose run demo
root@xxxxxx:/#

で、今回は Ubuntu のすっぴんイメージを使ってしまったので、curljq もこの中には入っていません。(ちょっと手抜きすぎた。。。)
皆様におかれましては是非とも jq 導入済みのイメージをご利用ください。今回は apt しちゃいます。

/# apt update && apt upgrade -y
...
/# apt install curl jq -y
...

curljq コマンドが入ったら retrieve.sh を叩いてみましょう。

6. デモプログラムの実行

retrieve.sh に記載した手順はちょっとストレートすぎるので、readme.md の Demo セクションに目を通してみましょう。

Make a request as user test and call the http://localhost:8082/data/user endpoint, (see Retrieve Tokens)
This will output

data for user "test"

Calling the http://localhost:8082/data/admin endpoint correctly fails with:

Access forbidden: role not allowed%

Make request as user admin and call the http://localhost:8082/data/user endpoint again
This will output:

data for user "admin"

calling http://localhost:8082/data/admin endpoint succeeds now

data for admin "admin"

要は・・・
1. "test" ユーザーでアクセストークン取得して"/user"にアクセスすると data for user "test" と返ってくるが、"/admin" にアクセスすると "Access forbidden" で怒られる。
2. "admin" ユーザーでアクセストークン取得して"/user"にアクセスすると data for user "admin" と返ってくるし、"/admin" にアクセスしてもちゃんと data for admin "admin" と返ってくる。

・・・ということですね!(そのまんま)
ん~?2.の手順の説明と curl でアクセスするパスが逆ですね・・・ここはちょっと気を付けてみましょう。

では、さっそくデモバッチを流してみます。ログのコメントは分かりやすいように追記しております。

$ ./retrieve.sh
# ↓ まずは "test" ユーザーでKeyCloakにログインし、アクセストークンを取得する
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2621  100  2550  100    71   5730    159 --:--:-- --:--:-- --:--:--  5889
# ↓ は取得したアクセストークンの表示
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ1aW81bFppVU9tN19RZ1JISTRYOWpwRXFlVkN3THBfekZMOWxUMWJ5TkR3In0.eyJqdGkiOiJiZGVjNTIyZC1lNjQ1LTRmY2YtYTY4Zi1iMTEzODJhYTJjOWQiLCJleHAiOjE1NzIzMzQ5MDMsIm5iZiI6MCwiaWF0IjoxNTcyMzM0NjAzLCJpc3MiOiJodHRwOi8va2V5Y2xvYWs6ODE4MC9hdXRoL3JlYWxtcy9xdWFya3VzLXF1aWNrc3RhcnQiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiYTE5YjJhZmMtZTk2ZS00OTM5LTgyYmYtYWE0YjU4OWRlMTM2IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoicXVhcmt1cy1mcm9udCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6ImJkYTQwNjg3LWM2NjMtNGEzZS04MjA0LWVhYTM2ZWM0NDUyZiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL3F1YXJrdXM6ODA4MiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsInVzZXIiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJUaGVvIFRlc3RlciIsImdyb3VwcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZXN0IiwiZ2l2ZW5fbmFtZSI6IlRoZW8iLCJmYW1pbHlfbmFtZSI6IlRlc3RlciIsImVtYWlsIjoidGVzdGVyQGxvY2FsaG9zdCJ9.XHPcdZ9Zd-lBRiXWU4KHmzhe6d_EtNsckjYh65tq0tDU1C5w5dX-K5PpR-zQveBHmaFIjKMtpeunQtR4iIil5oRskUpok7kCHbBRlbpOt_padLIsgNAy2TddFRHS0jlxsVMnigqGKmD6TSAA86e1MM3xrP2VxQKyVRiRQA9_m67mP99fUBsO13j5oZ2VcPz_WAvfFmhgIeTAi-OV0LCUUIpnudB3IHHUQXRazZUbXVr00c_TryWSf8-__TProbGZ-B8pmPh8jfOfTyZQ1DKy68rBNy2AcEYe1pfnVvK6cMMtqFvabdNqgvh83NZx20XR4LHAPauVbG4UZlnNILk5Dg",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI1ZjA0YmYwOC0yODMzLTRlN2EtOTg1MC0yZmVmNWJjYmY3YzYifQ.eyJqdGkiOiIyNGNiNDdmNS00ZWU5LTQ4YWMtODVlMS0xYzJjNjc4ZjQ1NGEiLCJleHAiOjE1NzIzMzY0MDMsIm5iZiI6MCwiaWF0IjoxNTcyMzM0NjAzLCJpc3MiOiJodHRwOi8va2V5Y2xvYWs6ODE4MC9hdXRoL3JlYWxtcy9xdWFya3VzLXF1aWNrc3RhcnQiLCJhdWQiOiJodHRwOi8va2V5Y2xvYWs6ODE4MC9hdXRoL3JlYWxtcy9xdWFya3VzLXF1aWNrc3RhcnQiLCJzdWIiOiJhMTliMmFmYy1lOTZlLTQ5MzktODJiZi1hYTRiNTg5ZGUxMzYiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoicXVhcmt1cy1mcm9udCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6ImJkYTQwNjg3LWM2NjMtNGEzZS04MjA0LWVhYTM2ZWM0NDUyZiIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSJ9._jkMbiSCk7j3qogzWhUzY8G5piVCxE05QLjXuaCHouA",
  "token_type": "bearer",
  "not-before-policy": 0,
  "session_state": "bda40687-c663-4a3e-8204-eaa36ec4452f",
  "scope": "email profile"
}
# ↓ 上記のアクセストークンで /data/user にアクセス
*   Trying 172.28.0.3...
* TCP_NODELAY set
* Connected to quarkus (172.28.0.3) port 8082 (#0)
> GET /data/user HTTP/1.1
> Host: quarkus:8082
> User-Agent: curl/7.58.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ1aW81bFppVU9tN19RZ1JISTRYOWpwRXFlVkN3THBfekZMOWxUMWJ5TkR3In0.eyJqdGkiOiJiZGVjNTIyZC1lNjQ1LTRmY2YtYTY4Zi1iMTEzODJhYTJjOWQiLCJleHAiOjE1NzIzMzQ5MDMsIm5iZiI6MCwiaWF0IjoxNTcyMzM0NjAzLCJpc3MiOiJodHRwOi8va2V5Y2xvYWs6ODE4MC9hdXRoL3JlYWxtcy9xdWFya3VzLXF1aWNrc3RhcnQiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiYTE5YjJhZmMtZTk2ZS00OTM5LTgyYmYtYWE0YjU4OWRlMTM2IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoicXVhcmt1cy1mcm9udCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6ImJkYTQwNjg3LWM2NjMtNGEzZS04MjA0LWVhYTM2ZWM0NDUyZiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL3F1YXJrdXM6ODA4MiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsInVzZXIiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJUaGVvIFRlc3RlciIsImdyb3VwcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZXN0IiwiZ2l2ZW5fbmFtZSI6IlRoZW8iLCJmYW1pbHlfbmFtZSI6IlRlc3RlciIsImVtYWlsIjoidGVzdGVyQGxvY2FsaG9zdCJ9.XHPcdZ9Zd-lBRiXWU4KHmzhe6d_EtNsckjYh65tq0tDU1C5w5dX-K5PpR-zQveBHmaFIjKMtpeunQtR4iIil5oRskUpok7kCHbBRlbpOt_padLIsgNAy2TddFRHS0jlxsVMnigqGKmD6TSAA86e1MM3xrP2VxQKyVRiRQA9_m67mP99fUBsO13j5oZ2VcPz_WAvfFmhgIeTAi-OV0LCUUIpnudB3IHHUQXRazZUbXVr00c_TryWSf8-__TProbGZ-B8pmPh8jfOfTyZQ1DKy68rBNy2AcEYe1pfnVvK6cMMtqFvabdNqgvh83NZx20XR4LHAPauVbG4UZlnNILk5Dg
> 
< HTTP/1.1 200 OK
< Connection: keep-alive
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 20
< Date: Tue, 29 Oct 2019 07:36:43 GMT
< 
* Connection #0 to host quarkus left intact
data for user "test"
# ↑ ログイン成功!ちゃんと "test" ユーザーが認識されました。
# ↓ /data/admin にアクセスしてみると・・・
*   Trying 172.28.0.3...
* TCP_NODELAY set
* Connected to quarkus (172.28.0.3) port 8082 (#0)
> GET /data/admin HTTP/1.1
> Host: quarkus:8082
> User-Agent: curl/7.58.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ1aW81bFppVU9tN19RZ1JISTRYOWpwRXFlVkN3THBfekZMOWxUMWJ5TkR3In0.eyJqdGkiOiJiZGVjNTIyZC1lNjQ1LTRmY2YtYTY4Zi1iMTEzODJhYTJjOWQiLCJleHAiOjE1NzIzMzQ5MDMsIm5iZiI6MCwiaWF0IjoxNTcyMzM0NjAzLCJpc3MiOiJodHRwOi8va2V5Y2xvYWs6ODE4MC9hdXRoL3JlYWxtcy9xdWFya3VzLXF1aWNrc3RhcnQiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiYTE5YjJhZmMtZTk2ZS00OTM5LTgyYmYtYWE0YjU4OWRlMTM2IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoicXVhcmt1cy1mcm9udCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6ImJkYTQwNjg3LWM2NjMtNGEzZS04MjA0LWVhYTM2ZWM0NDUyZiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL3F1YXJrdXM6ODA4MiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsInVzZXIiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJUaGVvIFRlc3RlciIsImdyb3VwcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZXN0IiwiZ2l2ZW5fbmFtZSI6IlRoZW8iLCJmYW1pbHlfbmFtZSI6IlRlc3RlciIsImVtYWlsIjoidGVzdGVyQGxvY2FsaG9zdCJ9.XHPcdZ9Zd-lBRiXWU4KHmzhe6d_EtNsckjYh65tq0tDU1C5w5dX-K5PpR-zQveBHmaFIjKMtpeunQtR4iIil5oRskUpok7kCHbBRlbpOt_padLIsgNAy2TddFRHS0jlxsVMnigqGKmD6TSAA86e1MM3xrP2VxQKyVRiRQA9_m67mP99fUBsO13j5oZ2VcPz_WAvfFmhgIeTAi-OV0LCUUIpnudB3IHHUQXRazZUbXVr00c_TryWSf8-__TProbGZ-B8pmPh8jfOfTyZQ1DKy68rBNy2AcEYe1pfnVvK6cMMtqFvabdNqgvh83NZx20XR4LHAPauVbG4UZlnNILk5Dg
> 
< HTTP/1.1 403 Forbidden
< Connection: keep-alive
< Content-Type: text/html;charset=UTF-8
< Content-Length: 34
< Date: Tue, 29 Oct 2019 07:36:43 GMT
< 
* Connection #0 to host quarkus left intact
Access forbidden: role not allowed
# ↑ アクセスが拒否されました。
# ↓ 続いて admin ユーザーのトークンを取得
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2652  100  2580  100    72  20640    576 --:--:-- --:--:-- --:--:-- 21216
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ1aW81bFppVU9tN19RZ1JISTRYOWpwRXFlVkN3THBfekZMOWxUMWJ5TkR3In0.eyJqdGkiOiIyNWRjOTI1Zi03YWNjLTRmMmMtOWI4ZS0zNjBkOWU0YmFiMzIiLCJleHAiOjE1NzIzMzQ5MDMsIm5iZiI6MCwiaWF0IjoxNTcyMzM0NjAzLCJpc3MiOiJodHRwOi8va2V5Y2xvYWs6ODE4MC9hdXRoL3JlYWxtcy9xdWFya3VzLXF1aWNrc3RhcnQiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMDc3M2IxZTYtYWMwMy00Y2VjLTllNjctNTYwNzFhNzJlOTlkIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoicXVhcmt1cy1mcm9udCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6IjQ5ZDNkNTU0LTg3ZjgtNGI0Zi05ZTkzLWQwNDM2MTFiNzBiMiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL3F1YXJrdXM6ODA4MiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkFybm8gQWRtaW4iLCJncm91cHMiOlsib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsImdpdmVuX25hbWUiOiJBcm5vIiwiZmFtaWx5X25hbWUiOiJBZG1pbiIsImVtYWlsIjoiYWRtaW5AbG9jYWxob3N0In0.nUT8IU7jLNuN_00-yN8BccXAdYedJIyW-_dNY5ICPJR8bgWoie10TJpiwGdij6cOc5TsecSjuT6BvivqXgwbbOyTnh3Fs2ATXtbVFJI2WKfP4f2bbie_fBvRCD2wRfkpR5wGt3q9eceXwMOp0fh2ExvjJ3HgY_zhmerQpyZljeU6-YiF8JptiKRCrmD-USwmc0onQ50KKjWAFrhUYKcOAx7XTT2rHFspJrVPmbOhaSV6LCan4cqalbnfIgwoU8hVssvvW6DLdIEDjwEgrksmmKpT6nkl4sTMqFXVcbUA_UBhEopAEzts9DKEJZAGWXLqz-prt3wzFJm-Jws__2njog",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI1ZjA0YmYwOC0yODMzLTRlN2EtOTg1MC0yZmVmNWJjYmY3YzYifQ.eyJqdGkiOiI5MGEzMDVkMi1kMzcyLTRjOTItOWJjMy0wNDA5OWIyYjg4YzIiLCJleHAiOjE1NzIzMzY0MDMsIm5iZiI6MCwiaWF0IjoxNTcyMzM0NjAzLCJpc3MiOiJodHRwOi8va2V5Y2xvYWs6ODE4MC9hdXRoL3JlYWxtcy9xdWFya3VzLXF1aWNrc3RhcnQiLCJhdWQiOiJodHRwOi8va2V5Y2xvYWs6ODE4MC9hdXRoL3JlYWxtcy9xdWFya3VzLXF1aWNrc3RhcnQiLCJzdWIiOiIwNzczYjFlNi1hYzAzLTRjZWMtOWU2Ny01NjA3MWE3MmU5OWQiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoicXVhcmt1cy1mcm9udCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6IjQ5ZDNkNTU0LTg3ZjgtNGI0Zi05ZTkzLWQwNDM2MTFiNzBiMiIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsImFkbWluIiwidW1hX2F1dGhvcml6YXRpb24iLCJ1c2VyIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIn0.GnGiVju0yuzzrrfpM_6F37tNmJCfVpOynnmfIveoQBs",
  "token_type": "bearer",
  "not-before-policy": 0,
  "session_state": "49d3d554-87f8-4b4f-9e93-d043611b70b2",
  "scope": "email profile"
}
# ↓ "admin" トークンで /data/admin にアクセスしてみます。
*   Trying 172.28.0.3...
* TCP_NODELAY set
* Connected to quarkus (172.28.0.3) port 8082 (#0)
> GET /data/admin HTTP/1.1
> Host: quarkus:8082
> User-Agent: curl/7.58.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ1aW81bFppVU9tN19RZ1JISTRYOWpwRXFlVkN3THBfekZMOWxUMWJ5TkR3In0.eyJqdGkiOiIyNWRjOTI1Zi03YWNjLTRmMmMtOWI4ZS0zNjBkOWU0YmFiMzIiLCJleHAiOjE1NzIzMzQ5MDMsIm5iZiI6MCwiaWF0IjoxNTcyMzM0NjAzLCJpc3MiOiJodHRwOi8va2V5Y2xvYWs6ODE4MC9hdXRoL3JlYWxtcy9xdWFya3VzLXF1aWNrc3RhcnQiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMDc3M2IxZTYtYWMwMy00Y2VjLTllNjctNTYwNzFhNzJlOTlkIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoicXVhcmt1cy1mcm9udCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6IjQ5ZDNkNTU0LTg3ZjgtNGI0Zi05ZTkzLWQwNDM2MTFiNzBiMiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL3F1YXJrdXM6ODA4MiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkFybm8gQWRtaW4iLCJncm91cHMiOlsib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsImdpdmVuX25hbWUiOiJBcm5vIiwiZmFtaWx5X25hbWUiOiJBZG1pbiIsImVtYWlsIjoiYWRtaW5AbG9jYWxob3N0In0.nUT8IU7jLNuN_00-yN8BccXAdYedJIyW-_dNY5ICPJR8bgWoie10TJpiwGdij6cOc5TsecSjuT6BvivqXgwbbOyTnh3Fs2ATXtbVFJI2WKfP4f2bbie_fBvRCD2wRfkpR5wGt3q9eceXwMOp0fh2ExvjJ3HgY_zhmerQpyZljeU6-YiF8JptiKRCrmD-USwmc0onQ50KKjWAFrhUYKcOAx7XTT2rHFspJrVPmbOhaSV6LCan4cqalbnfIgwoU8hVssvvW6DLdIEDjwEgrksmmKpT6nkl4sTMqFXVcbUA_UBhEopAEzts9DKEJZAGWXLqz-prt3wzFJm-Jws__2njog
> 
< HTTP/1.1 200 OK
< Connection: keep-alive
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 22
< Date: Tue, 29 Oct 2019 07:36:43 GMT
< 
* Connection #0 to host quarkus left intact
data for admin "admin"
# ↑ アクセスできました!そしてユーザーが"admin" であることが確認できています。
# ↓ 続いて /data/user
*   Trying 172.28.0.3...
* TCP_NODELAY set
* Connected to quarkus (172.28.0.3) port 8082 (#0)
> GET /data/user HTTP/1.1
> Host: quarkus:8082
> User-Agent: curl/7.58.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ1aW81bFppVU9tN19RZ1JISTRYOWpwRXFlVkN3THBfekZMOWxUMWJ5TkR3In0.eyJqdGkiOiIyNWRjOTI1Zi03YWNjLTRmMmMtOWI4ZS0zNjBkOWU0YmFiMzIiLCJleHAiOjE1NzIzMzQ5MDMsIm5iZiI6MCwiaWF0IjoxNTcyMzM0NjAzLCJpc3MiOiJodHRwOi8va2V5Y2xvYWs6ODE4MC9hdXRoL3JlYWxtcy9xdWFya3VzLXF1aWNrc3RhcnQiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMDc3M2IxZTYtYWMwMy00Y2VjLTllNjctNTYwNzFhNzJlOTlkIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoicXVhcmt1cy1mcm9udCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6IjQ5ZDNkNTU0LTg3ZjgtNGI0Zi05ZTkzLWQwNDM2MTFiNzBiMiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL3F1YXJrdXM6ODA4MiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkFybm8gQWRtaW4iLCJncm91cHMiOlsib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsImdpdmVuX25hbWUiOiJBcm5vIiwiZmFtaWx5X25hbWUiOiJBZG1pbiIsImVtYWlsIjoiYWRtaW5AbG9jYWxob3N0In0.nUT8IU7jLNuN_00-yN8BccXAdYedJIyW-_dNY5ICPJR8bgWoie10TJpiwGdij6cOc5TsecSjuT6BvivqXgwbbOyTnh3Fs2ATXtbVFJI2WKfP4f2bbie_fBvRCD2wRfkpR5wGt3q9eceXwMOp0fh2ExvjJ3HgY_zhmerQpyZljeU6-YiF8JptiKRCrmD-USwmc0onQ50KKjWAFrhUYKcOAx7XTT2rHFspJrVPmbOhaSV6LCan4cqalbnfIgwoU8hVssvvW6DLdIEDjwEgrksmmKpT6nkl4sTMqFXVcbUA_UBhEopAEzts9DKEJZAGWXLqz-prt3wzFJm-Jws__2njog
> 
< HTTP/1.1 200 OK
< Connection: keep-alive
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 21
< Date: Tue, 29 Oct 2019 07:36:43 GMT
< 
* Connection #0 to host quarkus left intact
data for user "admin"
# ↑ こちらもアクセスできました!ユーザーが "admin" であることが認識できています。

うまく動いているようですね!

7. Quarkus の実装を確認

デモで無事に Quarkus と KeyCloak の連動が出来ていることが確認できましたが実装はどうなっているのでしょう?

実は非常にコード量が少ないので以下にこのデモAPIのプログラムを全文、載せます。

src/main/java/com/github/thomasdarimont/keycloak/DataResource.java
package com.github.thomasdarimont.keycloak;

import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;

import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.json.Json;
import javax.json.JsonString;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.Optional;

/**
 * Simple REST Resource that consumes information provided by a JWT token.
 *
 * Note that {@code RequestScoped} is explicitly needed here, since Quarkus changed the default
 * scope for JAX-RS Resources to be {@code ApplicationScoped}.
 *
 * See: https://github.com/quarkusio/quarkus/issues/1710
 */
@Path("/data")
@RequestScoped
public class DataResource {

    private static final JsonString ANOYNMOUS = Json.createValue("anonymous");

    @Inject
    @Claim("raw_token")
    String rawToken;

    @Inject
    @Claim(standard = Claims.sub)
    Optional<JsonString> subject;

    @Inject
    @Claim(standard = Claims.preferred_username)
    Optional<JsonString> currentUsername;

    @GET
    @Path("/user")
    @Produces(MediaType.TEXT_PLAIN)
    @RolesAllowed({"user"})
    public String userData() {
        return "data for user " + currentUsername.orElse(ANOYNMOUS);
    }

    @GET
    @Path("/admin")
    @Produces(MediaType.TEXT_PLAIN)
    @RolesAllowed({"admin"})
    public String adminData() {
        return "data for admin " + currentUsername.orElse(ANOYNMOUS);
    }
}

AOP というか JavaEE(?) のアノテーション + Injection が効いててとんでもなく簡単にロール制御が実現できていることが分かります。
メソッド内のコードではロールやログインに関わる記述はせずに、呼び出されたときのコードだけを書いていることが分かります。
ロール制御のアノテーションも @RolesAllowed だけで済んでいます。

あとは、KeyCloak を参照する設定を application.properties に記述するだけです。
これで Quarkus の SSO 対応は完了、です。

う~ん、非常に簡単というか、さすがですね。。。これは参ったなぁ。。。SSO 必要なマイクロサービスは Quarkus でOKじゃないか。。。

まとめ

Quarkus + KeyCloak のデモで API 側の SSO 対応が圧巻の簡潔さで連携取れてしまうことが確認できました。
アノテーションでどうにかする文化はやっぱり JavaEE すごいなぁ。。。

また、GraalVMでネイティブ化 + Cloud Run に挑戦された方も発見しました!

Cloud Run + Cloud SQL の環境でネイティブ化された Quarkus + Hibernate のサービスが動いてるなんて、胸熱・・・だし、MicroProfile 対応なので Swagger ドキュメントも出せちゃうし、Zipkin も対応してるはずですよね?!ということで以下の記事!

いやいや、周辺技術をいろいろ調べだすと発散してしまうので・・・本日は以上といたします!

追記: Github に上げました。

上記の修正を施したquarkus-keycloak-demoを githubにて公開いたしました。(forkして修正しただけだけど・・・)

koinori
最近の記事はタグが5つじゃ足りない。
pro-japan
さまざまな価値観を持ち、日々変化し続ける技術を探究し、チャレンジし続けることで、楽しみながら「あったらいいな」を創り上げ、実現します。
https://www.pro-japan.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away