だいぶ遅くなってしまいましたが、この記事はリクルートライフスタイル Advent Calendar 2017の2日目の記事です。
はじめに
Ktorとは
KtorとはJetBrains社が提供する、kotlinのwebフレームワークです。
"Easy to use, fun and asynchronous."を標榜しており、とても軽量で簡単に使えるのが特徴です。
この記事を書こうと思った11月末にはQiitaにktorのtagすら無い状態だったのですが、モタモタしている内にいくつかQiitaにも記事が投稿されていました。
kotlinをサポートしたWeb FrameworkといえばSpring Framwork 5.0が有名ですが、kotlinの提供元であるJetBrains製ということでKtorは安心感も高く、シンプルなものを作る時には選択肢になりうるのかなと思っております。
本記事の目的・モチベーション
公式ページでも説明が色々されているのですが、まだ未整備の部分も多く認証に関するページもまだだったため、自分で試してみたいと思いたち、Githubの該当部分の実装やsampleを色々調べたため記事としてまとめました。
今回は、GitHubアカウントでOAuth2認証してログインし、ユーザ情報を表示するだけの簡単なアプリをサンプルとして作りました。
記事内のソースコードはGitHubにあげてあります。
なおkotlin, Ktorの使用したバージョンは下記となっております。
- kotlin:
1.2.0
- Ktor:
0.9.0
Feature
今回使用する主要なFeature
のそれぞれの簡単な説明です。
- Routing
- Sessions
- Authentication
まずは、使用するFeature
をinstallします。
class App {
fun Application.install() {
install(Sessions)
install(Routing)
}
}
Application
に直接 Authentication
をinstallしていないかというと、特定のパスでのみOAuth2認証をするようにするためです。
詳しくは下記で説明します。
Routing
まずは、Routing
の設定をおこないます。
このFeatureはその名の通りrequetのルーティングを管理するFeature
です。
こちらは本記事では詳しくは触れませんが、Routingをタイプセーフに行うためにLocation Feature
もinstallし、各エンドポイントをclassとして宣言します。
それを用いて、Route
の拡張関数として各エンドポイント毎の設定をすると、以下のようになります。
これでルーティングの設定ができている状態となります。
@location("/") class Index()
@location("/login") class Login()
@location("/login/github") class LoginWithGitHub()
fun Route.index() {
get<Index> {
call.respondText { "TOPページ" }
}
}
fun Route.login() {
get<Login> {
call.respondText { "ログインページ" }
}
}
fun Route.loginWithGitHub() {
get<LoginWithGitHub> {
call.respondText { "ここからGitHubの認証ページにリダイレクトします" }
}
}
class App {
fun Application.install() {
install(Sessions)
install(Locations)
install(Routing) {
index()
login()
loginWithGitHub()
}
}
}
Sessions
このFeatureを使うことでSessionへの値の保存・読み出しが可能となります。
値をCookieで渡すか、session idを発行するかセッションのストア先はどうするかなどを設定できます。
今回は、Cookieにsession idを発行して渡しインメモリで保存するように設定しました。
install(Sessions) {
cookie<GitHubSession>("GitHubSession", SessionStorageMemory())
}
またここではSessionに保存する型も指定しています。
今回はaccess_token
だけを持つようにしています。
data class GitHubSession(val accessToken: String)
Authentication
やっと本題の認証の設定についてです。
- 認証済みであれば、Indexページへリダイレクト
- 未認証であれば、Loginページへリダイレクト
- Loginページ内の「GitHubでログイン」ボタンを押すとLoginWithGitHubページへ遷移、OAuth2認証開始
という動きにしたいと思います。
Sessionから認証情報を読み出す。
まずは、Sessionから認証情報を読み出して判定するように、Routeの各拡張関数を修正します。
fun Route.index() {
get<Index> {
if (call.sessions.get<GitHubSession>() != null) {
call.respondText { "TOPページ" }
} else {
call.respondRedirect(application.locations.href(Login()))
}
}
}
fun Route.login() {
get<Login> {
if (call.sessions.get<GitHubSession>() != null) {
call.respondRedirect(application.locations.href(Index()))
} else {
call.respondHtml {
head { title { +"Login" } }
body { a(href = locations.href(LoginWithGitHub())) { +"GitHubでログイン" } }
}
}
}
}
このように、PipelineContext<*, ApplicationCall>
内でcall.sessions
とすることでSessionへとアクセスできます。
OAuth2認証設定
次に、LoginWithGitHub
ページでOAuth2認証を行うための設定をします。
サービスプロバイダや登録したクライアントの情報
まずはサービスプロバイダであるGithubの情報と、そこへ登録したサンプルアプリの情報をOAuth2ServerSettings
として宣言します。
// OAuth2の設定
private val gitHubOAuth2Settings = OAuthServerSettings.OAuth2ServerSettings(
name = "github",
authorizeUrl = "https://github.com/login/oauth/authorize",
accessTokenUrl = "https://github.com/login/oauth/access_token",
clientId = "xxxx",
clientSecret = "xxxx",
defaultScopes = listOf("read:user")
)
OAuth2認証のinstall
Route
にAuthentication
をinstallすることで、該当のパスで認証のチェックが行われます。
Authentication
の詳細として、先ほど宣言したOAuth2ServerSettings
を用いてOAuth2認証をおこないます。
oauthAtLocation
関数自体は標準のAPIで、OAuth1用の設定を渡せばOAuth1認証をおこなってくれます。
また、GitHubとのHTTP通信にHttpClient
が必要になるためメソッド引数として受け取っています。
private val exec = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 4)
fun Route.loginWithGitHub(client: HttpClient) {
location<LoginWithGitHub> {
// Authentication Featureをinstall
install(Authentication) {
// OAuth2認証
oauthAtLocation<LoginWithGitHub>(
client,
exec.asCoroutineDispatcher(),
providerLookup = { gitHubOAuth2Settings },
urlProvider = { location, _ -> "http://localhost:8080${application.locations.href(location)}" })
}
// 認証後(OAuth2フローでcallbackとして戻ってきてから)の処理)
handle { TODO("認証後の処理") }
}
}
また、今までget<LoginWithGitHub>
となっていた部分がlocation<LoginWithGitHub>
となっていることに気づいたかもしれませんが、実は元々のget
関数はlocation
関数のラッパーなのですが、これを使うとRoute
に認証処理を挟み込めないためlocation
関数を使うように変更しています。
inline fun <reified T : Any> Route.get(noinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Unit): Route {
return location(T::class) {
method(HttpMethod.Get) {
handle(body)
}
}
}
認証後の処理
先ほど省略していた認証後の処理の中身ですが、取得した認証情報をSessionに格納してTOPページへリダイレクトします。
取得した認証情報はcall.principal<OAuthAccessTokenResponse.OAuth2>()
で取得できます。
// 認証後(OAuth2フローでcallbackとして戻ってきてから)の処理)
handle {
val principal = call.principal<OAuthAccessTokenResponse.OAuth2>()
if (principal != null) {
// セッションへ認証情報を格納
call.sessions.set(GitHubSession(principal.accessToken))
call.respondRedirect(application.locations.href(Index()))
} else {
call.respondText { "Login Error" }
}
}
これでOAuth2を用いた認証の一連のフローが完成しました!!
Pipeline/ApplicationCall/PipelineContext周りの動きを理解するのが最初は大変でしたが、
とてもシンプルに書けて楽しいので、もう少しKtorを色々触ってみたいと思いました。
全体のコードが気になる方は、GitHubをご覧ください!