# Ktor勉強会 #003 ## 前回(#002)の内容 - HackMD - https://hackmd.io/mxcKW-CoR9akMf8oKAW76A?view - Gist(HackMDの内容と同じ) - https://gist.github.com/yoshixmk/8720d48ad19bbf2d5675cde0937ecb92 - repository - https://github.com/yoshixmk/ktor-sample ## 参加者 - うえき - やました - あっきー ## 開催日 2020/05/09(土) 13:00ごろ ## 宿題 - 下記をお願いした。 - 各自、興味のあるコンテンツがあれば、調べて発表する - 関連して話したいことがあれば、考えておく - (重要)強制ではないので、なければないでOK - (やました) OR Mapperの興味深いポスト - https://toranoana-lab.hatenablog.com/entry/2020/05/08/174941 ## Agenda - 前回(#002)~今回(#003)の終了時点のdiffはここ - https://github.com/yoshixmk/ktor-sample/pull/2/files ### 前回やった、テストかどうかの判別の修正 - フラグで渡してたので、そちらに書き換え - https://github.com/yoshixmk/ktor-sample/commit/825232db4a8f1c32415a776833f2ddfa09dc760a ## DAO形式とDSL形式 復習もかねて。 Exposeをはじめ、SQLライブラリは、DAO (Data Access Object) 形式とDSL形式の両方できることが多いです。 ライブラリによってはこれにプラスして、SQLInterpolation(いわゆる生のSQLテンプレート)を用意してを実行できるものなどがあったりします。 一般的には、下記の傾向があると思ってます。 - DAO形式: 可読性が高いことが多い。クエリの複雑さが増すと一気にコストがかかる - 前回、うえきが書いたパターン - https://github.com/JetBrains/Exposed/wiki/DAO - DSL形式: 自由度が高く、コストのかからないクエリを作成できる - 前回、やました、あっきーが書いたパターン - https://github.com/JetBrains/Exposed/wiki/DSL #### DAOのコードジェネレータ - 決定的なものは、今はない。 - 有志で少しはあるので試してみても面白いかも - https://github.com/rnett/ExposedDaoGen ### GET /memos (一覧取得) - 出力イメージ: json - IDで昇順ソートが望ましい ```json [ { "memo_id": 1, "subject": "sample1" }, { "memo_id": 2, "subject": "sample2" } ・・・ ] ``` - HTTP response code - 成功 ok(200) - 指定がおかしい 400 - 新たにJson Responseのクラスを作成して使用 - memo_idがスネークケースなのは、この命名がjsonのkeyに一致するため。 - 一般に、Javaをはじめ言語の命名規則を優先し、キャメルケースとする場合もある。 ```kotlin= data class Memo(val memo_id: Int, val subject: String) YAMASHITA: responseにJavaのくらすをそのまま指定してもうまくいかないかも ```kotlin= // ぜんぶだめ get("/memos") { val memos = Memo.all() call.respond(memos) } get("/memos/{id}") { val id = call.parameters["id"]?.toInt() ?: return@get call.respond(HttpStatusCode.BadRequest) val memo = Memo.findById(id) ?: return@get call.respond(HttpStatusCode.NotFound) call.respond(memo) } ``` [回答例 by yoshixmk](https://github.com/yoshixmk/ktor-sample/commit/4b10fa88afbb9832ea86a5aa3ae4c0ca9a1954c6) また、このMemoクラスをしようして、`GET /memos/{id}`もリファクタリングできる。 [リファクタリング後](https://github.com/yoshixmk/ktor-sample/commit/09530ba29f511e902fdef906b9e35bd0484d554e#diff-e78211018bd7a5d57ad2557242da1ce9L113-L118) ### PUT /memo/{id} (更新) - 入力イメージ: json ```json { "subject": "これはアップデートしたメモです" } ``` データクラスはこれ使ってください(前に`Post`リクエストの時に作ったクラスがあれば、それを使ってもらっても良いです) ```kotlin= data class MemoContent(val subject: String) ``` - 出力イメージ: json ```json { "memo_id": 1, "subject": "これはアップデートしたメモです" } ``` - response code - 成功 ok(200) - 存在しない 404 - 指定がおかしい 400 [回答例 by yoshixmk](https://github.com/yoshixmk/ktor-sample/commit/68489f1072c6f66a22a8d37a231ed2b0c8289999#diff-e78211018bd7a5d57ad2557242da1ce9R133-R145) やました: postは `Generic` を引き受けるがputは引き受けない?エラーになる ```kotlin= // これいい post<MemoMemo>("/memos") { input -> // これだめ put<MemoMemo>("/memos") { input -> ``` ### DELETE /memo/{id} (削除) - response code - 成功のみ返す No content(204) - 存在しない 404 - 指定がおかしい 400 [回答例 by yoshixmk](https://github.com/yoshixmk/ktor-sample/commit/2514d40eca526e97fa1eb161e77e4c293577879a) ### ルーティング、configの分割 - この[Routing定義の切り出し](https://rinoguchi.hatenablog.com/entry/2020/04/24/100000#Routing%E5%AE%9A%E7%BE%A9%E3%81%AE%E5%88%87%E3%82%8A%E5%87%BA%E3%81%97)に倣ってやってみる。 - Routing.XXXとなるようにクラスを拡張します。 - 例: Routes.kt ```kotlin= fun Routing.routes() { // システム全般、サンプルコード systems() // メモ機能 memos() // 静的アクセス static("/") { resource("favicon.ico") } } ``` - 例: MemosRoutes.ktを作成し、Routes.ktからMemosRoutes#memos()を呼び出す - https://ktor.io/servers/features/routing.html#routing-tree - Routing Treeを使って、memosのパスは切り出したうえでFunctinalにした ```kotlin= fun Routing.memos() = route("memos") { get("") { val list = transaction { infrastructure.dao.Memo.all().sortedBy { it.id } .map { m -> Memo(m.id.value, m.subject) } } call.respond(list) } ・・・ } ``` - [模範例 by yoshixmk](https://github.com/yoshixmk/ktor-sample/commit/442e580372ea568ac0db045324ce8c17b3f554d4) ### 認証機能 認証は前処理として行われる、いわゆるインターセプタ機能を提供します。 今回はベーシック認証とJWTで行います。 これ以外にはフォーム認証や、ダイジェスト認証、JWT、外部プロバイダを用いるOpenID Connect(OIDC)などがあります。 #### ベーシック認証のための[Setup](https://ktor.io/servers/features/authentication.html) - build.gradle ```gradle dependencies { ・・・ implementation "io.ktor:ktor-auth:$ktor_version" ・・・ } ``` - Application.kt - 試しにvalidateを、username, passwordが両方"basic-auth"の場合に認証成功とします。 ```kotlin= ・・・ install(Authentication) { basic(name = "basic-auth") { realm = "Ktor Server" validate { credentials -> if (credentials.name == name && credentials.password == name) { UserIdPrincipal(credentials.name) } else { null } } } } ・・・ ``` #### [ベーシック認証のrouting実装](https://ktor.io/servers/features/authentication/basic.html) - rotuing、Controllerの処理追加 試しに、system系(Hello worldなど)に対して、認証設定を入れてみます。 ```kotlin= ・・・ authenticate("basic-auth") { get("/") { call.respondText("HELLO WORLD!", contentType = ContentType.Text.Plain) } get("/echo/{echo}") { call.respondText(call.parameters["echo"] ?: "EMPTY", contentType = ContentType.Text.Plain) } get("/json/jackson") { call.respond(mapOf("hello" to "world")) } get("/boom") { throw Exception("boom") } } ・・・ ``` - 認証成功 認証後に、用意されたrouting設定が実行されます。 そのため、200系とは限りません。 - 認証失敗 401 Unauthorizedです。 ![](https://i.imgur.com/cvj7Efw.png) 失敗した場合に、設定していたrealmのHeader情報が受け取れます。 ![](https://i.imgur.com/hwznPMP.png) [回答例 by yoshixmk](https://github.com/yoshixmk/ktor-sample/commit/4d6db8f4a6aafb93a55e3ff9658b961ad937767c) #### テストの失敗と修正 さてここで、buildをした際に、テストが失敗してしまっています。 理由は、テストは、/(Hello Worldのエントリポイント)へのアクセスは、先ほどbasic認証するように書き換えたためです。 ApplicationTest#testRoot を修正してみてください。 - 認証ヘッダは、「Authorization」です。 - 下記の方法で、テストのリクエスト時にヘッダを加えることができます。 ```kotlin= hint.kt fun testRoot() { withTestApplication({ module(testing = true) }) { handleRequest(HttpMethod.Get, "/") { addHeader( "<nameを書く>", "<valueを書く>" ) }.apply { assertEquals(HttpStatusCode.OK, response.status()) assertEquals("HELLO WORLD!", response.content) } } } ``` [回答はテストの章](https://hackmd.io/4_9hMrPUSre3xRxY0cK9pQ?both#%E5%9B%9E%E7%AD%94-%E3%83%99%E3%83%BC%E3%82%B7%E3%83%83%E3%82%AF%E8%AA%8D%E8%A8%BC%E3%81%AE%E5%AE%9F%E8%A3%85%E6%99%82%E3%81%AB%E5%A4%B1%E6%95%97%E3%81%97%E3%81%9F%E6%99%82%E3%81%AB%E5%9F%8B%E3%82%81%E3%82%8B%E3%82%B3%E3%83%BC%E3%83%89)に記載しています。回答例もここに書いてます。 #### [JWT](https://ktor.io/servers/features/authentication/jwt.html)の設定と実装 - JWT(JSON Web Token)は、2つの当事者間で安全に(暗号化した)クレームを表すためのオープンな規格[RFC 7519](https://tools.ietf.org/html/rfc7519)で定義されてる内容です。 - [jwt.ioのデバッガ](https://jwt.io/#debugger-io)を使用すると、JWTをデコード、検証、および生成できます。 - Ktorの[JWTのドキュメントページ](https://ktor.io/servers/features/authentication/jwt.html)を参考にまずは、設定をやってみます。 - 下記はイメージ。認証サーバに対して、ログインを行う(①)と、JWTが返されます(②)。そのJWTを用いて、APIサーバヘアクセスできる(③)と言う仕組みです。今回は、①、②、③の全てが、テストユーザとしてではありますが、APIサーバで行うことを考えます。 ![](https://i.imgur.com/Bf7F5yM.png) buid.gradleに、ktorのJWTのライブラリを加えます ```groovy: buid.gradle dependencies { ・・・ implementation "io.ktor:ktor-auth-jwt:$ktor_version" } ``` ktor-auth-jwtでは、認証するためにCredentialもしくは、Principalのオブジェクトが必要です。 まずjwt認証についての設定を加えます ```kotlin= install(Authentication) { ・・・ jwt(name = "jwt") { verifier(JwtConfig.verifier) realm = "ktor.io" validate { if (it.payload.claims.contains("id")) User.testUser else null } } } ``` テストユーザ、及びモックメソッドも作っておきます ```kotlin= import io.ktor.auth.Principal data class User( val id: Int, val name: String, val countries: List<String> ) : Principal { companion object { val testUser = User(1, "Test", listOf("Egypt", "Austria")) } fun findUserById(id: Int): User = testUser } ``` パブリックAPIとして、JWT token発行のエントリポイントを作成します(先ほどのテストユーザを使用します)。 ```kotlin= post("/login") { // val credentials = call.receive<UserPasswordCredential>() val user = User.testUser // user by credentials val token = JwtConfig.makeToken(user) call.respondText(token) } ``` [デバッガ](https://jwt.io/#debugger-io)を使って、デコードして、トークンの中身を確認してみます。 テストユーザの情報が入っていることがわかります。 ![](https://i.imgur.com/CM5uh7r.png) 認証を使用したいroutingを、authenticateで括ります(ここでは、`/boom`にを括って、使いました) ```kotlin= authenticate("jwt") { get("/boom") { get("/boom") { throw Exception("boom") // throw Exception("boom") call.respondText("boom!!") } } ``` Insomniaでこのように設定し、「ENABLE」をONの時は200、OFFのときは401となることが分かります。 ![](https://i.imgur.com/RAuYvEy.png) [JWT実装例 by yoshixmk](https://github.com/yoshixmk/ktor-sample/commit/b575f8df5e849fa8230323ec4eeaa88e76dc0fb6) [参考にしたGithub repository](https://github.com/AndreasVolkmann/ktor-auth-jwt-sample/blob/master/src/main/kotlin/me/avo/io/ktor/auth/jwt/sample/JwtConfig.kt) ### WebSockets - [WebSocket](https://ktor.io/servers/features/websockets.html)(ウェブソケット)は、最初に選択していたため用意されてサンプルがあるので実行してみます。 - これもKtorでは、routesとして記載されています。メッセージが届いたら"Client said: "を付与して、返すサンプルです。 ```kotlin= ・・・ webSocket("/myws/echo") { send(Frame.Text("Hi from server")) while (true) { val frame = incoming.receive() if (frame is Frame.Text) { send(Frame.Text("Client said: " + frame.readText())) } } } ・・・ ``` - RESTとは違いますが、メッセージアプリケーションではよく使われる手法です。 #### Websocket Clientで試す GUIの[Chrome extention/smart-websocket-client](https://chrome.google.com/webstore/detail/smart-websocket-client/omalebghpgejjiaoknljcfmglgbpocdp/related)もしくは、CUIの[コマンドラインツール/wscat](https://tricknotes.hateblo.jp/entry/20120227/p1)がおすすめです。 ちなみに、Postmanや[Insomnia](https://github.com/Kong/insomnia/issues/528)はREST Clientなので、この括りに入らないWebsocketは非対応です。 - サーバと会話できていればok - ws://localhost:8080/myws/echo ![](https://i.imgur.com/AvYBS5R.png) ### heroku - 山下さんのパート ### DIを用いた責務分割(次回にします) 結構な整備が必要なため、多分時間かかるやつでした。次回にします。 依存関係をpackageごとに、build.gradleでアクセスできないようにする - クリーンアーキテクチャやる - DIには、Koinを使用する - [参考1: いつもの](https://rinoguchi.hatenablog.com/entry/2020/04/24/100000#DI%E4%BE%9D%E5%AD%98%E6%80%A7%E3%81%AE%E6%B3%A8%E5%85%A5%E3%81%AE%E5%B0%8E%E5%85%A5) - [参考2: Qiita repository by kaonash](https://qiita.com/kaonash/items/41f605c10bbff413eabe#infrastructure) - [参考2: Github repository by kaonash](https://github.com/kaonash/ktor-clean-architecture-sample) ### テストコード #### [回答] ベーシック認証の実装時に失敗した時に埋めるコード ```kotlin= fun testRoot() { withTestApplication({ module(testing = true) }) { handleRequest(HttpMethod.Get, "/") { addHeader( "Authorization", "Basic YmFzaWMtYXV0aDpiYXNpYy1hdXRo" ) }.apply { assertEquals(HttpStatusCode.OK, response.status()) assertEquals("HELLO WORLD!", response.content) } } } ``` [回答commit by yoshixmk](https://github.com/yoshixmk/ktor-sample/commit/2ed37231e6940b95c2bb3d208475a8dc5e9c3323) #### テストレポートについて テストFailやテストエラーが起こった際、テストレポートをみると把握しやすいです。 プロジェクト配下の`build/reports/tests/test/classes/**/*.html`に吐き出されます。 例えば、ApplicationTestであれば、こういった感じ。 > build/reports/tests/test/classes/yoshixmk.ApplicationTest.html ![](https://i.imgur.com/XnO0rdx.png) #### 続きは次回 テストについて次回解説します。 ### === 自由にどうぞ === ## Memo ### Heroku, Vercel, Netlify #### Heroku [https://heroku.com](https://heroku.com) [KtorのHerokuデプロイ方法](https://jp.ktor.work/servers/deploy/hosting/heroku.html) - 方法としては2つ - jarにビルドして動かす - Dockerfile作ってコンテナで動かす #### Vercel Zeit から名前変わったみたい [https://vercel.com](https://vercel.com) #### Netlify [https://www.netlify.com](https://www.netlify.com) #### 質問 1. 無料でも何かデプロイしたいときはこれ?でも、Java, Kotlinみたいな重めの言語でも動かせる? * Heroku はできそう * Versel は Node.js のみかも * Netlify は static site hosting 3. RDB, NoSQLとか、なにかと連携できる?できれば同一ネットワーク内で、マニーかからなくて ## 次回の開催日 2020/05/16(土) 13:00ごろ * しゅくだい * Heroku アカウントつくっておく * Heroku CLIを Terminal で打てるようにしておく