はじめに
Quarkusのアプリケーションを作ったことが無かったので、エコーサーバを作りたいと思います。ただ、エコーサーバ作るだけだと面白くないので、JWT認証と合わせてやってみようかと思います。
以下のようなことを目標にします。
- Quarkusのプロジェクトの作成方法を知る
- Quarkusでエコーサーバがかけるようになる(ユニットテスト含む)
- Quarkusアプリケーションに対してJWT RBACをかけることができるようになる
基本的にはドキュメントのここの内容をなぞる感じでやろうと思います。
環境
$ uname -srvmpio Linux 5.4.0-42-generic #46-Ubuntu SMP Fri Jul 10 00:24:02 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.1 LTS Release: 20.04 Codename: focal $ java --version openjdk 15-ea 2020-09-15 OpenJDK Runtime Environment (build 15-ea+25-1229) OpenJDK 64-Bit Server VM (build 15-ea+25-1229, mixed mode, sharing) $ mvn -v Apache Maven 3.6.3 Maven home: /usr/share/maven Java version: 15-ea, vendor: Oracle Corporation, runtime: /home/someone/.sdkman/candidates/java/15.ea.25-open Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-42-generic", arch: "amd64", family: "unix"
プロジェクトの作成
作成方法は、Mavenアーキタイプを使う方法やIntelliJなどのIDEから作成する方法などいろいろあるようですがStart coding with code.quarkus.ioを使いたいと思います。 設定は以下のような感じ
依存はSmallRye JWT
だけを追加しました。
JAX-RS(RESTEasy)はデフォルトで入るみたいです。
ざっくりとできたものを眺めてみる
$ cd jwt-sample/ $ tree . ├── README.md ├── jwt-sample.iml ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main │ ├── docker │ │ ├── Dockerfile.fast-jar │ │ ├── Dockerfile.jvm │ │ └── Dockerfile.native │ ├── java │ │ └── dev │ │ └── hirooka │ │ └── ExampleResource.java │ └── resources │ ├── META-INF │ │ └── resources │ │ └── index.html │ └── application.properties └── test └── java └── dev └── hirooka ├── ExampleResourceTest.java └── NativeExampleResourceIT.java
初期状態では大きく以下のようなものが作成されていました。
hello
の文字列を返すリソースクラスとそのテスト- Dockerfile群
- pom.xml
DockerfileはJVM用とNativeImage用の2つのタイプで用意されているようです。
pomの依存とプラグインは以下のような感じにました。
<dependencies> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>io.quarkus</groupId> <artifactId>quarkus-maven-plugin</artifactId> <version>${quarkus-plugin.version}</version> <executions> <execution> <goals> <goal>build</goal> </goals> </execution> </executions> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>${compiler-plugin.version}</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>${surefire-plugin.version}</version> <configuration> <systemPropertyVariables> <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager> <maven.home>${maven.home}</maven.home> </systemPropertyVariables> </configuration> </plugin> </plugins>
RESTEasyとテスト系、そしてquarkus-smallrye-jwtの依存が初期状態で入ってました。
また、quarkusの起動とかに使われると思しきプラグインとテストの実行プラグインのmaven-surefire-plugin
が入ってます。
動かしてみるしてみる
$ ./mvnw quarkus:dev
以下のコマンドでdevモードでの起動が可能なようです。
$ ./mvnw quarkus:dev [INFO] Scanning for projects... [INFO] [INFO] -----------------------< dev.hirooka:jwt-sample >----------------------- [INFO] Building jwt-sample 1.0.0-SNAPSHOT [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- quarkus-maven-plugin:1.6.1.Final:dev (default-cli) @ jwt-sample --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] Copying 2 resources [INFO] Nothing to compile - all classes are up to date OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release. Listening for transport dt_socket at address: 5005 __ ____ __ _____ ___ __ ____ ______ --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ 2020-08-01 12:59:05,700 INFO [io.quarkus] (Quarkus Main Thread) jwt-sample 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.6.1.Final) started in 0.983s. Listening on: http://0.0.0.0:8080 2020-08-01 12:59:05,711 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated. 2020-08-01 12:59:05,711 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, mutiny, resteasy, security, smallrye-context-propagation, smallrye-jwt, vertx, vertx-web]
8080ポートで起動したようなので、curlを叩いてみます。
$ curl localhost:8080/hello hello
初期状態で作成されているExampleResourceがhello
の文字列を返してきますね。
エコーサーバを書いてみる
パスパラメータで受け取った値をそのまま返すだけのエコーサーバを作成します 以下のようなリソースクラスを作成します。
EchoResource.java
@Path("/echo") public class EchoResource { @GET @Path("{value}") @Produces(MediaType.TEXT_PLAIN) public String echo(@PathParam("value") String value){ return value; } }
ここは簡単なJAX-RSなので、Quarkus固有のところは無いかなと思います。
devモードだとアプリの再起動はいらなさそうなので、curlを叩いてみます。
$ curl localhost:8080/echo/ping ping
期待通り動いていますね。
リソースクラスのテストを書く
せっかくなんで、テストを追加しておきます。
import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.is; @QuarkusTest class EchoResourceTest { @Test void testEchoResource() { given().pathParam("value", "hello") .when().get("/echo/{value}") .then() .statusCode(200) .body(is("hello")); } }
@QuarkusTest
はJunit5のQuarkusの拡張を追加するためのアノテーションです。
その拡張であるQuarkusTestExtension
では、Quarkusアプリの起動とかテストに必要な諸々をやってくれてます。
また、テストはRestAssured
を使って書くようです。
JWT RBACをかける
アプリケーションとテストを書いたので、JWT認証をかけていきます。
今回、公開、非公開鍵の作成は行わず、公式サイトのチュートリアルの方で作成されているものを利用します。
まずは作成したリソースクラスを以下のように修正します。
@Path("/echo") @RequestScoped public class EchoResource { private final JsonWebToken jwt; static final Logger logger = Logger.getLogger(EchoResource.class); public EchoResource(JsonWebToken jwt) { this.jwt = jwt; } @GET @Path("{value}") @RolesAllowed({"Echoer", "Subscriber"}) @Produces(MediaType.TEXT_PLAIN) public String echo( @PathParam("value") String value ) { String issuer = jwt.getIssuer(); if (issuer != null) { logger.info(String.format("issuer name is %s", issuer)); } return value; } }
まず、@RequestScoped
の部分ですがQuarkusではDIのライフサイクルがデフォルトで@ApplicationScoped
になります。しかし、JWTはリクエストスコープである必要があるため、望まない挙動が起こることがあるため、@RequestScoped
をつけるようにしています。
つぎに、@RolesAllowed({"Echoer"})
の部分ですが、JSR 250のエンドポイントを保護するための一般的なアノテーションです。この場合呼び出し側がEchoer
というロールを持つ場合に置いてアクセスが可能となります。
JsonWebTokenインターフェースはjava.security.Principal
を継承しており、認証済みのトークン情報へのアクセスを提供しています。メソッドの部分では、このインターフェースを通して、クレイムをログ出力する処理を追加しています。
次に、JWT RBACに必要な設定をいくつか記述します。$srcRoot/src/main/resources/application.properties
に以下の設定を追加します。
mp.jwt.verify.publickey.location=META-INF/resources/publicKey.pem mp.jwt.verify.issuer=https://quarkus.io/using-jwt-rbac # quarkus.smallrye-jwt.enabled=true
quarkus.smallrye-jwt.enabled=true
でJWTの有効無効を操作できますが、デフォルトで有効となっているので、記述する必要はありません。 その他の設定も名前から推測できるかもしれませんが、
mp.jwt.verify.publickey.location
にはJWT発行者の公開鍵を登録し、mp.jwt.verify.issuer
の発行者のドメインを登録しておきます。
$srcRoot/src/main/resources/META-INF/resources/publicKey.pem
を作成しておきます。
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9 AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x nQIDAQAB -----END PUBLIC KEY-----
再度書きますと、この鍵は公式サイトのチュートリアルで作成されているものをそのまま利用しています。
ここまでで、アプリケーション側の準備が整ったので、一旦cURLlを叩いてみます。
$ curl -v localhost:8080/echo/hello * Trying 127.0.0.1:8080... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8080 (#0) > GET /echo/hello HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 401 Unauthorized < www-authenticate: Bearer {token} < Content-Length: 0 < * Connection #0 to host localhost left intact
401が帰っているので、きちんと認証が追加されているみたいですね。
それではこのメソッドにアクセスできるようにcurlにJWTを渡してみましょう。
利用するJsonClaimsは以下の通りとなります。
{ "iss": "https://hirooka.dev/jwt-rbac", "jti": "a-123", "sub": "jdoe-using-jwt-rbac", "upn": "henoheno@hirooka.dev", "preferred_username": "henoheno", "aud": "using-jwt-rbac", "birthdate": "1993-05-27", "groups": [ "Echoer" ] }
簡単に説明を表にまとめると以下の通りです。
クレイム名 | 説明 |
---|---|
iss | issはトークンの発行者を示しmp.jwt.verify.issuer の値と一致している必要があります |
upn | MicroProfile JWT RBAC specで定義されているクレイムで、Principalによって利用されます |
groups | JWTに付与されるトップレベルでのロールの一覧を定義します。 |
こいつを、以下の秘密鍵でJWTを作ります。
-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWK8UjyoHgPTLa PLQJ8SoXLLjpHSjtLxMqmzHnFscqhTVVaDpCRCb6e3Ii/WniQTWw8RA7vf4djz4H OzvlfBFNgvUGZHXDwnmGaNVaNzpHYFMEYBhE8VGGiveSkzqeLZI+Y02G6sQAfDtN qqzM/l5QX8X34oQFaTBW1r49nftvCpITiwJvWyhkWtXP9RP8sXi1im5Vi3dhupOh nelk5n0BfajUYIbfHA6ORzjHRbt7NtBl0L2J+0/FUdHyKs6KMlFGNw8O0Dq88qnM uXoLJiewhg9332W3DFMeOveel+//cvDnRsCRtPgd4sXFPHh+UShkso7+DRsChXa6 oGGQD3GdAgMBAAECggEAAjfTSZwMHwvIXIDZB+yP+pemg4ryt84iMlbofclQV8hv 6TsI4UGwcbKxFOM5VSYxbNOisb80qasb929gixsyBjsQ8284bhPJR7r0q8h1C+jY URA6S4pk8d/LmFakXwG9Tz6YPo3pJziuh48lzkFTk0xW2Dp4SLwtAptZY/+ZXyJ6 96QXDrZKSSM99Jh9s7a0ST66WoxSS0UC51ak+Keb0KJ1jz4bIJ2C3r4rYlSu4hHB Y73GfkWORtQuyUDa9yDOem0/z0nr6pp+pBSXPLHADsqvZiIhxD/O0Xk5I6/zVHB3 zuoQqLERk0WvA8FXz2o8AYwcQRY2g30eX9kU4uDQAQKBgQDmf7KGImUGitsEPepF KH5yLWYWqghHx6wfV+fdbBxoqn9WlwcQ7JbynIiVx8MX8/1lLCCe8v41ypu/eLtP iY1ev2IKdrUStvYRSsFigRkuPHUo1ajsGHQd+ucTDf58mn7kRLW1JGMeGxo/t32B m96Af6AiPWPEJuVfgGV0iwg+HQKBgQCmyPzL9M2rhYZn1AozRUguvlpmJHU2DpqS 34Q+7x2Ghf7MgBUhqE0t3FAOxEC7IYBwHmeYOvFR8ZkVRKNF4gbnF9RtLdz0DMEG 5qsMnvJUSQbNB1yVjUCnDAtElqiFRlQ/k0LgYkjKDY7LfciZl9uJRl0OSYeX/qG2 tRW09tOpgQKBgBSGkpM3RN/MRayfBtmZvYjVWh3yjkI2GbHA1jj1g6IebLB9SnfL WbXJErCj1U+wvoPf5hfBc7m+jRgD3Eo86YXibQyZfY5pFIh9q7Ll5CQl5hj4zc4Y b16sFR+xQ1Q9Pcd+BuBWmSz5JOE/qcF869dthgkGhnfVLt/OQzqZluZRAoGAXQ09 nT0TkmKIvlza5Af/YbTqEpq8mlBDhTYXPlWCD4+qvMWpBII1rSSBtftgcgca9XLB MXmRMbqtQeRtg4u7dishZVh1MeP7vbHsNLppUQT9Ol6lFPsd2xUpJDc6BkFat62d Xjr3iWNPC9E9nhPPdCNBv7reX7q81obpeXFMXgECgYEAmk2Qlus3OV0tfoNRqNpe Mb0teduf2+h3xaI1XDIzPVtZF35ELY/RkAHlmWRT4PCdR0zXDidE67L6XdJyecSt FdOUH8z5qUraVVebRFvJqf/oGsXc4+ex1ZKUTbY0wqY1y9E39yvB3MaTmZFuuqk8 f3cg+fr8aou7pr9SHhJlZCU= -----END PRIVATE KEY-----
詳細な、JWTの作成方法はここをご覧ください。
作成されたトークンは以下の通りになります。
(トークンの有効期限がきれていると思うので、自分で作成してください)
eyJraWQiOiIvcHJpdmF0ZUtleS5wZW0iLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2hpcm9va2EuZGV2L2p3dC1yYmFjIiwianRpIjoiYS0xMjMiLCJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwidXBuIjoiaGVub2hlbm9AaGlyb29rYS5kZXYiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJoZW5vaGVubyIsImF1ZCI6InVzaW5nLWp3dC1yYmFjIiwiYmlydGhkYXRlIjoiMTk5My0wNS0yNyIsImdyb3VwcyI6WyJFY2hvZXIiXSwiaWF0IjoxNTk2Mzc1NDEwLCJhdXRoX3RpbWUiOjE1OTYzNzU0MTAsImV4cCI6MTU5NjM3NTcxMH0.aGBodEwgJIJMbe36F9HRqeZtA2ZGTxbOvfyax7iqbbHpAO4RajtbkQOiPG6BXIDQUAV6lhnR6-SjHhWu1i9B1oHkUMQUKEA70C9wMeUQCk1Va_NWkjsMbUMv-n5inkC2RLzBraNg91MI7d9HqawzC4oOQl1MB2oSDsaQIKQs_oy8lMmBvGYFi9TkiZxrbkpeiyZi5eFGCxr5m7yonEYkPz8c6-C5FfjInyaCU8v-OTHKQRJN0QSQckkUsJuZdfc1k-tTYmUfyTqJpkFpukCq6Lfob6qELpmYB3Zc5UQ8I-__OcFFT7DOjRt7HpjL8xdh1RHIz4pEjJY8J6NGj6XTtw
最後に作成したトークンをつかってエンドポイントにアクセスしてみます。
$ curl -v -H "Authorization: Bearer eyJraWQiOiIvcHJpdmF0ZUtleS5wZW0iLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2hpcm9va2EuZGV2L2p3dC1yYmFjIiwianRpIjoiYS0xMjMiLCJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwidXBuIjoiaGVub2hlbm9AaGlyb29rYS5kZXYiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJoZW5vaGVubyIsImF1ZCI6InVzaW5nLWp3dC1yYmFjIiwiYmlydGhkYXRlIjoiMTk5My0wNS0yNyIsImdyb3VwcyI6WyJFY2hvZXIiXSwiaWF0IjoxNTk2Mzc1NDEwLCJhdXRoX3RpbWUiOjE1OTYzNzU0MTAsImV4cCI6MTU5NjM3NTcxMH0.aGBodEwgJIJMbe36F9HRqeZtA2ZGTxbOvfyax7iqbbHpAO4RajtbkQOiPG6BXIDQUAV6lhnR6-SjHhWu1i9B1oHkUMQUKEA70C9wMeUQCk1Va_NWkjsMbUMv-n5inkC2RLzBraNg91MI7d9HqawzC4oOQl1MB2oSDsaQIKQs_oy8lMmBvGYFi9TkiZxrbkpeiyZi5eFGCxr5m7yonEYkPz8c6-C5FfjInyaCU8v-OTHKQRJN0QSQckkUsJuZdfc1k-tTYmUfyTqJpkFpukCq6Lfob6qELpmYB3Zc5UQ8I-__OcFFT7DOjRt7HpjL8xdh1RHIz4pEjJY8J6NGj6XTtw" localhost:8080/echo/hello * Trying 127.0.0.1:8080... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8080 (#0) > GET /echo/hello HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.68.0 > Accept: */* > Authorization: Bearer eyJraWQiOiIvcHJpdmF0ZUtleS5wZW0iLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2hpcm9va2EuZGV2L2p3dC1yYmFjIiwianRpIjoiYS0xMjMiLCJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwidXBuIjoiaGVub2hlbm9AaGlyb29rYS5kZXYiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJoZW5vaGVubyIsImF1ZCI6InVzaW5nLWp3dC1yYmFjIiwiYmlydGhkYXRlIjoiMTk5My0wNS0yNyIsImdyb3VwcyI6WyJFY2hvZXIiXSwiaWF0IjoxNTk2Mzc1NDEwLCJhdXRoX3RpbWUiOjE1OTYzNzU0MTAsImV4cCI6MTU5NjM3NTcxMH0.aGBodEwgJIJMbe36F9HRqeZtA2ZGTxbOvfyax7iqbbHpAO4RajtbkQOiPG6BXIDQUAV6lhnR6-SjHhWu1i9B1oHkUMQUKEA70C9wMeUQCk1Va_NWkjsMbUMv-n5inkC2RLzBraNg91MI7d9HqawzC4oOQl1MB2oSDsaQIKQs_oy8lMmBvGYFi9TkiZxrbkpeiyZi5eFGCxr5m7yonEYkPz8c6-C5FfjInyaCU8v-OTHKQRJN0QSQckkUsJuZdfc1k-tTYmUfyTqJpkFpukCq6Lfob6qELpmYB3Zc5UQ8I-__OcFFT7DOjRt7HpjL8xdh1RHIz4pEjJY8J6NGj6XTtw > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Content-Length: 5 < Content-Type: text/plain;charset=UTF-8 < * Connection #0 to host localhost left intact hello
今度はきちんとアクセスができていることを確認できました。 また、ログには以下のように出力されておりクレイムへのアクセスもできていることがわかります。
2020-08-02 22:37:09,599 INFO [dev.hir.EchoResource] (executor-thread-199) issuer name is https://hirooka.dev/jwt-rbac