Passportを使ってUser Login機能を追加する(Session編)
引き続きUser Login画面を追加する方法を勉強しながら教わったことや調べたことをメモしていく。
相変わらずPassport.jsを実際に設定する手前のところですでに分からないことが多すぎて全然本題に辿り着けない💦 普通のシンプルなユーザー登録とユーザーログインの機能を追加したいだけなんだけどね。
express-sessionのインストール
まずは以下インストールしておく。これをしないと後の工程でエラーが出てくる。
npm i express-session
npm i --save-dev @types/express-session
Session
セッションとは、ユーザーがWebサイトを訪れてから離れるまでの間、サーバー側で一時的にユーザーの情報を保存する仕組み。今回は主にユーザーの「ログイン状態」を管理するために使用する。
具体的には、ユーザーがログインに成功すると、サーバー側でセッションデータ(ユーザーIDやログイン状態など)が作成され、ブラウザにはそのセッションを特定するための「セッションID」がクッキーとして保存される。以降のリクエストでは、ブラウザが自動的にこのセッションIDをサーバーに送信し、サーバーはそのIDに紐づくセッションデータを参照することで、ユーザーのログイン状態を維持する。
なんか難しいなぁ、と思ったけど逆にセッションを使わないとどうなるのかを想像すると、その役割が分かりやすくなった。セッションを使わない場合、ページを移動したりAPIを呼び出したりする度に、ユーザーは認証情報(メールアドレスとパスワード)の入力を求められ、毎回データベースのユーザー情報と照らし合わせなければならないという、あり得ないくらい使いづらいアプリになってしまう。
というわけで5行目以下を追加してセッションの設定をする。
// Initialize Express app
const app = express();
app.use(express.json());
//express-sessionの設定
app.use(
session({
secret: process.env.SESSION_SECRET, // セッションを暗号化するためのシークレットキー
resave: false, // セッションストアの保存の最適化
saveUninitialized: false,// 未初期化のセッションを保存しない
cookie: {
secure: process.env.NODE_ENV === "production", // 本番環境ではHTTPSのみ許可
httpOnly: true, // JavaScriptからクッキーにアクセスできないようにする
maxAge: 24 * 60 * 60 * 1000, // クッキーの有効期限(24時間)
},
})
);
設定の中身を一つずつ確認していく。
secret
envファイル(絶対にコミットしてはいけない)に長くてランダムで推測しにくいシークレットキーを設定する。シークレットキー、何でもいいのでキーボードを猿のようにバチバチ適当に叩いてもいいのだけれど、スマートにやりたい場合は、Node.js のcryptoを使うと適当な文字を生成してくれるので、出てきた文字をenvファイルに SESSION_SECRETを設定すればOK。
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
ちなみに、.envファイルでは、シングルクォート(’)もダブルクォート(”)も必要ないらしい。ただしスペースや特殊文字を含まない場合に限る。何となくつけちゃいますけどね。
SESSION_SECRET=my-secret-key # OK
SESSION_SECRET="my-secret-key" # OK
SESSION_SECRET='my-secret-key' # OK
そして、secret: process.env.SESSION_SECRET,
を渡したのだけれど、タイプエラー発生。
Type ‘string | undefined’ is not assignable to type ‘CipherKey | CipherKey[]’.
Type ‘undefined’ is not assignable to type ‘CipherKey | CipherKey[]’.
envファイルに絶対あるのになぁ、undefinedじゃないのになぁ。
タイプエラーを黙らせるために手前にthrow new Errorを追加。これでエラー解消。
if (!process.env.SESSION_SECRET) {
throw new Error('SESSION_SECRET environment variable is required');
}
// Initialize Express app
const app = express();
app.use(express.json());
//express-sessionの設定
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
})
);
resave
続いてresave
について。true
かfalse
かを設定するが、結論から言ってしまうとfalse
のケースがほとんどとのこと。true
にするとセッションを読むだけ(セッションに変更加えない)の場合でもセッションが再保存されて有効期限が更新されるので、true
にするべきでは?と思ってしまうが、false
(変更があったセッションのみ保存)でいいらしい。特にセッションの保存先(セッションストア)がPrismaなどの広く一般的に使われているものを使う場合は、resave: false
でもユーザーがページを見ただけで自動的にセッションの有効期限(TTL: Time To Live)を更新してくれるTouch機能というのがついているのでfalseで十分。
むしろ、trueにしてしまうと、1000ユーザーが同時アクセスした場合、それだけでサーバーでは1000回のセッション保存が発生してしまうので、セッションストアへの無駄な書き込みが発生。データベースの負荷が増大し、レスポンス時間が遅くなる可能性がある、ということでfalseが推奨。
saveUninitialized
saveUninitialized
もtrue
かfalse
かを設定する項目。これは具体例の方がわかりやすい。
買い物サイトでログインせずに商品を買い物カゴに入れて行き、後からログインや新規会員登録を許すようなサイトではsaveUninitialized: true
にすると未ログインでもセッションIDが発行され、カートが維持されるようにできる。
一方で、今回のようにログインが必須のアプリであればログインしたユーザーのみセッション作成されるようにsaveUninitialized: false
とすることで、不要なセッションの作成を防ぎ、サーバーのメモリを節約できる。
cookie設定
secure
secureの設定に関してはいつか本番環境の設定をするときにNODE_ENV="production"
とすればOK。
trueの場合、HTTPSでのみクッキーが送信される。ただ、開発中はhttp://localhost:5173/
などHTTPでもクッキーが欲しいので、このような設定をしている。
secure: process.env.NODE_ENV === "production",
httpOnly
JavaScriptからcookieへのアクセスができないようにtrueにする。悪意のあるJavaScriptからの攻撃(XSS攻撃)からセッションを守るため、今回のようなユーザー認証情報を含むものなどは必ずtrueにする。
maxAge
Cookieのデータ保持期間を設定する。ユーザーがブラウザを閉じても、この時間内であればセッションは維持されるが、ここで設定した時間を経過すると失効するのでユーザーは再度ログインが必要になる。とりあえず今回は24時間で設定する。1時間でも30日でもいいのだがセキュリティと利便性のバランスを取りつつ、必要最小限の権限とデータ保持に留めることが推奨されている。
Store
ログインが成功すると、ユーザーの認証情報が保存される。storeを指定しないとデフォルトでサーバーのメモリ上にセッション情報が保存されることになるが、不都合が多いので、PrismaSessionStoreのような永続的なストレージを保存場所に使用するのが一般的。(不都合の例としては、サーバー再起動するとセッションが消えてしまったり、複数サーバーを使用するケースでは、あるサーバーでログインしても、負荷分散で別のサーバーにリクエストが振り分けられると、ログイン状態が維持されない問題が発生することなど。)
// Initialize Express app
const app = express();
app.use(express.json());
//express-sessionの設定
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000,
},
store: new PrismaSessionStore(db, {
checkPeriod: 5 * 60 * 1000,
dbRecordIdIsSessionId: true,
dbRecordIdFunction: undefined,
sessionModelName: "userSession"
})
})
);
中の項目も確認していく。
checkPeriod
期限切れのセッションをチェックする間隔。上記の例では5分(5 * 60 * 1000ミリ秒)ごとにチェックが行われ、古くなって使われていないセッションを自動的に削除するための仕組み。
dbRecordIdIsSessionId
セッションIDをデータベースのレコードIDとして使用するかどうか。trueでOK。
dbRecordIdFunction
セッションIDの生成方法に関する設定で、undefined
の場合デフォルトの生成方法が使用される。
sessionModelName
データベース内でセッション情報を保存するテーブルの名前を指定できる。
Prisma
セッション用の新しいテーブルを用意する。model名は先ほど指定した名前と同じにする。
項目は使用しているライブラリ(今回はこちら)によって違うこともあるかもしれない。
// schema.prismaファイルにmodelを追加する
model UserSession {
id String @id
sid String @unique
data String
expiresAt DateTime @map("expires_at")
}
schemaを変えたので、データベースを作り直す。
長くなったので、今日はここまで💦
ハウステンボスに行ってきました🌷🌷🌷
オススメポイント
– 人が少ないのでゆったり過ごせる
– アーティストとの距離が近い🇺🇦🇭🇺
– 次に待っている人がいないとアトラクションを連続利用できる
– 広大なエリアをフル活用した謎解きが楽しい
