見出し画像

Next.jsとUpstashでセッション管理をしてみる【Redis】

こんにちは。
本日はNext.jsでクッキーを使用したセッション管理を実践してみたいと思います。
なお、認証に関連するセッションについては、各認証サービスの公式ドキュメントを参照してください。

まずは、簡単にセッションの概念について説明します。

1.セッションとは?

PHPやRubyなどのサーバーサイド言語を扱う場合、セッション管理は当たり前のように行われることです。
セッションは、サーバー側にデータを保存する仕組みです。
簡単に言えば、サーバーがユーザーの情報や状態を記憶しておくためのものです。
しかし、セッションについて理解する前に、ステートについても説明したいと思います。

2.ステートレスとステートフル

● ステートレス
ステートレスサーバーは、クライアントのセッション状態を保持せず、リクエストに対するレスポンスが一貫して同じです。

● ステートフル
ステートフルサーバーは、クライアントのセッション状態を保持し、リクエストに対するレスポンスが状態に応じて変化する。

ECサイトでは、会員登録していない状態でも商品をカートに追加することができます。
その後、サイトを閉じて再度訪れると、カートには商品が残っています。
このような状態を実現するために、クライアントからの情報をサーバー側で保持し、セッションを管理しています。
つまり、サーバーがクライアントのセッション状態を保持していることで、ステートフルな状態が実現されています。

3.一般的なセッション管理の流れ

では実際にセッション管理をする方法について説明します。
セッションを管理する方法はさまざまですが、一般的な方法は以下の通りです。

● セッションを識別するためのID(セッションID)を生成します。このセッションIDは一意の値であり、セッションを識別するためのキーとなります。

● セッションIDをクライアントのCookieに保存します。通常、セッションIDは暗号化や署名などの手法を用いて保護されます。

● クライアントからの通信リクエストがあると、サーバー側はCookieからセッションIDを読み取ります。

● 読み取ったセッションIDを使用して、サーバー側のセッションストレージ(データベースやメモリなど)からセッション情報を取得します。セッション情報にはユーザー情報や処理状況などの様々なデータが紐付いています。

● サーバー側は取得したセッション情報を利用してリクエストの処理を行います。必要に応じてセッション情報を更新または保存します。

● レスポンスがクライアントに返される際には、必要に応じてセッションIDをCookieに再度設定します。

4.セッション管理の方法

セッション管理の方法はいくつかありますが、代表的な3つを紹介しましょう。
それぞれの方法にはメリットとデメリットがあります。

● CookieStore(クッキー方式)
セッション情報を全て暗号化してクライアントのCookieに保存します。
Cookieは最大で4kBまでの情報しか保存できません。
クライアントのCookieに保存されているため、データベースへのアクセスが不要で処理が高速です。

● Redis(インメモリ方式)
Redisや他のKey-Value型のデータベースにセッションIDとセッション情報を保存します。
クライアントのCookieにはセッションIDのみを保存し、リクエストで受け取ったセッションIDを使用してRedisから情報を取得します。
処理が高速であり、情報漏洩のリスクが低いです。
ただし、コストがかかり、メモリを使い果たすと書き込みが全てエラーになってしまいます。

● DB(データベース方式)
インメモリ方式と同様にセッションIDとセッション情報を保存しますが、データベースに保存されます。
クライアントのCookieにはセッションIDのみを保存します。
クライアントからのリクエストで受け取ったセッションIDを使用してデータベースから情報を取得します。
インメモリやクッキーと比べてデータの取得が遅くなる場合があります。
セッションの期限付き発行やセッションの永続性に優れています。

今回はセッション管理にRedis(インメモリ方式)を使用します。

5.Next.jsとセッション管理

Next.jsでは、サーバーサイドで処理されるライフサイクルを持つため、セッションを扱うことができます。

6.さっそくやってみる

まず、以下の順序で環境を構築していきます。

● Next.jsの環境を作成します。
● Upstashを使用してRedisのデータベースを作成します。
● Next.jsからRedisにアクセスできるように設定します。
● 必要なライブラリであるnookies、nanoid、ioredisをインストールします。
● セッション管理の処理を実装します。

7.ライブラリをインストールする

$ yarn add -D nookies nanoid ioredis

8.処理を書く

// utils/redisClient.ts

import Redis from 'ioredis';

//-----------------------------------------------------------------
// ioredisの初期化処理
//-----------------------------------------------------------------
export const redisClient = new Redis(`rediss://:${process.env.REDIS_PASSWORD ?? ''}@${process.env.REDIS_HOST ?? ''}:${process.env.REDIS_PORT ?? ''}`, {
  tls: { rejectUnauthorized: false }
});
// pages/index.tsx

import { parseCookies, setCookie } from 'nookies';
import { nanoid } from 'nanoid';
import { redisClient } from 'utils/redisClient';

//-----------------------------------------------------------
// component
//-----------------------------------------------------------
const Index = ({ session, datas, isResetStatus, weeks }: IProps) => {

  //-----------------------------------------------------------
  // カートに追加するハンドラー
  //-----------------------------------------------------------
  const onAddCartHandler = async (productId: number, productName: string, productPrice: number) => {
    try {
      const datas = await fetch('api/v1/card/add', {
        method: 'POST',
        body: JSON.stringify({
          productId, title, price
        }),
        headers: { 'Content-Type': 'application/json', Accept: '*/*' }
      });
      alert('カートに追加しました');
    } catch (e) {
      alert('カート追加に失敗しました');
    } finally {
      setLoading(false);
    }
  };

  return (
    <button
      onClick={() => {
        onAddCartHandler(9999, 'サンプル商品', 420);
      }}
    />
  );
}

//-----------------------------------------------------------
// ssr
//-----------------------------------------------------------
export const getServerSideProps = async (context: GetServerSidePropsContext) => {

  // contextを渡してcookie内の値を取得する
  const cookie = parseCookies(context);

  // cookie内のsessionIdがあるかどうかを判定
  if(cookie.sessionId) {

    // RedisからsessionIdを取得する
    const sessionId = await redisClient.hget(cookie.sessionId, 'sessionId');

    // Redisから取得できたsessionIdをもとになにか処理する
    // ここで実際の処理

  } else {

    // sessionIdを生成する
    const newSessionId = nanoid(35);

    // sessionIdをredisに保存してexpiredを7日間に設定する
    await this.redisClient.hsetnx(newSessionId, 'sessionId, newSessionId);
    await this.redisClient.expire(newSessionId, 60 * 60 * 24 * 7);

    // クライアント側のcookieにsessionIdを保存する
    setCookie(context, 'sessionId, newSessionId), {
      maxAge: 60 * 60 * 24 * 7, // 7日間
      httpOnly: true, // trueにしないとJSから書き換えられるのでtrueにする
      secure: true, // HTTPSの通信の場合のみ送信されるようにtrueにする
      sameSite: 'lax',
      path: '/'
    });
  }

  return {
    props: {};
  }
};

export default Index;
// pages/api/v1/cart/add.ts

import type { NextApiRequest, NextApiResponse } from 'next';
import { parseCookies } from 'nookies';
import { redisClient } from 'utils/redisClient';

//-----------------------------------------------------------
// type
//-----------------------------------------------------------
type IBody = {
  productId: number;
  productName: string;
  productPrice: number;
};

//-----------------------------------------------------------
// api
//-----------------------------------------------------------
const Index = async (req: NextApiRequest, res: NextApiResponse) => {

  // クライアントからPOSTされてきたBody
  const datas = req.body as IBody;

  // contextのreqを渡してcookie内の値を取得する
  const cookie = parseCookies({ req });

  // cookieがなければエラー
  if(!cookie) throw new Error('Cookie Parse Error.');
  
  // RedisのsessionにproductIdを追加する
  await redisClient.hset(cookie.sessionId, 'productId', datas.productId);
};

export default Index;

処理は大まかに以下の3つに分けられます。

● ioredis の初期化処理を作成します。
● /index のサーバーサイドレンダリング(SSR)処理内で、Cookie にセッションが存在しない場合はセッションIDを生成し、Redis と Cookie にセットします。
● クライアント側からセッションに入れたいデータを API に POST し、API 側でセッションIDを使って Redis のセッションに情報を格納します。

ポイントは、getServerSideProps および API 内で Cookie から sessionId を取り出す部分です。
Cookie の値はリクエストのタイミングでサーバー側に送信されるため、Cookie を解析して sessionId を取得しています。

sessionId が存在する場合は、ユーザーを識別するための特別な処理を挿入することができます。一方、sessionId が存在しない場合は、新たに sessionId を生成します。

cookieにsessionIdのみを格納する方法を採用することで、Redis(インメモリ方式)でsessionIdを保持している間、ユーザーを識別する値を保存することができました。

それでは。

この記事が気に入ったらサポートをしてみませんか?