Cookie/Headerセッション

Estimated reading time: 2 minutes

セッションとして、Cookieを使うこともカスタムのHTTPヘッダーを使うこともできます。 コードは大体同じですが、どこにセッション情報を送信したいかによって、cookieheaderメソッドのどちらかを呼び出す必要があります。

Cookie vs ヘッダー セッション

cookieまたはヘッダーを使い、sessionIDまたはペイロードを変換する際、どの方式をとりたいかはコンシューマによります。 例えば、ウェブサイトの場合、通常はCookieを使い、APIにおいてはヘッダーを使いたいかもしれません。

Sessions.Configurationはcookieheaderの2つのメソッドを提供し、セッションをどう変換するかを選択できます:

application.install(Sessions) {
    cookie<MySession>("SESSION")
} 

追加のブロックを提供することでCookieの設定ができます。 例えばSameSite拡張の追加のようなCookieプロパティの設定ができます:

application.install(Sessions) {
    cookie<MySession>("SESSION") {
        cookie.extensions["SameSite"] = "lax"
    }
} 

cookieメソッドはブラウザセッションを想定しています。 標準的なSet-Cookie headerが使われます。 cookieブロック内では、Set-Cookieヘッダーを設定するようなcookieプロパティにアクセスすることができます。 例えばcookieのpathや有効期限やドメインやhttpsに関わることなどを設定できます。

install(Sessions) {
    cookie<SampleSession>("COOKIE_NAME") {
        cookie.path = "/"
        /* ... */
    }
}

Header関数はAPI用途を想定しています。 JavaScript XHRリクエストと、サーバサイドからのリクエストの療法を想定しています。 APIクライアントにとっては、cookieを操作するよりも、カスタムのヘッダーを読み書きするほうが通常簡単です。

install(Sessions) {
    header<SampleSession>("HTTP_HEADER_NAME") { /* ... */ }
}
application.install(Sessions) {
    header<MySession>("SESSION")
} 

カスタムストレージ

Session APIはSessionStorageインターフェースを提供し、以下のようになっています:

interface SessionStorage {
    suspend fun write(id: String, provider: suspend (ByteWriteChannel) -> Unit)
    suspend fun invalidate(id: String)
    suspend fun <R> read(id: String, consumer: suspend (ByteReadChannel) -> R): R
}

3つの関数すべてsuspendでマークされており、完全に非同期になるよう設計されています。 そして非同期Channelから読み書きできるAPIを提供するkotlinx.coroutines.ioから、ByteWriteChannelByteReadChannelを利用しています。

実装において、ByteWriteChannelとByteReadChannelを提供するcallbackを呼びだす必要があります。 ByteWriteChannelとByteReadChannelをあなたは提供する必要があり、オープン・クローズするのはあなたの責務です。 ByteWriteChannelByteReadChannelについてはそれらのライブラリのドキュメント内でより詳細に読めます。 単にByteArrayを読み込み・保存する必要があるだけならば、このシンプルなセッションストレージを提供するスニペットを使えます。

SimplifiedSessionStorage.kt
abstract class SimplifiedSessionStorage : SessionStorage {
    abstract suspend fun read(id: String): ByteArray?
    abstract suspend fun write(id: String, data: ByteArray?): Unit

    override suspend fun invalidate(id: String) {
        write(id, null)
    }

    override suspend fun <R> read(id: String, consumer: suspend (ByteReadChannel) -> R): R {
        val data = read(id) ?: throw NoSuchElementException("Session $id not found")
        return consumer(ByteReadChannel(data))
    }

    override suspend fun write(id: String, provider: suspend (ByteWriteChannel) -> Unit) {
        return provider(CoroutineScope(Dispatchers.IO).reader(coroutineContext, autoFlush = true) {
            write(id, channel.readAvailable())
        }.channel)
    }
}

suspend fun ByteReadChannel.readAvailable(): ByteArray {
    val data = ByteArrayOutputStream()
    val temp = ByteArray(1024)
    while (!isClosedForRead) {
        val read = readAvailable(temp)
        if (read <= 0) break
        data.write(temp, 0, read)
    }
    return data.toByteArray()
}

このシンプルなストレージを使い、2つのよりシンプルなメソッドを実装する必要があります:

abstract class SimplifiedSessionStorage : SessionStorage {
    abstract suspend fun read(id: String): ByteArray?
    abstract suspend fun write(id: String, data: ByteArray?): Unit
}

そのため例えば、redisセッションストレージは以下のようになります:

class RedisSessionStorage(val redis: Redis, val prefix: String = "session_", val ttlSeconds: Int = 3600) :
    SimplifiedSessionStorage() {
    private fun buildKey(id: String) = "$prefix$id"

    override suspend fun read(id: String): ByteArray? {
        val key = buildKey(id)
        return redis.get(key)?.unhex?.apply {
            redis.expire(key, ttlSeconds) // refresh
        }
    }

    override suspend fun write(id: String, data: ByteArray?) {
        val key = buildKey(id)
        if (data == null) {
            redis.del(buildKey(id))
        } else {
            redis.set(key, data.hex)
            redis.expire(key, ttlSeconds)
        }
    }
}