What's?
Expressでサーバーサイドセッションを利用するには、express-session
を使います。
セッションデータを保存するストレージは、すでに実装済みのものを選ぶか、自分で作成することになります。
今回、この中から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-session
、express-mysql-session
、mysql2
をインストールします。
$ 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"
},
作成したソースコード。
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を使う理由
mysql2
(node-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
であれば大丈夫なようなので、こちらにしておきました。