こんにちは、なかにしです。
今回は、ログイン管理をCookieを使用して頑張ろうぜのコーナーです。
やりたかったこと
ざっくり言うと、Cookieを使用したログイン管理です。
ログイン時にSession-IDをブラウザのCookieに保存させ、
ブラウザを閉じても一定期間はログインなしでダイジョーブ!ってやつです。
実務だとJavaとかのAPIを叩くだけで自動でやってくれることが多いのですが、
仕組みを理解する為に、Nextだけでやってみよう!というチャレンジです。
ソースはこちらに上げてます。
デモ
1回ログインすればログアウトするまで、
何回ブラウザを閉じてもログイン情報を保持してくれます!
バージョン
・Docker 24.0.5
・Node 18.16.1
・Next 13.4.19
ざっくりの仕組み
ルートのページに到達時、「/api/login-check」というAPIを叩きます。
コードは以下。
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
// Cookieの取り出し
const sessionId = request.cookies.get("session-id");
// Cookieがない、もしくは空の場合
if (!sessionId || !sessionId.value) {
return new NextResponse(JSON.stringify({ auth: false },), { status: 401 })
}
// Cookieがあった場合
// user画面に遷移
return new NextResponse(JSON.stringify({ auth: true },), { status: 200 })
}
ここでCookieのあるなしを判定し、フロントに判定結果を返します。
フロントでは、その結果を使用して遷移先を変えます。
'use client'
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function Home() {
// ルーター
const router = useRouter();
/**
* ログインチェック
*/
const loginCheck = async () => {
const cookieResponse = await fetch("/api/login-check",
{
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (!cookieResponse.ok) {
return router.push("/login");
}
return router.push("/user");
}
// 初回はログインチェック
useEffect(() => {
loginCheck();
}, []);
}
ログイン画面では、IDとPWを入力し、「/api/login」というAPIにリクエストを送ります。
/api/loginでは、受け取ったIDを使用してDBのUserテーブルを検索し、
取ってきたPWと受け取ったPWを比較します。
PWが一致したら、セッションIDを作成し、Set-Cookieヘッダに指定します。
ここで、同時にSession-IDとユーザーのIDを紐づけ、Sessionというテーブルに保存します。
Set-Cookieヘッダに値をKey-Value形式で入れてレスポンスを返すことで、
ブラウザ側が自動でCookieに保存してくれます。
import mysql from 'mysql2/promise';
import { NextRequest, NextResponse } from "next/server";
import { v4 as uuidv4 } from 'uuid';
export async function POST(request: NextRequest) {
// Iパスを受け取る
const { id, pw } = await request.json();
// DB接続
const connection = await mysql.createConnection({
host: "db",
user: "root",
password: "root",
database: "sampleDB",
})
// 検索用SQL
const selectSql = `SELECT * FROM users WHERE id = ?`;
// Insert用SQL
const insertSql = `INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)`;
try {
// SELECTし、結果をアンパックする(metaデータいらないから)
const [rows]: any = await connection.execute(selectSql, [id]);
// パスワード照合
if (rows[0].password !== pw) {
// パスワード不一致でエラーを返却
return new NextResponse(JSON.stringify({ "errMsg": "IDかパスワードが違います。" }), {
status: 401,
})
}
// セッションIDの作成
const sessionID = uuidv4();
// セッション期限の設定(1時間後)
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 1);
// INSERTする
await connection.execute(insertSql, [sessionID, id, expiresAt]);
return new NextResponse(JSON.stringify({ auth: true }), {
status: 200,
headers: {
'Set-Cookie': `session-id=${sessionID}; HttpOnly;`
},
})
} catch (err) {
return new NextResponse(JSON.stringify({ "errMsg": "DB接続時にエラーが発生しました。" }), { status: 500 })
} finally {
await connection.end();
}
}
無事にCookieが設定されたら、「/user」へと画面遷移。
/userでは、「/api/user」というAPIヘリクエストを送ります。
/api/userは、Session-IDを基にDBを検索し、ユーザーのデータを返却します。
import mysql from 'mysql2/promise';
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
// Cookie取得
const sessionCookie = request.cookies.get("session-id")
if (!sessionCookie) {
return new NextResponse(JSON.stringify({ errMsg: "一定時間が経過したので、再ログインが必要です。" }), { status: 400 })
}
// sessionのcookieからidを取り出す
const sessionId = sessionCookie.value;
// SessionIdを使用してユーザーデータを取り出す
// DB接続
const connection = await mysql.createConnection({
host: "db",
user: "root",
password: "root",
database: "sampleDB",
})
// 検索用SQL
const selectSqlToSessions = `SELECT * FROM sessions WHERE id = ?`;
const selectSqlToUsers = `SELECT * FROM users WHERE id = ?`;
try {
// sessionテーブルからSELECTし、結果をアンパックする
const [sessionRows]: any = await connection.execute(selectSqlToSessions, [sessionId]);
// sessionsテーブルのuser_idを使用してusersテーブルから名前とかをSELECTし、結果をアンパックする
const [userRows]: any = await connection.execute(selectSqlToUsers, [sessionRows[0].user_id])
// 返却するユーザーデータをまとめる
const userData = {
name: userRows[0].name,
email: userRows[0].email
}
return new NextResponse(JSON.stringify({ userData }), { status: 200 });
} catch (err) {
return new NextResponse(JSON.stringify({ errMsg: "DB接続が失敗しました。" }), { status: 500 })
}
finally {
await connection.end();
}
}
/userでは「/api/user」から返ってきたユーザーデータを受け取り、
事前にuseContextでプロバイダーの値として渡しておいた
[user, setUser]というuseStateを使用して、全体でユーザー情報を管理します。
'use client'
import { useRouter } from "next/navigation";
import { useContext, useEffect } from "react";
import { UserContext } from "../context/UserContext";
export default function User() {
// 全体で管理しているユーザー情報
const { user, setUser } = useContext(UserContext);
// ルーター
const router = useRouter();
/**
* ユーザー情報を取りに行く関数
*/
const getUserData = async () => {
// user情報を取りに行く
const response = await fetch("/api/user")
const responseJson = await response.json();
if (!response.ok) {
alert(responseJson.errMsg);
return router.replace("/login")
}
// レスポンスからuserDataを取り出す
const { userData } = responseJson;
// 名前とemailをセット
setUser({ name: userData.name, email: userData.email })
}
/**
* ログアウト処理
*/
const logout = async () => {
// ログアウト用のAPIを叩く
const response = await fetch("/api/logout",
{
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const responseJson: any = await response.json();
if (!response.ok) {
alert("サーバーの調子が悪いようです。\nもう一度ログインしてください。");
return router.push("/login");
}
alert(responseJson.msg);
return router.push("/login");
}
// ユーザー情報の取得
useEffect(() => {
getUserData();
}, []);
return (
<div>
<p>userPageです。</p>
{user && (
<>
<p>ユーザー名:{user.name}</p>
<p>ユーザーMail:{user.email}</p>
<button onClick={logout}>ログアウト</button>
</>
)}
</div>
)
}
これでCookieを基に取得したユーザー情報を、全体で使用できるようになりました!
オワリ!
さいごに
軽々しくやり始めましたが、思った100倍労力がかかりました。
もうやりません。実務以外では。
今後は、Discordで動くBotを作成する予定です。
楽しみ!!
今回はここまで!
Enjoy Hacking!!