🔥

【Next.js】Middlewareの処理でトークンを検証してみよう

2023/05/12に公開

概要

以前、記事でMiddlewareについて記載しました。
https://zenn.dev/kiriyama/articles/b0d6f8b2362107

ただ、Next.jsの機能としてのmiddlewareではなく、一般的な??middlewareをしたいと思ったのでメモ。

仕様について

今回は、jsonwebtokenを使って、下記の流れでトークンを検証したいと思う。
jsonwebtokenの使い方やクリックしたらuseStateで~~といった内容は省く。

  • jsonwebtokenでトークンを発行してlocalstorageに保存
  • フォームに入力した文字列を送信してapiで処理する前にmiddlewareの処理を入れる
  • middlewareでは「トークンの有無」「トークンを検証して有効」かをチェック
  • トークンが無い・もしくは無効であればそれぞれエラーメッセージをアラートで表示
  • 有効であれば、フォームの文字列をレスポンスして画面に表示させる

デモはこちら

実装

実装にあたって、以下のディレクトリー構成にする。

  • pages/index.js
  • pages/api/token.js
  • utiles/Middleware.js

index.jsはフォームに入力したテキストをAPI側に送る/トークンを発行する/トークンを保存する などの処理です。
token.jsはバックエンド側の処理としてindex.jsから受けったリクエストを処理してレスポンスを返します。
Middleware.jsが今回のメインとなる処理です。こちらではindex.jsでAPI側にリクエストを送る前にトークンを受けって、トークンが存在しているか/トークンが有効かをチェックしています。

jsonwebtokenをインストール

jsonwebtokenを利用するので、インストールしておく。

npm install jsonwebtoken

index.js

まずはファイルの内容。デモのように同じではなく、必要部分以外は消しています。

pages/index.js
import Head from 'next/head'
import { useEffect, useState } from 'react'
import jwt from 'jsonwebtoken'

const SECRET_KEY = "secret_key"

export default function Home() {
 //useStateで必要な値を保存する
  const [name, setName] = useState('')
  const [getName, setGetName] = useState('')
  const [token, setToken] = useState('')

  //localstorageからトークンを取得する
  useEffect(() => {
    const gettoken = localStorage.getItem("token")
    if (gettoken) {
      setToken(gettoken)
    }
  }, [token])

  const nameHandler = (e) => {
    setName(e.target.value)
  }

  const clickHandler = () => {
    //jsonwebtokenトークンを発行する
    if (!token) {
      const gettoken = jwt.sign({ name: name }, SECRET_KEY, { expiresIn: '2m' })
      localStorage.setItem("token", gettoken)
      setToken(gettoken)
    }
  }

  const submitHanbdler = async (e) => {
    e.preventDefault()
    
  //API側にデータを送る
    try {
      const response = await fetch("/api/token", {
        method: "POST",
        headers: {
          "Accept": "application/json",
          "Content-Type": "application/json",
          "authorization": `Bearer ${localStorage.getItem("token")}`
        },
        body: JSON.stringify({
          name: name
        })
      })
      
      //APIからのレスポンス
      const jsonData = await response.json()
     alert(jsonData.message)
      if (jsonData.error) {
        localStorage.removeItem("token")
        setToken("")
      } else {
        setGetName(jsonData.name)
      }

    } catch (error) {
      alert("送信失失敗")
    }

  }
  return (
    <>
      <Head>
        <title>middlewareテスト</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className={styles.main}>
        <h1 className={styles.title}>middlewareテスト</h1>

        {(!token) ?
          (<>
            <p>トークンを発行してください</p> <button className={styles.btn} onClick={clickHandler}>トークンを発行する</button>
          </>) :
          <p>トークンを発行済みです。</p>
        }
        {getName &&
          <>
            <hr />
            <p>こんにちは、{getName}さん</p>
            <hr />
          </>
        }
        <form className={styles.forms} onSubmit={submitHanbdler}>
          名前: <input onChange={(e) => nameHandler(e)} value={name} type="text" name="name" /><br />
          <button type="submit">送信</button>
        </form>
      </main>
    </>
  )
}

jsonwebtokenトークンを発行する

まずトークンがない場合は、「トークンを発行してください」というテキストと「トークンを発行する」というボタンがひょじされている。
クリックするとjwt.sign()でトークンを発行する。そしてlocalStorageにトークンを保存している。これは他のページでもトークンを使う可能性もある為である。

他、トークンをsetTokentokenに値を保存しているので、トークンを発行するボタンを表示/非表示させる為。

API側にデータを送る

フォームにテキストを入力して、送信ボタンをクリックしたら、fecth()/api/tokenにデータを送信する。データ自体はbodyJSON.stringify()で送る。重要なのは次である。

Authorization リクエストヘッダーにトークンを渡す

ヘッダーの部分だけ抜粋。

headers: {
       "Accept": "application/json",
       "Content-Type": "application/json",
       "authorization": `Bearer ${localStorage.getItem("token")}`
    },

Authorizationの箇所にlocalStorageからトークンを取得してきている。またBearerという値は慣例なのか??よく使用されている。またBearerとトークンとの間には半角スペースを設ける事。 これはmiddlwareでトークンの値をsplit()で取り出すときに指定の区切り文字として半角スペースを利用します。

https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Authorization

APIからのレスポンス

middlewareや通過後のAPI側から受け取ったレスポンスの処理。

とりあえず、受け取ったメッセージ(message)をアラートで表示している。
後術しているが、トークンの期限が切れていた場合はAPI側からerrorプロパティが送られてくるので、errortrueの場合は、localStorageの値とsetTokenを空にすることで、再度トークン発行ボタンを表示させるようにしています。

token.js

今度はapiフォルダ内のtoken.jsに関して。

pages/api/token.js
import React from "react";
import Middleware from "@/utiles/Middleware";

const token = (req, res) => {
    try {
        return res.status(200).json({ message: "送信成功", name: req.body.name });
    } catch (error) {
        return res.status(400).json({ message: "送信失敗" });
    }
};

export default Middleware(token);

token.jsでの処理はmiddlewareでの処理が通過していることが前提なので、今回はフォームで受け取った値をそのままフロント側にjsonデータとして返します。

フォームからの値はreq.body.nameに入ってるので、成功した場合はnameというプロパティ名を追加して返している。エラーの場合はmessageのみ返す。

Middlewareを通過させるには

middlewareの処理は後述するとして、token.jsでの処理の前にmiddlewareの処理を通過させる方法としては、middlewareの引数にtoken.jsの関数をそのまま指定する事です。
そのため、Middleware.jsをインポートして以下のように利用する。

import Middleware from "@/utiles/Middleware";

const token = (req, res) => {
}

export default Middleware(token);

//以下のような方法もあり
const token = Middleware( (req, res) => {})
export default token;

Middleware.js

本題である、middlewareの処理。
フォームからのリクエストをapi側の処理の前にトークンが有効かどうかの処理を挟む。

utiles/Middleware.js
import React from 'react'
import jwt from 'jsonwebtoken'

const SECRET_KEY = "secret_key"

const Middleware = (handler) => {

  //トークンを調べる
  return async (req, res) => {
    const token = req.headers.authorization.split(" ")[1]
   
    if (!token) {
      return res.status(401).json({ message: "トークンがありません。" })
    }

    //トークンを検証する
    try {
      const decodedToken = jwt.verify(token, SECRET_KEY)
      return handler(req, res)

    } catch (error) {

      return res.status(401).json({ message: "トークンの期限が切れている",error:true })

    }

  }
}

export default Middleware

引数について

引数のhandlerは今回の事例でいえば、token.jsの関数そのものである。

トークンを調べる

今回、フォームからAPI側へリクエストを投げてるので、middleware内でもリクエスト(req)とレスポンス(res)が受けとれる。
フォーム(index.js)からfetch()で送る際にheadersauthorizationにトークンを設定したと思うので、こちらを下記のように取り出す。

req.headers.authorization.split(" ")[1]

前述のAPI側にデータを送るでも説明したようにBearerとトークンとの間には半角スペースを設けてるので、こちらをsplit関数を利用する事でトークンの値のみ取り出すことができる。
取り出したトークンを調べて、トークンが無ければmessageプロパティで「トークンがありません」とretrunすることで、APIのtoken.jsまで処理が通過せず、処理が終わります。

トークンを検証する

トークンがある場合は、今度はトークンをtry...catch文とjwt.verify()を使って検証する。
検証して、トークンの期限が問題なければ、handlerreturnすることでAPI側の処理へ移行する。
もし、期限が切れてるようであればcatch(error)となるので、messageプロパティにエラーメッセージを記載して、今回はさらにerrorプロパティを追加してreturnで返す事でAPIのtoken.jsまで処理が通過せず、処理が終わります。

こちらはフロント側でレスポンス(res)で受け取った際に、このerrortrueの場合はlocalStragetokensetTokenの値を削除することで、トークン発行ボタンを表示せるようにしている。

下記、フロント側(index.js)の抜粋。

pages/index.js
 //APIからのレスポンス
 const jsonData = await response.json()
    
 if (jsonData.error) {
      localStorage.removeItem("token")
      setToken("")
    } else {
      setGetName(jsonData.name)
   }

まとめ

とりあえず、middlewareでトークンの検証をしてみたが、今度はNext.jsでのmiddlewareの機能を使ってトークンの検証をやってみたいと思う。

Discussion