ユーザーのセッション情報をスケーラブルに保つ 2 つの方法


セッション管理について誤った認識を持っていたり、サーバのスケーラビリティを考慮しないままアプリケーションの開発を進めてはいけません。開発の終盤でサーバの負荷分散ができない、なんということになりかねません。

本記事では、Web 初心者向けにセッション管理の主要な2種類の方法について説明します。具体的な実装例として、Web サーバに express を使用しますが、基本的な考え方はどの言語、どの Web サーバでも同じです。

セッション管理とは

そもそも、セッション管理とは何でしょうか。

ブラウザから Web サーバへの通信は HTTP で行われます。HTTP プロトコルはステートレスです。つまり「状態」を持てません。
クライアントから Web サーバへリクエストがあるたびにクライアントとサーバ間でコネクションが張られ、リクエストを受け、レスポンスを返却した後、コネクションは破棄されます。
もう一度リクエストをしても、Web サーバは同じクライアントからリクエストがあったと判断することはできません。

そこで一般的に Web アプリケーションにおいては、ユーザがログインした後、そのユーザ情報をセッションデータとしては何らかの方法で保持することが求められます。このログインしたユーザの情報を何とか保持しようとする仕組みのことを「セッション管理」と呼びます。
しっかりと学びたい人は MDN web docs を読みましょう。

セッションデータの管理方法

ユーザーのセッションデータを保持するには主に2つの方法があります(実際には他にもいくつかありますが、一般的にはこの2つがあげられるでしょう)。

  1. セッション ID を使う
    クライアントではセッション ID のみを Cookie 内に保持し、
    サーバでセッション ID に紐づくセッションデータを保持する方法。
  2. Cookie のみでユーザ情報を保持する
    クライアントにてセッションデータ全てを Cookie 内に保持する方法。

1. セッションIDを使う

1 の方法を採用すると、クライアントとサーバは以下のような関係になります。
ユーザ 0001 が自身のセッション ID を Cookie に id=0001 で保持しており、サーバサイドで 0001 に紐づくユーザ情報を取得しています。

ただしこの方法ではいくつか問題があります。通常 Web システムではバックエンドのサーバは冗長構成をとり、LB(ロードバランサ)でリクエストが負荷分散されます。また、スケールアウト/スケールインすることも考慮しなければいけません。
単一サーバ内でセッションデータを保持していた場合、1つ前にリクエストしたサーバ以外へルーティングされてしまうとセッションデータが取得できません。

そこで Redis などの Key/Value で取得できるデータベースを用意しておき、どのサーバからも取得できるようにしておく構成とすることが一般的です。

2. Cookie のみでユーザ情報を保持する

2 の方式を採用することで、サーバサイドのスケーラビリティを簡単に担保できます。クライアントにユーザのセッションデータを全て格納しておき、毎回サーバサイドに送信することでユーザのセッションデータを維持できます。以下の図では、Cookie には name=田中, [email protected] を格納しており、リクエストごとにサーバへ送信しています。実際にこの方式を使用する場合は、Cookie には生身のデータを保持することはありません。暗号化した文字列を保存し、サーバサイドで復号化して取り出すことが一般的でしょう。

この方式を採ることでサーバー側にデータベースやリソースを用意する必要が無くなります。ただし、セッションデータの合計がブラウザの最大 Cookie サイズ(4096バイト)を超えることはできないことに注意しましょう。

セッションデータの管理方法の判断基準

まとめると、セッションデータの取り扱い方法で考慮するのは以下のようになります。
アプリケーションの仕様や必要に応じて選択できるようにしておきましょう。

管理方法 メリット デメリット
1. セッション ID を使う ・セッションデータのサイズの上限を気にする必要がない
・セッションデータはクライアントに対して不可視
・サーバサイドで Redis などのスケーラブルなセッションストアを用意する必要がある。
2. Cookie のみでユーザ情報を保持する ・Redis などのスケーラブルなセッションストアが不要 ・ブラウザには Cookie のサイズ上限があり、大きなデータを保持できない(4096 バイト)
・Cookie データがクライアントに見えてしまう

主要なライブラリ

さて、express でセッションデータを保持する有名なライブラリとしては以下の2つがあります。

モジュール 概要
express-session 1 の方法に対応。
クライアント上のセッション識別子のみを Cookie 内に格納し、セッションデータはサーバーに格納します。通常は Redis などのデータベースに保存します。
cookie-session 2 の方法に対応。
クライアント上のセッションデータを Cookie 内に格納します。

express-session

express-session は、セッションデータをサーバーに保管します。Cookie にはセッションデータそのものではなく、セッション ID のみを保存します。デフォルトで、メモリー内のストレージを使用するため、本番環境向けには設計されていません。本番環境では、Redis などのスケーラブルなセッションストアをセットアップする必要があります。互換性のあるセッションストアのリストを参照してください。

サーバ内メモリにセッションデータを保持する場合、最もシンプルな例は以下になるでしょう。使用できるオプションの詳細は公式 GitHub リポジトリを参照ください。
Cookie にセッション ID を保持し、サーバに保持されたセッションデータがあれば、それをカウントアップして返却しています。
特定のユーザのリクエスト数を計測しています。

const express = require("express");
const session = require("express-session");
const app = express();

app.use(
  session({
    secret: "input your secret string", // 署名に使用するシークレット文字列
    cookie: { maxAge: 10000 }, // 10秒間リクエストがなければセッションデータは削除されます。
  })
);

app.get("/", function (req, res, next) {
  res.setHeader("Content-Type", "text/html");
  if (req.session.views) {
    req.session.views++;
    res.write("<p>views: " + req.session.views + "</p>");
    res.write("<p>expires in: " + req.session.cookie.maxAge / 1000 + "s</p>");
    res.end();
  } else {
    req.session.views = 1;
    res.end("<p>welcome to the session demo. refresh!</p>");
  }
});

app.listen("3000", () => {
  console.log("Application started");
});

もちろん、異なるクライアントごとに一意なセッション ID が発行されます。以下は Chrome と Firefox を同時にたちあげて振る舞いを観測しています。セッション ID に紐づいたセッションデータをサーバサイドから取り出せています。

さて、セッションデータをメモリに保存していますが、このままでは不十分です。サーバを再起動するとセッションデータが全て消えてしまったり、サーバを複数台用意してロードバランサなどで負荷分散する構成をとることができません。セッションストアを利用できるようにしましょう。

以下は、Redis を使用した例です。connect-redis モジュールを使用しています。

const redis = require('redis')
const session = require('express-session')

const RedisStore = require('connect-redis')(session)
const redisClient = redis.createClient()

app.use(
  session({
    store: new RedisStore({ client: redisClient }),
    secret: 'keyboard cat',
    resave: false,
  })
)

cookie-session

cookie-session はセッション・キーだけでなく、セッション全体を Cookie に保存します。ブラウザは Cookie 当たり最小 4096 バイトをサポートするので、比較的小さいデータを取り扱う場合にのみ使用を検討してください。

const cookieSession = require("cookie-session");
const express = require("express");

const app = express();

app.use(
  cookieSession({
    name: "session",
    keys: ["key1", "key2"],
    maxAge: 10000,
  })
);

app.get("/", function (req, res, next) {
  res.setHeader("Content-Type", "text/html");
  if (req.session.views) {
    req.session.views++;
    res.write("<p>views: " + req.session.views + "</p>");
    res.end();
  } else {
    req.session.views = 1;
    res.end("<p>welcome to the session demo. refresh! -- cookie-session --</p>");
  }
});

app.listen(3000);

express-session とは異なり、session.sig, session というキーの Cookie が保管されています。セッションデータの実体は { views: 3 } のようなオブジェクトですが、暗号化されて保持されています。

まとめ

express で使用される主要な2つのライブラリとその使用方法を簡単に説明しました。
セッションの管理は Web 開発において最も基本的なところですので、しっかり理解しておきましょう。