1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TISAdvent Calendar 2021

Day 11

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

Last updated at Posted at 2022-01-06

What's?

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

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

Compatible Session Stores

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

環境

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

$ node --version
v14.18.2


$ npm --version
6.14.15

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

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

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

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のプロジェクトを作成します。

$ npm init -y

Express、express-sessionexpress-mysql-sessionmysql2をインストールします。

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

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

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

作成したソースコード。

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();
});

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

let port;

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

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

express-mysql-session / With mysql2

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

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

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.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

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側のデータベースの状態を見てます。まだなにもありません。

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

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

$ node app.js


$ node app.js 3001

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

$ 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!!"}

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

$ 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側を見るとセッション用のテーブルが作られています。中身を見ると、セッションデータが保存されているのが確認できます。

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)

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

$ 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

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

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

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

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

mysql2node-mysql2)は、mysql互換のモジュールです。

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

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を使った認証方式に対応していないからですね。

issueもOpenのままです。

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

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

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

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?