express-mysql-sessionで、caching_sha2_passwordのままMySQL 8.0をセッションストレージにする

# What's？

Expressでサーバーサイドセッションを利用するには、`express-session`を使います。

https://github.com/expressjs/session

セッションデータを保存するストレージは、すでに実装済みのものを選ぶか、自分で作成することになります。

[Compatible Session Stores](https://github.com/expressjs/session#compatible-session-stores)

今回、この中から`express-mysql-session`を選択したのですが、`caching_sha2_password`認証方式のままMySQL 8.0と組み合わせるようにしてみます。

https://github.com/chill117/express-mysql-session

# 環境

今回の環境は、こちらです。

```shell
$ node --version
v14.18.2


$ npm --version
6.14.15
```

MySQLは172.21.67.190で動作しているものとし、バージョンは以下とします。

```shell
$ mysql --version
mysql  Ver 8.0.27 for Linux on x86_64 (MySQL Community Server - GPL)
```

データベースは`example`、ユーザーは`express_user`として作成しておきます。

```shell
mysql> create database example;
Query OK, 1 row affected (0.01 sec)

mysql> create user express_user@localhost identified by 'password';
Query OK, 0 rows affected (0.02 sec)

mysql> create user express_user@'%' identified by 'password';
Query OK, 0 rows affected (0.00 sec)

mysql> grant all privileges on example.* to express_user@localhost;
Query OK, 0 rows affected, 1 warning (0.01 sec)

mysql> grant all privileges on example.* to express_user@'%';
Query OK, 0 rows affected (0.00 sec)

mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)
```

# アプリケーションの作成

まずは、Node.jsのプロジェクトを作成します。

```shell
$ npm init -y
```

Express、`express-session`、`express-mysql-session`、`mysql2`をインストールします。

```json
$ npm i express express-session express-mysql-session
$ npm i mysql2
```

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

```json
  "dependencies": {
    "express": "^4.17.2",
    "express-mysql-session": "^2.1.7",
    "express-session": "^1.17.2",
    "mysql2": "^2.3.3"
  },
```

作成したソースコード。

```javascript:app.js
const express = require('express');
const session = require('express-session');
const mysql2 = require('mysql2/promise');
const MySQLStore = require('express-mysql-session')(session);

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

const app = express();

const address = 'localhost';

let port;

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

const mysqlOptions = {
  host: '172.21.67.190',
  port: 3306,
  user: 'express_user',
  password: 'password',
  database: 'example',
};

const connection = mysql2.createPool(mysqlOptions);
const mysqlSessionStore = new MySQLStore({}, connection);

app.use(
  session({
    secret: 'myCookieSecret',
    name: 'session',
    store: mysqlSessionStore,
    resave: false,
    saveUninitialized: true,
    cookie: {
      maxAge: 1 * 60 * 1000, // 1 min
    },
  })
);

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

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

  req.session.message = message;

  res.send('OK!!');
});

app.get('/get', (req, res) => {
  res.send({
    message: req.session.message,
  });
});

const server = app.listen(port, address, () => {
  console.log(`start express[${address}:${port}]`);
});

const cleanup = () => {
  mysqlSessionStore.close(() => {
    console.log('MySQL session store closed');
  });

  connection.end().then(() => {
    console.log('MySQL2 connection closed');
  });

  server.close(() => {
    console.log('HTTP server closed');
  });
};

process.on('SIGINT', () => {
  console.log('SIGTINT signal received: closing HTTP server');

  cleanup();
});

process.on('SIGTERM', () => {
  console.log('SIGTERM signal received: closing HTTP server');

  cleanup();
});
```

リッスンポートは、引数で指定できるようにしています。

```javascript
let port;

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

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

[express-mysql-session / With mysql2](https://github.com/chill117/express-mysql-session/tree/v2.1.7#with-mysql2)

```javascript
const mysql2 = require('mysql2/promise');
```

mysql2を使ったMySQLへの設定および、express-sessionの設定はこちら。
セッションの有効期限は、確認のため1分と短く設定しています。

```javascript
const mysqlOptions = {
  host: '172.21.67.190',
  port: 3306,
  user: 'express_user',
  password: 'password',
  database: 'example',
};

const connection = mysql2.createPool(mysqlOptions);
const mysqlSessionStore = new MySQLStore({}, connection);

app.use(
  session({
    secret: 'myCookieSecret',
    name: 'session',
    store: mysqlSessionStore,
    resave: false,
    saveUninitialized: true,
    cookie: {
      maxAge: 1 * 60 * 1000, // 1 min
    },
  })
);
```

セッション操作は、メッセージを保存・取得するだけのシンプルなものにしました。

```javascript
app.post('/set', (req, res) => {
  const message = req.body.message;

  req.session.message = message;

  res.send('OK!!');
});

app.get('/get', (req, res) => {
  res.send({
    message: req.session.message,
  });
});
```

また、停止時にはリソースの破棄をするようにしています。  
特に`mysql2`の場合は、コネクションプールをクローズしないとアプリケーションが停止しません…。

[express-mysql-session / Closing the session store](https://github.com/chill117/express-mysql-session/tree/v2.1.7#closing-the-session-store)

https://expressjs.com/en/advanced/healthcheck-graceful-shutdown.html

```javascript
const server = app.listen(port, address, () => {
  console.log(`start express[${address}:${port}]`);
});

const cleanup = () => {
  mysqlSessionStore.close(() => {
    console.log('MySQL session store closed');
  });

  connection.end().then(() => {
    console.log('MySQL2 connection closed');
  });

  server.close(() => {
    console.log('HTTP server closed');
  });
};

process.on('SIGINT', () => {
  console.log('SIGTINT signal received: closing HTTP server');

  cleanup();
});

process.on('SIGTERM', () => {
  console.log('SIGTERM signal received: closing HTTP server');

  cleanup();
});
```

# 動作確認

では、動作確認します。

最初にMySQL側のデータベースの状態を見てます。まだなにもありません。

```shell
mysql> use example;
Database changed
mysql> show tables;
Empty set (0.00 sec)

```

アプリケーションを2つ起動します。

```shell
$ node app.js


$ node app.js 3001
```

片方のサーバーでデータを保存し、もう片方から取り出してみます。

```shell
$ curl -i -b cookie1.txt -c cookie1.txt -XPOST -H 'Content-Type: application/json' localhost:3000/set -d '{"message": "Hello World!!"}'
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 4
ETag: W/"4-tBQOhWSjM740wHRnkZx9epHpxWg"
Set-Cookie: session=s%3AHu_qt68hk7hSv5KbUEYw71U63EIU2XwD.mAdpdbZaU8XmlgRdVWOkPeAgRlLYYSPYCPsnHdxwFH4; Path=/; Expires=Thu, 06 Jan 2022 10:19:29 GMT; HttpOnly
Date: Thu, 06 Jan 2022 10:18:29 GMT
Connection: keep-alive
Keep-Alive: timeout=5


$ curl -i -b cookie1.txt -c cookie1.txt localhost:3001/get
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 27
ETag: W/"1b-lVYCDPtlpFek/50knKXqrlR1pns"
Date: Thu, 06 Jan 2022 10:18:46 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"message":"Hello World!!"}
```

アクセスするサーバーの順番を逆にしてみます。

```shell
$ curl -i -b cookie2.txt -c cookie2.txt -XPOST -H 'Content-Type: application/json' localhost:3000/set -d '{"message": "Hello Express!!"}'
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 4
ETag: W/"4-tBQOhWSjM740wHRnkZx9epHpxWg"
Set-Cookie: session=s%3ALXeD53vNAjz2RajItYyAg-sBbuRBbYZv.vItD0oUwDLEvFrIFqztgFU85cmyqu2dQxQIoFRSVZV8; Path=/; Expires=Thu, 06 Jan 2022 10:20:06 GMT; HttpOnly
Date: Thu, 06 Jan 2022 10:19:06 GMT
Connection: keep-alive
Keep-Alive: timeout=5

OK!!


$ curl -i -b cookie2.txt -c cookie2.txt localhost:3000/get
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 29
ETag: W/"1d-WymH484iaIrCpzFCpbfFvKokYqE"
Date: Thu, 06 Jan 2022 10:19:27 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"message":"Hello Express!!"}
```

どちらも、データが保存、取得ができていますね。

この時、MySQL側を見るとセッション用のテーブルが作られています。中身を見ると、セッションデータが保存されているのが確認できます。

```sql
mysql> show tables;
+-------------------+
| Tables_in_example |
+-------------------+
| sessions          |
+-------------------+
1 row in set (0.00 sec)


mysql> select * from sessions;
+----------------------------------+------------+---------------------------------------------------------------------------------------------------------------------------------+
| session_id                       | expires    | data                                                                                                                            |
+----------------------------------+------------+---------------------------------------------------------------------------------------------------------------------------------+
| Hu_qt68hk7hSv5KbUEYw71U63EIU2XwD | 1641464386 | {"cookie":{"originalMaxAge":60000,"expires":"2022-01-06T10:19:29.985Z","httpOnly":true,"path":"/"},"message":"Hello World!!"}   |
| LXeD53vNAjz2RajItYyAg-sBbuRBbYZv | 1641464427 | {"cookie":{"originalMaxAge":60000,"expires":"2022-01-06T10:20:06.954Z","httpOnly":true,"path":"/"},"message":"Hello Express!!"} |
+----------------------------------+------------+---------------------------------------------------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)

```

セッションの有効期限が切れると、新規セッションになります。

```shell
$ curl -i -b cookie1.txt -c cookie1.txt localhost:3001/get
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 2
ETag: W/"2-vyGp6PvFo4RvsFtPoIWeCReyIC8"
Set-Cookie: session=s%3A93zZdF9CH5IluRMHOGlA6rh0RIavbOGN.mSZbE2Ltsxg06vbfbOmuiQkUh9rbPmWwAMiEyU2eyEU; Path=/; Expires=Thu, 06 Jan 2022 10:21:13 GMT; HttpOnly
Date: Thu, 06 Jan 2022 10:20:13 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{}


$ curl -i -b cookie2.txt -c cookie2.txt localhost:3000/get
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 2
ETag: W/"2-vyGp6PvFo4RvsFtPoIWeCReyIC8"
Date: Thu, 06 Jan 2022 10:20:33 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{}
```

簡単な動作確認は、こんな感じでしょうか。

# 設定について

`express-mysql-session`の設定項目は、こちら。

[express-mysql-session / Options](https://github.com/chill117/express-mysql-session/tree/v2.1.7#options)

デフォルト値はこちらのようです。

https://github.com/chill117/express-mysql-session/blob/v2.1.7/index.js#L61-L83

有効期限切れしたセッションのクリーニングやテーブルの作成有無、テーブル名やカラム名の調整が可能なようですが。

特に、コネクションプールのデフォルトサイズが`1`なので、実利用時には設定すべきかなと思います。

# express-mysql-sessionでmysql2を使う理由

`mysql2`（`node-mysql2`）は、`mysql`互換のモジュールです。

https://github.com/sidorares/node-mysql2

https://github.com/mysqljs/mysql

`express-mysql-session`はデフォルトでは`mysql`を使うのですが、こちらを使って素直にMySQL 8.0に接続すると、以下のエラーを見ることになると思います。

```shell
Error: ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server; consider upgrading MySQL client
    at Handshake.Sequence._packetToError (/path/to/node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14)
    at Handshake.ErrorPacket (/path/to/node_modules/mysql/lib/protocol/sequences/Handshake.js:123:18)
    at Protocol._parsePacket (/path/to/node_modules/mysql/lib/protocol/Protocol.js:291:23)
    at Parser._parsePacket (/path/to/node_modules/mysql/lib/protocol/Parser.js:433:10)
    at Parser.write (/path/to/node_modules/mysql/lib/protocol/Parser.js:43:10)
    at Protocol.write (/path/to/node_modules/mysql/lib/protocol/Protocol.js:38:16)
    at Socket.<anonymous> (/path/to/node_modules/mysql/lib/Connection.js:88:28)
    at Socket.<anonymous> (/path/to/node_modules/mysql/lib/Connection.js:526:10)
    at Socket.emit (node:events:390:28)
    at addChunk (node:internal/streams/readable:315:12)
    --------------------
    at Protocol._enqueue (/path/to/node_modules/mysql/lib/protocol/Protocol.js:144:48)
    at Protocol.handshake (/path/to/node_modules/mysql/lib/protocol/Protocol.js:51:23)
    at PoolConnection.connect (/path/to/node_modules/mysql/lib/Connection.js:116:18)
    at Pool.getConnection (/path/to/node_modules/mysql/lib/Pool.js:48:16)
    at Pool.query (/path/to/node_modules/mysql/lib/Pool.js:202:8)
    at MySQLStore.query (/path/to/node_modules/express-mysql-session/index.js:439:33)
    at MySQLStore.set (/path/to/node_modules/express-mysql-session/index.js:240:8)
    at Session.save (/path/to/node_modules/express-session/session/session.js:72:25)
    at Session.save (/path/to/node_modules/express-session/index.js:406:15)
    at ServerResponse.end (/path/to/node_modules/express-session/index.js:335:21)
```

これは、`mysql`がMySQL 8.0.4からデフォルトになった`caching_sha2_password`を使った認証方式に対応していないからですね。

https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-4.html

issueもOpenのままです。

https://github.com/mysqljs/mysql/issues/1959

回避方法としては、MySQL自体の設定、`create user`時の指定、`alter user`のいずれかで`mysql_native_password`認証方式に変更する必要があります。

ですが、このためだけに変更するのも…と。

`mysql2`であれば大丈夫なようなので、こちらにしておきました。