Ktor 3.6.0で注目すべき4つの変更
Kotlin向けWebフレームワークKtorの最新リリース、Ktor 3.6.0が公開されました。今回のアップデートでは、認証処理の型安全化、OpenID Connect(OIDC)対応、Nettyにおける実験的なHTTP/3対応、Kotlin Multiplatform(KMP)クライアントの簡易化などが追加されています。
単なるAPI追加にとどまらず、認証基盤や共有クライアントを設計する際の実装方針に影響する変更が含まれている点が特徴です。一方で、HTTP/3など一部の機能は実験的な位置付けであるため、既存システムへの導入では適用範囲を見極める必要があります。
1. 認証処理が型安全に変わる
Ktorの従来の認証では、設定した認証プロバイダーとルート側の処理を暗黙的に結び付ける場面がありました。3.6.0では、認証プロバイダーに型を与え、認証済みユーザーを明示的に扱える仕組みが追加されています。
例えば、JWTのペイロードからアプリケーション固有のUser型を生成し、ルート内でその型を利用できます。
val jwtAuth = jwt<User>("my-jwt") {
verifier(jwkProvider, issuer)
validate { credential ->
val payload = credential.payload
User(
id = payload.subject,
email = payload.getClaim("email").asString()
)
}
}
routing {
authenticateWith(jwtAuth) {
get("/profile") {
val user: User = call.principal
call.respondText(user.toString())
}
}
}
この変更による実務上のメリットは、認証情報を文字列や汎用的なプリンシパルとして扱う範囲を減らせることです。ユーザーIDやメールアドレスなどを各ハンドラーで都度取り出す実装に比べ、認証後のデータ構造をアプリケーションのドメインモデルに近づけられます。
また、ロールベースのアクセス制御や匿名ユーザーもサポートされています。管理者向けAPI、一般ユーザー向けAPI、未ログインでも閲覧できるAPIを同じ認証設計の中で整理しやすくなります。
既存プロジェクトでの適用ポイント
導入時は、まず認証処理と業務ロジックの境界を整理すると効果を得やすくなります。
- JWTやセッションから取得する情報をアプリケーションのユーザー型へ変換する
- ルートハンドラーではトークンのクレームを直接参照しない
- ロールや権限の判定を認証設定とルート構成の双方で一貫させる
- 認証失敗時と匿名アクセス時のレスポンス方針を明確にする
既存コードを一度に全面移行するのではなく、新規エンドポイントや認証周りの改修箇所から適用する方法が現実的です。
2. OpenID Connect対応で認証基盤との接続を簡素化
3.6.0では、OpenID Connectプロバイダーを扱うための新しいOidcプラグインが追加されました。OIDCプロバイダーの設定、JWT Bearer認証、セッション、ブラウザー向けログイン、トークンの自動更新などを、Ktorの認証機構と組み合わせて利用できます。
suspend fun Application.module() {
val oidc = install(Oidc)
val auth0 = oidc.identityProvider("auth0") {
issuer = "https://my-tenant.auth0.com"
bearer {
audience = setOf("https://api.example.com")
}
}
routing {
authenticateWith(auth0.jwtBearer) {
get("/orders") {
val subject = call.principal.claims.subject
call.respondText("Hello $subject")
}
}
}
}
OIDCを利用するAPIでは、認証プロバイダーの設定とアプリケーションの認可ルールを分離して考えることが重要です。issuerやaudienceが正しくても、それだけで業務上の権限管理が完了するわけではありません。ユーザーのロール、テナント、リソース所有権などは、アプリケーション側の認可処理として設計する必要があります。
OIDC導入時に整理したい項目
- トークンの発行者である
issuerをどの値として扱うか - APIが受け入れる
audienceをどう定義するか - JWTの検証と、業務上の権限判定をどこで分けるか
- ブラウザー向けセッションとAPI向けBearerトークンをどう使い分けるか
- トークン更新やログアウトをクライアントとサーバーでどう分担するか
複数のサービスが同じOIDCプロバイダーを利用する環境では、プロバイダー設定を型付きの構成として管理できることが、保守性の向上につながります。
3. Nettyで実験的なHTTP/3対応
Nettyエンジンでは、QUIC上で動作するHTTP/3が実験的にサポートされました。SSLコネクターを設定したうえで、enableHttp3 { }を有効化します。
embeddedServer(Netty, environment, {
sslConnector(
keyStore = keyStore,
keyAlias = "server",
keyStorePassword = { "changeit".toCharArray() },
privateKeyPassword = { "changeit".toCharArray() }
) {
port = 8443
}
enableHttp3 {
quicMaxIdleTimeout = 30.seconds
}
}) {
// application
}.start(wait = true)
HTTP/3ではTCPではなくQUICとUDPを利用するため、アプリケーションコードだけでなく、ネットワーク、ロードバランサー、ファイアウォール、監視基盤も含めて検証する必要があります。特に本番環境では、HTTP/3を有効にしたことによる到達性や運用監視への影響を確認しなければなりません。
また、Ktor 3.6.0では、同一のNettyサーバーで一方のコネクターにh2c、別のコネクターにTLS付きHTTP/2を提供する構成も可能になりました。内部通信と外部通信で異なるプロトコル要件があるシステムでは、選択肢が広がります。
HTTP/3を試す価値があるケース
- モバイルネットワークなど、接続状態が変化しやすい環境を対象にしている
- QUICやHTTP/3を前提としたネットワーク構成を検証したい
- 将来のプロトコル移行に向けて、アプリケーションの互換性を確認したい
ただし、今回のHTTP/3対応は実験的機能です。既存APIの標準プロトコルを直ちに置き換えるより、検証環境や限定的なエンドポイントで性能、障害時の挙動、運用手順を確認する用途に適しています。
4. KMPクライアントの初期設定を簡略化
Kotlin Multiplatform向けには、ktor-client-engine-defaultsが追加されました。commonMainに依存関係を追加すると、共有コード側で特定のエンジンを直接選ばずにHttpClient()を生成できます。
kotlin {
sourceSets {
commonMain {
dependencies {
api("io.ktor:ktor-client-engine-defaults:3.6.0")
}
}
}
}
val client = HttpClient {
install(HttpCache) {
publicStorage(FileStorage(Path("build/cache")))
}
}
ターゲットごとにCIOやDarwinなどのエンジンを選択する設定を共有コードから切り離せるため、KMPプロジェクトの初期構築がシンプルになります。特定プラットフォームで細かな設定が必要になった場合には、従来どおり個別エンジンを明示的に選択できます。
さらに、ファイルベースのHTTPキャッシュはkotlinx-ioのPathを利用する形に移行しました。JVMのjava.io.Fileに依存しない永続キャッシュを構成しやすくなり、共有コードにキャッシュ処理を置く際の制約が減ります。
KMPクライアントを導入するチームにとっては、次のような構成を検討しやすくなります。
- HTTPクライアントの基本設定を
commonMainに集約する - 認証、シリアライズ、キャッシュなどの共通処理を共有する
- プラットフォーム固有の証明書やネットワーク設定だけを各ターゲットで補う
- 必要になった段階で特定のクライアントエンジンを明示的に選択する
5. ルーティングとリクエスト処理の改善
リクエストパラメーターの変換では、KotlinのUuid、Byte、符号なし数値型が扱えるようになりました。IDや範囲が明確な値を、ルート定義の段階から適切な型で扱いやすくなります。
また、ApplicationCall.receive()がnullable型を受け取れるようになり、リクエストボディが存在しないケースをルートの型に反映できます。従来のreceiveNullable()は非推奨となります。
put("/users/{id}") {
val id: Uuid by call.parameters
val preferences = call.receive<NotificationPreferences?>()
if (preferences == null) {
preferenceService.clear(id)
} else {
preferenceService.update(id, preferences)
}
call.respond(HttpStatusCode.NoContent)
}
この変更は、入力値の欠落や空のリクエストボディを業務処理に渡す前に明示できる点で有用です。ただし、nullableであることと、入力内容が妥当であることは別の問題です。必須項目、空オブジェクト、部分更新の意味は、API仕様として定義しておく必要があります。
HTMLレスポンスでは、非推奨となったrespondHtmlFragmentに代わり、respondHtmlPartialが追加されました。FlowContentでサポートされる要素を含む部分HTMLを返せるため、サーバーサイドレンダリングやHTMX系の構成でも利用しやすくなります。
6. クライアントのAcceptヘッダーをより細かく制御
クライアントのContentNegotiationプラグインには、登録済みのコンテンツタイプをAcceptヘッダーへ追加する動作があります。3.6.0では、明示的に設定したAcceptヘッダーを優先するContentTypeMergeStrategy.SkipIfPresentが利用できます。
install(ContentNegotiation) {
register(ContentType.Application.Json, noOpJsonConverter)
acceptHeaderMergeStrategy = ContentTypeMergeStrategy.SkipIfPresent
}
APIごとにレスポンス形式を厳密に指定したい場合や、サーバー側のコンテントネゴシエーションに依存する場合に有効です。共通クライアント設定が意図せず各リクエストのAcceptヘッダーへ影響していたプロジェクトでは、リクエスト単位の制御を見直すきっかけになります。
既存プロジェクトはどこから更新するべきか
Ktor 3.6.0の変更をすべて同時に導入する必要はありません。実務では、次の優先順位で評価すると整理しやすくなります。
- 認証処理を持つAPIは、型安全な認証とOIDCプラグインの適用範囲を検討する
- KMPクライアントは、共通コードでのエンジン選択とHTTPキャッシュを見直す
- 入力値の型変換やnullableな
receive()を使い、ルート契約を明確にする Acceptヘッダーの制御が必要なクライアントでマージ戦略を調整する- HTTP/3は検証環境でネットワークと運用面を含めて評価する
特に認証周りは、コンパイル時に検出できる問題を増やせる可能性があります。一方、認証プロバイダーや権限モデルの変更を伴うため、単純な依存関係更新だけでは終わりません。既存のテストに、認証成功、認証失敗、権限不足、匿名アクセス、期限切れトークンのケースを追加してから移行するのが安全です。
まとめ
Ktor 3.6.0は、Web APIの認証設計とKotlin Multiplatformクライアントの構築に、特に大きな影響を与えるリリースです。
- 型付き認証により、認証済みユーザーをアプリケーションの型として扱いやすくなった
- OIDCプロバイダーとの接続をKtorの認証機構に統合しやすくなった
- Nettyで実験的なHTTP/3と、柔軟なHTTP/2構成を利用できるようになった
- KMPクライアントでエンジン選択とファイルベースキャッシュの設定が簡略化された
- ルーティング、入力値変換、HTML部分レスポンス、Acceptヘッダー制御が改善された
まずは認証や共有クライアントなど、変更による効果を測定しやすい領域から試すのがよいでしょう。HTTP/3については実験的機能であることを踏まえ、性能だけでなく、ネットワーク到達性や障害対応まで含めた検証が導入判断のポイントになります。
出典: JetBrains Blog
