KotlinのAPIサーバー開発の自分的標準構成模索 Ktor, Graphql Kotlin, Exposed, Koin, Cognito
KtorとGraphql Kotlinの設定方法
JWTとGraphQL Kotlinの連携
GraphQLの各クエリーで、JWTのユーザーを取得する方法
Ktor:2.3.7
graphql-kotlin-ktor-server:7.0.2
JWTの設定
import com.auth0.jwk.JwkProviderBuilder
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
fun Application.configureJwt() = environment.config.run {
install(Authentication) {
jwt {
val audience = property("jwt.audience").getString()
val issuer = property("jwt.issuer").getString()
val jwkProvider = JwkProviderBuilder(issuer).build()
verifier(jwkProvider, issuer)
validate { credential ->
JWTPrincipal(credential.payload)
}
}
}
}
上の設定でcall.principalにJWTPrincipalが設定される。
(Principalは任意のカスタマイズしたものでもOK)
GraphQL設定
fun Application.graphQlModule() {
install(GraphQL) {
schema {
packages = listOf("presentation.graphql")
queries = listOf(
HelloWorldQuery()
)
schemaObject = AppSchema()
}
server {
contextFactory = CustomGraphQLContextFactory()
}
}
routing {
authenticate(optional = true) {
graphQLPostRoute()
}
}
}
class CustomGraphQLContextFactory : DefaultKtorGraphQLContextFactory() {
override suspend fun generateContext(request: ApplicationRequest): GraphQLContext {
val principal = request.call.principal<JWTPrincipal>()
?: return super.generateContext(request)
return super.generateContext(request).plus(
mapOf("principal" to principal)
)
}
}
class HelloWorldQuery : Query {
fun hello(dfe: DataFetchingEnvironment): String {
val principal = dfe.graphQlContext.get<JWTPrincipal?>("principal") ?: return "Hello World!"
return "Hello ${principal["username"]}!"
}
}
CustomGraphQLContextFactoryで、セットされたApplicationCall内のPrincipalを取得し、GraphQL Contextに登録する。
GraphQL Contextは各クエリ等で利用できるので、JWTの情報を扱うことができる。
※ authenticate(optional = true)を使うと、認証がオプショナルになるので、認証されていなくても各ルーティングが行われるようになる。
DIを設定する
class HelloWorldQuery(service: HelloWorldService) : Query {
fun hello(dfe: DataFetchingEnvironment): String {
val principal = dfe.graphQlContext.get<JWTPrincipal?>("principal") ?: return "Hello World!"
val hello = service.getHello(principal["username"])
return hello
}
}
interface HelloWorldService {
fun getHello(name: String): String
}
class HelloWorldServiceImpl : HelloWorldService {
override fun getHello(name: String): String {
return "Hello ${name}!"
}
}
Koin.kt
fun Application.configureDi() {
install(Koin) {
modules(appModules())
}
}
fun appModules() = listOf(module {
single<HelloWorldService> { HelloWorldServiceImpl() }
singleOf(::HelloWorldQuery)
})
Application.kt
fun Application.graphQlModule() {
val helloWorldQuery by inject<HelloWorldQuery>()
install(GraphQL) {
schema {
packages = listOf("presentation.graphql")
queries = listOf(
helloWorldQuery,
)
schemaObject = AppSchema()
}
server {
contextFactory = CustomGraphQLContextFactory()
}
}
routing {
authenticate(optional = true) {
graphQLPostRoute()
}
}
}
KoinでInjectしたインスタンスを、graphQlModuleのqaueriesやmutationsに渡すとうまくいく。
Exposed
Exposedのドキュメント(wiki)
Ktorのドキュメント(Exposedの設定)
Exposed has two flavors of database access: typesafe SQL wrapping DSL and lightweight Data Access Objects (DAO)
ExposedはSQLをラッピングしたDSLと、DAOと、2種類のDBアクセスを提供している。
第一印象、検索のような複雑なクエリはDSLを使って、更新や登録はDAO使うのが良さそうかな。例外はあると思うけど。
DAOでも割と柔軟に検索できそうだからいったんDAOでできるところまでやるのが良さそう