CLOVER🍀

That was when it all began.

express-sessionを試してみる(メモリ、Redis)

これは、なにをしたくて書いたもの?

Expressのセッション管理の仕組みを使ってみようかな、と思いまして。

Expressとセッション

Expressのセッションに関する話は、以下に少し出てきます。

Production Best Practices: Security / Use cookies securely

Expressには、以下の2つのセッションモジュールがあるようです。

cookie-sessionはCookieにセッションデータを保存するミドルウェアです。
express-sessionはCookieにセッションIDのみを保存して、セッションデータは別のストレージに保存するミドルウェアです。

cookie-sessionはCookieにセッションデータも含むため、以下のような特徴があります。

  • 保存するデータの合計は、Cookieの上限に縛られる
    • 代わりに、サーバー側にデータベース等は必要ない
  • 負荷分散が容易になる

セッションデータが比較的小さく、データもプリミティブとして簡単にエンコードできる場合にのみ使用する、という
方針みたいです。

Only use it when session data is relatively small and easily encoded as primitive values (rather than objects).

今回は、express-sessionの方を使ってみます。

ちなみに、このセクションに書かれている内容はセッション用のミドルウェアを使う際のセキュリティ上の注意事項
なので、実際に使う時には見ておくとよいでしょう。

Production Best Practices: Security / Use cookies securely

express-session

express-sessionについて、もう少し。

GitHub - expressjs/session: Simple session middleware for Express

繰り返しになりますが、express-sessionはCookieにセッションIDのみを保存して、セッションデータは
別のストレージに保存するミドルウェアです。

デフォルトのストレージはインメモリです。こちらはメモリーリークが発生する可能性が高く、本番環境での
利用を意図して設計されたものではないことに注意が必要そうです。

Warning The default server-side session storage, MemoryStore, is purposely not designed for a production environment. It will leak memory under most conditions, does not scale past a single process, and is meant for debugging and developing.

ストレージ(セッションストア)は、他のものに変更することができます。その一覧はこちら。

express-session / Compatible Session Stores

今回はインメモリ(デフォルト)とRedisをそれぞれ試してみたいと思います。

最初にインメモリの方で確認して、そちらをベースにRedisを組み込む方向でいってみたいと思います。

環境

今回の環境は、こちら。

$ node --version
v16.13.1


$ npm --version
8.1.2

Redisは以下のバージョンを使用し、172.17.0.2で動作しているものとします。また、パスワードはredispassとします。

$ bin/redis-server --version
Redis server v=6.2.6 sha=00000000:0 malloc=jemalloc-5.1.0 bits=64 build=ef9c41a6cf56a536

準備

Node.jsのプロジェクトは2つ作ることにしますが、共通のところはまとめて書きます。

$ npm init -y
$ npm i -D typescript
$ npm i -D -E prettier
$ mkdir src

TypeScriptのバージョン。

$ npx tsc --version
Version 4.5.4

Express、それからNode.jsとExpressの型宣言のインストール。

$ npm i express
$ npm i -D @types/node @types/express

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

.prettierrc.json

{
  "singleQuote": true
}

インメモリでexpress-sessionを使う

では、まずはexpress-sessionに慣れていきましょう。

express-sessionと型宣言をインストール。

$ npm i express-session
$ npm i -D @types/express-session

今回の依存関係は、このようになりました。

  "devDependencies": {
    "@types/express": "^4.17.13",
    "@types/express-session": "^1.17.4",
    "@types/node": "^17.0.5",
    "prettier": "2.5.1",
    "typescript": "^4.5.4"
  },
  "dependencies": {
    "express": "^4.17.2",
    "express-session": "^1.17.2"
  }

作成したソースコードは、こちら。

src/app.ts

import express from 'express';
import session from 'express-session';

declare module 'express-session' {
  interface SessionData {
    firstAccessTime: string;
    counter: number;
    message: string;
  }
}

const app = express();
let port: number;

const args = process.argv.slice(2);

if (args.length == 0) {
  port = 3000;
} else {
  port = parseInt(args[0], 10);
}

app.set('trust proxy', 1);
app.use(
  session({
    secret: 's3Cur3',
    name: 'session', // default: connect.sid
    resave: false,
    saveUninitialized: true,
    cookie: {
      path: '/', // default
      httpOnly: true, // default
      maxAge: 10 * 1000, // 10sec
    },
  })
);

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use((req, res, next) => {
  if (!req.session.firstAccessTime) {
    const now = new Date();
    req.session.firstAccessTime = now.toISOString();
  }

  req.session.counter = req.session.counter ? req.session.counter + 1 : 1;

  next();
});

app.post('/message', (req, res) => {
  const message = req.body['message'];

  req.session.message = message;

  res.send({
    firstAccessTime: req.session.firstAccessTime,
    counter: req.session.counter,
    message: req.session.message,
  });
});

app.get('/message', (req, res) => {
  res.send({
    firstAccessTime: req.session.firstAccessTime,
    counter: req.session.counter,
    message: req.session.message ? req.session.message : 'Hello World',
  });
});

app.listen(port, () => {
  console.log(`[${new Date().toISOString()}] start server[${port}]`);
});

ちょっとずつ、解説していきます。

モジュールのインポート。

import session from 'express-session';

次に、セッションで扱うデータ(SessionData)の型宣言をしておきます。

declare module 'express-session' {
  interface SessionData {
    firstAccessTime: string;
    counter: number;
    message: string;
  }
}

これをやっておかないと、Request#sessionを使ったセッションデータを扱えません。少し補足があるので、それは
後述します。

セッションおよびCookieの設定。

app.use(
  session({
    secret: 's3Cur3',
    name: 'session', // default: connect.sid
    resave: false,
    saveUninitialized: true,
    cookie: {
      path: '/', // default
      httpOnly: true, // default
      maxAge: 10 * 1000, // 10sec
    },
  })
);

各設定項目は、こちらに記載があります。

express-session / Options

Cookieの部分はさておき、その他の項目をいくつか見てみましょう。

  • name … Cookie名。デフォルトではconnect.sidだが、変更が推奨されている
  • resave … trueにすると、リクエスト中にセッションが変更されなかった場合でも、強制的にセッションストアに保存しなおす
    • デフォルト値はtrueだが、セッションストアとユースケースに応じて選択する
    • セッションストアにtouchが実装されておらず、かつセッションに有効期限がある場合は設定が必要な可能性がある
  • saveUninitialized … 初期化されていないセッションを、強制的にセッションストアに保存する
    • デフォルト値はtrueだが、明示的に設定することを推奨
  • secret … 必須項目。セッションIDを保存するCookieの署名に使用される
    • 今回はハードコードしているが、シークレットは環境変数等から取得し、ランダムな値にすることを推奨

その他にもいろいろオプションがあります。

Cookieに関する設定は、HTTPとしてのCookieに対する内容ほぼそのままなので、今回書いていないものも含めて割愛…。

有効期限は、maxAgeかexpiresで指定することになります。が、maxAgeのみを使うことが推奨されているみたいです。

    cookie: {
      path: '/', // default
      httpOnly: true, // default
      maxAge: 10 * 1000, // 10sec
    },

今回は、10秒にしておきました。CookienのmaxAgeに反映されます。

続いて。セッションに保存するデータはどうしようかな、と思ったのですが、初回アクセス時の日時とアクセスする度に
増えるカウンターをミドルウェアの処理で設定することにしました。

app.use((req, res, next) => {
  if (!req.session.firstAccessTime) {
    const now = new Date();
    req.session.firstAccessTime = now.toISOString();
  }

  req.session.counter = req.session.counter ? req.session.counter + 1 : 1;

  next();
});

あとは、POSTされたメッセージを保存したり、取得したり。

app.post('/message', (req, res) => {
  const message = req.body['message'];

  req.session.message = message;

  res.send({
    firstAccessTime: req.session.firstAccessTime,
    counter: req.session.counter,
    message: req.session.message,
  });
});

app.get('/message', (req, res) => {
  res.send({
    firstAccessTime: req.session.firstAccessTime,
    counter: req.session.counter,
    message: req.session.message ? req.session.message : 'Hello World',
  });
});

この時に、セッション内のデータもレスポンスに含めてみます。

バインドするポートは、起動引数に応じて変更できるようにしていますが、今回はそれほど意味はありません。

let port: number;

const args = process.argv.slice(2);

if (args.length == 0) {
  port = 3000;
} else {
  port = parseInt(args[0], 10);
}

完成したので、動作確認しましょう。

ビルド。

$ npx tsc --project .

起動。

$ node dist/app.js
[2021-12-28T17:04:26.565Z] start server[3000]

とりあえず、アクセス。

$ curl -c cookie.txt -b cookie.txt -i localhost:3000/message
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 82
ETag: W/"52-sYkTynMndBqQZveBjfEAdNjDMqw"
Set-Cookie: session=s%3AlMjoQ202glCfSgGGWHnKcDL72lPLVGle.VvlWEocjst3r9hNWO2xXFEfQT2FrlXnJ5o33VmGQi7c; Path=/; Expires=Tue, 28 Dec 2021 17:05:36 GMT; HttpOnly
Date: Tue, 28 Dec 2021 17:05:26 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"firstAccessTime":"2021-12-28T17:05:26.716Z","counter":1,"message":"Hello World"}

メッセージを保存していなくても、デフォルトのメッセージ(Hello World)が返ってきます。

10秒経過するまでは、初回アクセスの日時は同じですね。

$ curl -c cookie.txt -b cookie.txt -i localhost:3000/message
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 82
ETag: W/"52-vyS+8+D6lA6S21HrzlrP2QkfBcg"
Set-Cookie: session=s%3AlMjoQ202glCfSgGGWHnKcDL72lPLVGle.VvlWEocjst3r9hNWO2xXFEfQT2FrlXnJ5o33VmGQi7c; Path=/; Expires=Tue, 28 Dec 2021 17:05:42 GMT; HttpOnly
Date: Tue, 28 Dec 2021 17:05:32 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"firstAccessTime":"2021-12-28T17:05:26.716Z","counter":2,"message":"Hello World"}

カウンターは上がっていきます。

ここで、メッセージを保存。

$ curl -XPOST -c cookie.txt -b cookie.txt -i -H 'Content-Type: application/json' localhost:3000/message -d '{"message": "Hello Express-Session"}'
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 92
ETag: W/"5c-IKcfg7BQZtni1wQBP2yyNEfkayM"
Set-Cookie: session=s%3AlMjoQ202glCfSgGGWHnKcDL72lPLVGle.VvlWEocjst3r9hNWO2xXFEfQT2FrlXnJ5o33VmGQi7c; Path=/; Expires=Tue, 28 Dec 2021 17:05:47 GMT; HttpOnly
Date: Tue, 28 Dec 2021 17:05:37 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"firstAccessTime":"2021-12-28T17:05:26.716Z","counter":3,"message":"Hello Express-Session"

確認。

$ curl -c cookie.txt -b cookie.txt -i localhost:3000/message
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 92
ETag: W/"5c-3PLziySobG9OtC4EEA5aV6oo32k"
Set-Cookie: session=s%3AlMjoQ202glCfSgGGWHnKcDL72lPLVGle.VvlWEocjst3r9hNWO2xXFEfQT2FrlXnJ5o33VmGQi7c; Path=/; Expires=Tue, 28 Dec 2021 17:05:54 GMT; HttpOnly
Date: Tue, 28 Dec 2021 17:05:44 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"firstAccessTime":"2021-12-28T17:05:26.716Z","counter":4,"message":"Hello Express-Session"}

セッションに保存したメッセージが取得できました。

なお、10秒以上経過すると、CookieのmaxAgeを超えるのでセッションが取得できなくなります。

$ curl -c cookie.txt -b cookie.txt -i localhost:3000/message
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 82
ETag: W/"52-fTkTw7lMrghKrv3y6JTcAEkRQBM"
Set-Cookie: session=s%3ARZs7HJyez1JS4OUEv0RyN5NBxbV8xhBM.IkRYnD2xoZw%2BVjgsIhvDX%2Fl0zpOD2mrseTPpLKrQ33U; Path=/; Expires=Tue, 28 Dec 2021 17:06:06 GMT; HttpOnly
Date: Tue, 28 Dec 2021 17:05:56 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"firstAccessTime":"2021-12-28T17:05:56.916Z","counter":1,"message":"Hello World"}

これで、基本的な動作確認はできましたね。

ちなみに、もうひとつサーバーを起動して

$ node dist/app.js 3001

こちらに同じCookieを使ってアクセスしても、別のセッションになることが確認できます。

$ curl -c cookie.txt -b cookie.txt localhost:3001/message

メモリセッションなので、当然ですが。この後は、こちらも確認していきます。

補足

この時点で、いくつか補足をします。

SessionDataの型宣言のマージ

まず、こちらのSessionDataに対する型宣言について。

declare module 'express-session' {
  interface SessionData {
    firstAccessTime: string;
    counter: number;
    message: string;
  }
}

こちらは、以前は不要だったみたいなのですが、express-sessionの1.17.1以降はSessionDataに明示的に型宣言を
しないと、セッションにデータを保存、データを取得しようとして属性にアクセスするコードを書くと、コンパイルが
通らなくなります。

src/app.ts:33:17 - error TS2339: Property 'firstAccessTime' does not exist on type 'Session & Partial<SessionData>'.

33     req.session.firstAccessTime = now.toISOString();
                   ~~~~~~~~~~~~~~~

issueにも上がっていましたが、これは意図的な変更のようです。

Incorrect typings for session object · Issue #49941 · DefinitelyTyped/DefinitelyTyped · GitHub

もともとのSessionDataの型定義は以下なのですが、コメントにあるように利用側でも型宣言を追加、マージして
使用することを想定しているみたいです。

    /**
     * This interface allows you to declare additional properties on your session object using [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html).
     *
     * @example
     * declare module 'express-session' {
     *     interface SessionData {
     *         views: number;
     *     }
     * }
     *
     */
    interface SessionData {
        cookie: Cookie;
    }

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/9d35fe5b9f3bac0a30c252a61e411f84a6b7ba5d/types/express-session/index.d.ts#L202-L215

TypeScriptの型宣言のマージについては、こちら。

TypeScript: Documentation - Declaration Merging

これにはいろんな声があるみたいで、express-sessionの型宣言は1.17.0を利用するといった方法も案内されていましたが。
設計の意図とは反する形ですね。

デフォルトのインメモリセッションストアについて

express-sessionがデフォルトで使用するインメモリセッションストアには、こんな注意事項がありました。

Warning The default server-side session storage, MemoryStore, is purposely not designed for a production environment. It will leak memory under most conditions, does not scale past a single process, and is meant for debugging and developing.

GitHub - expressjs/session: Simple session middleware for Express

ほとんどのケースでメモリリークするということなのですが、どういうことでしょう?

ソースコードを見ていた感じ、明示的にセッションをdestroyしない限り、セッションデータが残り続けるみたいですね。
セッションIDを保持しているmaxAgeを過ぎてしまうと、セッションが追跡できなくなるので消すこともできず、
メモリリークします、と…。

というわけで、本番環境などではこのセッションストアは使わずに、他のセッションストアを使いましょう、と。

connect-redisを使う

express-sessionで利用できるセッションストアのうち、Redisをバックエンドに使うものがconnect-redisです。

GitHub - tj/connect-redis: Redis session store for Connect

今回は、こちらを使ってセッションデータをRedisに保存してみます。

connect-redisを使うには、Redisクライアントを別にインストールする必要があります。使用できるのは、以下の3つです。

どうしようかなと思ったのですが、今回はioredisを使うことにします。

新しいNode.jsプロジェクトを作成して、先ほどと同じようにExpressの後にexpress-sessionと型宣言をインストール。

$ npm i express-session
$ npm i -D @types/express-session

続いて、connect-redisとioredisをインストールします。

$ npm i ioredis connect-redis
$ npm i -D @types/ioredis @types/connect-redis

今回の依存関係は、こちら。

  "devDependencies": {
    "@types/connect-redis": "^0.0.18",
    "@types/express": "^4.17.13",
    "@types/express-session": "^1.17.4",
    "@types/ioredis": "^4.28.5",
    "@types/node": "^17.0.5",
    "prettier": "2.5.1",
    "typescript": "^4.5.4"
  },
  "dependencies": {
    "connect-redis": "^6.0.0",
    "express": "^4.17.2",
    "express-session": "^1.17.2",
    "ioredis": "^4.28.2"
  }

ソースコードは、先ほどのソースコードを元にconnect-redisを使うように修正します。

src/app.ts

import express from 'express';
import session from 'express-session';
import connectRedis from 'connect-redis';
import Redis from 'ioredis';

declare module 'express-session' {
  interface SessionData {
    firstAccessTime: string;
    counter: number;
    message: string;
  }
}

const app = express();
let port: number;

const args = process.argv.slice(2);

if (args.length == 0) {
  port = 3000;
} else {
  port = parseInt(args[0], 10);
}

const RedisStore = connectRedis(session);
const redisClient = new Redis({
  host: '172.17.0.2',
  port: 6379,
  password: 'redispass',
});

app.set('trust proxy', 1);
app.use(
  session({
    secret: 's3Cur3',
    name: 'session', // default: connect.sid
    resave: false,
    saveUninitialized: true,
    cookie: {
      path: '/', // default
      httpOnly: true, // default
      maxAge: 10 * 1000, // 10sec
    },
    store: new RedisStore({ client: redisClient }),
  })
);

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use((req, res, next) => {
  if (!req.session.firstAccessTime) {
    const now = new Date();
    req.session.firstAccessTime = now.toISOString();
  }

  req.session.counter = req.session.counter ? req.session.counter + 1 : 1;

  next();
});

app.post('/message', (req, res) => {
  const message = req.body['message'];

  req.session.message = message;

  res.send({
    firstAccessTime: req.session.firstAccessTime,
    counter: req.session.counter,
    message: req.session.message,
  });
});

app.get('/message', (req, res) => {
  res.send({
    firstAccessTime: req.session.firstAccessTime,
    counter: req.session.counter,
    message: req.session.message ? req.session.message : 'Hello World',
  });
});

app.listen(port, () => {
  console.log(`[${new Date().toISOString()}] start server[${port}]`);
});

変更部分は、モジュールのインポート。

import connectRedis from 'connect-redis';
import Redis from 'ioredis';

Redisへ接続するためのクライアントの作成。

const RedisStore = connectRedis(session);
const redisClient = new Redis({
  host: '172.17.0.2',
  port: 6379,
  password: 'redispass',
});

そして、セッションミドルウェアへのstore指定ですね。こちらで、先ほど作成したクライアントを使います。

app.use(
  session({
    secret: 's3Cur3',
    name: 'session', // default: connect.sid
    resave: false,
    saveUninitialized: true,
    cookie: {
      path: '/', // default
      httpOnly: true, // default
      maxAge: 10 * 1000, // 10sec
    },
    store: new RedisStore({ client: redisClient }),
  })
);

設定内容については、こちらを参照。

connect-redis / RedisStore(options)

今回はclientのみを指定しているのですが、cookie.expiresを指定している場合はそれがttlとしても使用されます。

と書くと、今回はcookie.maxAgeのみを指定しているので効果がなさそうに思えます。実際、connect-redisが見ているのも
cookie.expiresです。

https://github.com/tj/connect-redis/blob/v6.0.0/lib/connect-redis.js#L138-L147

ですが、express-session側でcookie.maxAgeを設定するとcookie.expiresも同時に設定するので、これでも問題
なさそうです。

session/cookie.js at v1.17.2 · expressjs/session · GitHub

このあたりも、動作確認時に見ていきましょう。

では、ビルド。

$ npx tsc --project .

サーバーを2つ起動します。

$ node dist/app.js
[2021-12-28T17:09:58.042Z] start server[3000]


$ node dist/app.js 3001
[2021-12-28T17:09:59.014Z] start server[3001]

セッションストアがメモリだった時と同じ操作を、アクセスするサーバーを交互にしながら試してみましょう。

まずは1つ目のサーバーにアクセス。

$ curl -c cookie.txt -b cookie.txt -i localhost:3000/message
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 82
ETag: W/"52-yJLufkq12oSrS08zelcHcp3KZFw"
Set-Cookie: session=s%3AkTnWdu3my-RutEu4qW2ppytVowE6exsk.S8lGv9ThCcOkGxQxAkPBpj8i0zq7J9ANLre2hcPQ8SQ; Path=/; Expires=Tue, 28 Dec 2021 17:11:05 GMT; HttpOnly
Date: Tue, 28 Dec 2021 17:10:55 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"firstAccessTime":"2021-12-28T17:10:55.322Z","counter":1,"message":"Hello World"}

2つ目のサーバーにアクセス。

$ curl -c cookie.txt -b cookie.txt -i localhost:3001/message
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 82
ETag: W/"52-Cr32RdSZjDB1tDYcFHwEedQGAjI"
Set-Cookie: session=s%3AkTnWdu3my-RutEu4qW2ppytVowE6exsk.S8lGv9ThCcOkGxQxAkPBpj8i0zq7J9ANLre2hcPQ8SQ; Path=/; Expires=Tue, 28 Dec 2021 17:11:11 GMT; HttpOnly
Date: Tue, 28 Dec 2021 17:11:01 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"firstAccessTime":"2021-12-28T17:10:55.322Z","counter":2,"message":"Hello World"}

同じセッションを見ていることが確認できます。

ここで、1つ目のサーバーに対してメッセージを保存。

$ curl -XPOST -c cookie.txt -b cookie.txt -i -H 'Content-Type: application/json' localhost:3000/message -d '{"message": "Hello Express-Session"}'
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 92
ETag: W/"5c-emnmjDwUdhAq5Z49Escy25ABU00"
Set-Cookie: session=s%3AkTnWdu3my-RutEu4qW2ppytVowE6exsk.S8lGv9ThCcOkGxQxAkPBpj8i0zq7J9ANLre2hcPQ8SQ; Path=/; Expires=Tue, 28 Dec 2021 17:11:16 GMT; HttpOnly
Date: Tue, 28 Dec 2021 17:11:06 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"firstAccessTime":"2021-12-28T17:10:55.322Z","counter":3,"message":"Hello Express-Session"}

2つ目のサーバーに対して、参照できるか確認。

s$ curl -c cookie.txt -b cookie.txt -i localhost:3001/message
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 92
ETag: W/"5c-x1y1bK4LdcEFThWoEN+fI3fnu6s"
Set-Cookie: session=s%3AkTnWdu3my-RutEu4qW2ppytVowE6exsk.S8lGv9ThCcOkGxQxAkPBpj8i0zq7J9ANLre2hcPQ8SQ; Path=/; Expires=Tue, 28 Dec 2021 17:11:21 GMT; HttpOnly
Date: Tue, 28 Dec 2021 17:11:11 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"firstAccessTime":"2021-12-28T17:10:55.322Z","counter":4,"message":"Hello Express-Session"}

サーバーを跨いで、セッションが操作できていますね。

10秒以上経過すると、CookieのmaxAgeを超えるのでセッションが取得できなくなるところも同じです。

s$ curl -c cookie.txt -b cookie.txt -i localhost:3000/message
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 82
ETag: W/"52-TZMcIC3EIc9lkB8W/R10PtrgYVg"
Set-Cookie: session=s%3A2TySiWJl8xIwdMq1-gR1cmNXOzJmLSak.dl0PVDfh9LAqIq6ec6kKLIrZLdeXiWiAkBFOp4GzsBM; Path=/; Expires=Tue, 28 Dec 2021 17:11:35 GMT; HttpOnly
Date: Tue, 28 Dec 2021 17:11:25 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"firstAccessTime":"2021-12-28T17:11:25.742Z","counter":1,"message":"Hello World"}

Redis側を確認する

connect-redisを使ってRedisに保存したデータがどうなっているか、ちょっと気になるところです。
確認してみましょう。

こんな感じで、とりあえずセッションを2つ作ります。アクセスするサーバーを変えているのは、なんとなくです。

$ curl -c cookie.txt -b cookie.txt -i localhost:3000/message
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 82
ETag: W/"52-DLTqL9qW8d2JlmuOtlrCspXxUPU"
Set-Cookie: session=s%3AdEMW9ngNDBJi_OUldTehMDEF4Igqplw3.NVZiKzWyVfNayzBJOPqRpg1bhB0Py8y4RisBQRxefzY; Path=/; Expires=Tue, 28 Dec 2021 17:19:15 GMT; HttpOnly
Date: Tue, 28 Dec 2021 17:19:05 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"firstAccessTime":"2021-12-28T17:19:05.638Z","counter":1,"message":"Hello World"}

$ curl -c cookie2.txt -b cookie2.txt -i localhost:3001/message
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 82
ETag: W/"52-CdWxLXIyKUZWlDXpISXDUgSZtrA"
Set-Cookie: session=s%3APxUkTEILqMR32DUkBV3f2cYu-sosa8-o.xgbiMNzt9VpbyMjRGMhUAFw8xz4JyOW9k4rF0yaTJjs; Path=/; Expires=Tue, 28 Dec 2021 17:19:15 GMT; HttpOnly
Date: Tue, 28 Dec 2021 17:19:05 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"firstAccessTime":"2021-12-28T17:19:05.647Z","counter":1,"message":"Hello World"}

このまま放っておくと、セッションの有効期限が切れてしまうので、watchコマンドでアクセスし続けるように
しておきます。

$ watch -n 1 'curl -c cookie.txt -b cookie.txt -i localhost:3000/message && curl -c cookie2.txt -b cookie2.txt -i localhost:3001/message'

この裏で、Redis CLIでログイン。

$ bin/redis-cli -a redispass

キーの一覧を見てみます。

127.0.0.1:6379> keys *
1) "sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3"
2) "sess:PxUkTEILqMR32DUkBV3f2cYu-sosa8-o"

先ほどのSet-Cookieヘッダーの内容を見ると、なんとなくセッションIDとRedisのキーが紐付けられそうですね。

Set-Cookie: session=s%3AdEMW9ngNDBJi_OUldTehMDEF4Igqplw3.NVZiKzWyVfNayzBJOPqRpg1bhB0Py8y4RisBQRxefzY; Path=/; Expires=Tue, 28 Dec 2021 17:19:15 GMT; HttpOnly


Set-Cookie: session=s%3APxUkTEILqMR32DUkBV3f2cYu-sosa8-o.xgbiMNzt9VpbyMjRGMhUAFw8xz4JyOW9k4rF0yaTJjs; Path=/; Expires=Tue, 28 Dec 2021 17:19:15 GMT; HttpOnly

キーに対するTTLを見てみます。

127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3
(integer) 9

CookieのmaxAgeが反映されていそうな感じですね。

キーに対する値はどうでしょうか。

127.0.0.1:6379> get sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3
"{\"cookie\":{\"originalMaxAge\":10000,\"expires\":\"2021-12-28T17:21:11.044Z\",\"httpOnly\":true,\"path\":\"/\"},\"firstAccessTime\":\"2021-12-28T17:19:05.638Z\",\"counter\":112}"

Cookieとセッションの内容が、JSONになって入っている感じですね。

これは、デフォルトのシリアライザーがJSONだからでしょう。

connect-redis / RedisStore(options) / serializer

ここで、watchコマンドの実行を停止すると、アクセスがなくなるのでTTLが減少していきます。

127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3
(integer) 7
127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3
(integer) 6
127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3
(integer) 4
127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3
(integer) 3
127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3
(integer) 2
127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3
(integer) 1
127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3
(integer) 0
127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3
(integer) -2

最終的に0になった後は、データもなくなってしまいます。

127.0.0.1:6379> keys *
(empty array)


127.0.0.1:6379> get sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3
(nil)

これで、セッションの有効期限が切れた後は、Redisからもデータがなくなることが確認できました。

まとめ

express-sessionを使って、サーバーサイドのセッションを使う方法と、Redisを組み合わせる方法を確認してみました。

基本的な知識としては押さえられたのかなという感じがするのと、デフォルト設定の落とし穴なども把握できて
良かったかなと思います。