TypeORM を利用して Session を管理する
概要
Express の Session ストアとして、MySQL などのデータベースを TypeORM (v0.2) 経由で利用する場合の備忘録です。
TypeORM 設定
Modules
express-session
と connect-typeorm
をインストールします。
$ npm i @types/express-session express-session $ npm i connect-typeorm
ormconfig
ormconfig.js に DB の接続情報を記述します。
module.exports = [ { name: 'default', type: 'mysql', host: 'db', port: 3306, username: 'root', password: 'password', database: 'mydb', charset: 'utf8mb4', synchronize: false, logging: false, entities: [__dirname + '/src/entity/**/*.ts'], migrations: [__dirname + '/src/migration/**/*{.ts,.js}'], subscribers: [], cli: { entitiesDir: 'src/entity', migrationsDir: 'src/migration', subscribersDir: '', }, } ];
Entity
Session の Entity を作成します。
import { ISession } from "connect-typeorm"; import { Column, Entity, Index, PrimaryColumn } from "typeorm"; @Entity() export class Session implements ISession { @Index() @Column("bigint") public expiredAt = Date.now(); @PrimaryColumn("varchar", { length: 255 }) public id = ""; @Column("text") public json = ""; }
DB Migration
package.json に、DB migration 用のスクリプトを記述しておきます。
"scripts": { "migration:generate": "ts-node $(npm bin)/typeorm migration:generate", "migration:run": "ts-node $(npm bin)/typeorm migration:run" },
DB migration ファイルを作成のため以下を実行します。
Session
の部分は任意で ok です。
$ npm run migration:generate -n Session
このコマンドにより、以下のようなファイルが作成されます。
import {MigrationInterface, QueryRunner} from "typeorm"; export class Session1650179415742 implements MigrationInterface { name = 'Session1650179415742' public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`CREATE TABLE \`session\` (\`expiredAt\` bigint NOT NULL, \`id\` varchar(255) NOT NULL, \`json\` text NOT NULL, INDEX \`IDX_28c5d1d16da7908c97c9bc2f74\` (\`expiredAt\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`DROP INDEX \`IDX_28c5d1d16da7908c97c9bc2f74\` ON \`session\``); await queryRunner.query(`DROP TABLE \`session\``); } }
DB migration を実行します。
$ npm run migration:run
これにより DB に Session テーブルが作成されます。
> show columns from session; +------------+--------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+-------+ | expiredAt | bigint(20) | NO | MUL | NULL | | | id | varchar(255) | NO | PRI | NULL | | | json | text | NO | | NULL | | +------------+--------------+------+-----+---------+-------+ 3 rows in set (0.010 sec)
Express アプリ作成
Express から DB に接続し、Session の Read/Write を実行してみます。 ここでは、以下の簡易なログインアプリを作成しています。
- ページアクセス時に Session を確認し、有効な Session が無い場合は '/login' にリダイレクトする
- '/login' にてログインが成功した場合、Session を発行する
- ログアウトした場合、Session を削除する
import express, { Request, Response, NextFunction } from "express"; import session from "express-session"; import bodyParser from "body-parser"; import { createConnection } from "typeorm"; import { TypeormStore } from "connect-typeorm"; import { Session } from "./entity/Session"; // Add field `userId` in session declare module "express-session" { export interface SessionData { userId: string; } } const app = express(); const port = 3000; const AppServer = async (): Promise<void> => { // DB connection const connection = await createConnection(); const sessionRepository = connection.getRepository(Session); // Session settings app.use( session({ secret: "session-sample", resave: false, saveUninitialized: false, cookie: { path: "/", httpOnly: true, secure: false, maxAge: 86400000, }, store: new TypeormStore({ cleanupLimit: 2, limitSubquery: false, ttl: 3600, // 60 minutes }).connect(sessionRepository), }) ); app.use(bodyParser.urlencoded({ extended: true })); app.get("/login", (req: Request, res: Response) => { res.type("text/html").send( ` <form method="POST" action="/login"> <div>UserID<input type="text" name="userId"></div> <div>Password<input type="password" name="password"></div> <div><input type="submit" value="Login"></div> </form> ` ); }); app.post("/login", async (req: Request, res: Response) => { const { userId, password } = req.body; if (userId === "admin" && password === "password") { req.session.regenerate((err) => { req.session.userId = userId; res.redirect('/'); }); } else { res.redirect("/login"); } }); app.get("/logout", (req: Request, res: Response) => { req.session.destroy((err) => { res.redirect("/"); }); }); // If no valid session, redirect to login page app.use((req: Request, res: Response, next: NextFunction) => { if (req.session.userId) { next(); } else { res.redirect("/login"); } }); app.get("/", async (req: Request, res: Response) => { res.type("text/html").send( ` <div>Hello ${req.session.userId}</div> <div><a href="/logout">Logout</a></div> ` ); }); app.listen(port, () => { console.log(`App listening on port ${port}`); }); }; AppServer();
動作確認
Express アプリを起動し動作を確認します。
初回アクセス時
Session が無いため、ログインページにリダイレクトされます。
ログイン後
Session に保持した userId ("admin") が画面に表示されます。
DB を見ると、確かに Session 情報が保持されています。
> select * from session; +---------------+----------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ | expiredAt | id | json | +---------------+----------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ | 1650648411894 | iNJEcXhrQF8y4v-OXC72qVoqZH740vUK | {"cookie":{"originalMaxAge":86400000,"expires":"2022-04-23T16:26:51.881Z","secure":false,"httpOnly":true,"path":"/"},"userId":"admin"} | +---------------+----------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ Empty set (0.001 sec)
ログアウト後
DB から Session 情報が削除されたため、ログインページにリダイレクトされます。
> select * from session; Empty set (0.001 sec)