【Next.js】Middlewareの処理でトークンを検証してみよう
概要
以前、記事でMiddlewareについて記載しました。
ただ、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
まずはファイルの内容。デモのように同じではなく、必要部分以外は消しています。
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
にトークンを保存している。これは他のページでもトークンを使う可能性もある為である。
他、トークンをsetToken
でtoken
に値を保存しているので、トークンを発行するボタンを表示/非表示させる為。
API側にデータを送る
フォームにテキストを入力して、送信ボタンをクリックしたら、fecth()
で/api/token
にデータを送信する。データ自体はbody
にJSON.stringify()
で送る。重要なのは次である。
Authorization リクエストヘッダーにトークンを渡す
ヘッダーの部分だけ抜粋。
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"authorization": `Bearer ${localStorage.getItem("token")}`
},
Authorizationの箇所にlocalStorage
からトークンを取得してきている。またBearer
という値は慣例なのか??よく使用されている。またBearerとトークンとの間には半角スペースを設ける事。 これはmiddlwareでトークンの値をsplit()
で取り出すときに指定の区切り文字として半角スペースを利用します。
APIからのレスポンス
middlewareや通過後のAPI側から受け取ったレスポンスの処理。
とりあえず、受け取ったメッセージ(message
)をアラートで表示している。
後術しているが、トークンの期限が切れていた場合はAPI側からerror
プロパティが送られてくるので、error
がtrue
の場合は、localStorage
の値とsetToken
を空にすることで、再度トークン発行ボタンを表示させるようにしています。
token.js
今度は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側の処理の前にトークンが有効かどうかの処理を挟む。
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()
で送る際にheaders
のauthorization
にトークンを設定したと思うので、こちらを下記のように取り出す。
req.headers.authorization.split(" ")[1]
前述のAPI側にデータを送るでも説明したようにBearerとトークンとの間には半角スペースを設けてるので、こちらをsplit関数を利用する事でトークンの値のみ取り出すことができる。
取り出したトークンを調べて、トークンが無ければmessage
プロパティで「トークンがありません」とretrun
することで、APIのtoken.js
まで処理が通過せず、処理が終わります。
トークンを検証する
トークンがある場合は、今度はトークンをtry...catch
文とjwt.verify()
を使って検証する。
検証して、トークンの期限が問題なければ、handler
をreturn
することでAPI側の処理へ移行する。
もし、期限が切れてるようであればcatch(error)
となるので、message
プロパティにエラーメッセージを記載して、今回はさらにerror
プロパティを追加してreturn
で返す事でAPIのtoken.js
まで処理が通過せず、処理が終わります。
こちらはフロント側でレスポンス(res
)で受け取った際に、このerror
がtrue
の場合はlocalStrage
のtoken
とsetToken
の値を削除することで、トークン発行ボタンを表示せるようにしている。
下記、フロント側(index.js
)の抜粋。
//APIからのレスポンス
const jsonData = await response.json()
if (jsonData.error) {
localStorage.removeItem("token")
setToken("")
} else {
setGetName(jsonData.name)
}
まとめ
とりあえず、middlewareでトークンの検証をしてみたが、今度はNext.jsでのmiddlewareの機能を使ってトークンの検証をやってみたいと思う。
Discussion