技術系

Cookie × Nextでログイン管理

技術系

こんにちは、なかにしです。
今回は、ログイン管理を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!!

タイトルとURLをコピーしました