はじめに
Next.js初心者なりに、getServerSidePropsを用いてページアクセス時のリダイレクトを実装してみました。
要件
- ログインしていない場合、認証が必要なページにアクセスした際にログイン画面にリダイレクトする
- 逆に、ログイン済みの場合、ログイン画面を表示しようとした時ホーム画面にリダイレクトする
- ユーザー情報の保持はRecoilを使用。recoil-persistなどのライブラリの使用はなし
- 認証はアクセストークン認証で、トークンはCookieに保存
- トークンの有効性はAPIを叩いて正常なレスポンスが返ってくるかで判断(APIは既製のものを使用した)
-
/accountにリクエストを送ると、tokenが有効であれば以下のようなデータが返ってくる
{ "user": { "id": 0, "name": "string", "email": "string", "iconImageUrl": "string" } }
-
tokenが無効なら以下のように返ってくる(空データなだけであって、エラーにならない点に注意)
{}
-
実装
以下、実装例です。getServerSidePropsの関数は、redirect.tsにまとめて定義し、各ページコンポーネントでredirectToLogin関数とredirectToHomepage関数の必要な方をインポートして使うことを想定しています。
utils/redirect.ts
import { GetServerSideProps } from "next";
import axiosInstance from "./axios";
// 空のオブジェクトかどうかを判定する関数
export const isEmptyObject = (obj: object) => {
return Object.keys(obj).length === 0;
};
// ログインページへのリダイレクト関数
export const redirectToLogin: GetServerSideProps = async (context) => {
const token = context.req.cookies.token;
// そもそもトークンがなければ即リダイレクト
if (!token) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
try {
// リクエストヘッダーにトークンを添付してリクエストを送信(js-cookieライブラリはサーバーサイドで使えないので、
// axiosInstanceで自動でtoken付与されない。そのため、ここでtokenを付与している)
const response = await axiosInstance.get("/account", {
headers: { Authorization: `Bearer ${token}` },
});
// getAccountはTokenを設定せずにリクエストを送った場合、200レスポンスで{}が返ってくる。なので以下のような判定をする。
// レスポンスが空のオブジェクトなら(トークン無効)、ログインページへリダイレクト
if (isEmptyObject(response.data)) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
// トークンが有効ならユーザーデータをpropsとして渡す
// これにより、リロード時にrecoilのユーザーデータが消えることを防ぐ
return {
props: { user: response.data.user },
};
} catch (error) {
console.error(error);
}
return {
props: {},
};
};
// ホームページへのリダイレクト関数
export const redirectToHomepage: GetServerSideProps = async (context) => {
const token = context.req.cookies.token;
try {
const response = await axiosInstance.get("/account", {
headers: { Authorization: `Bearer ${token}` },
});
// レスポンスが空のオブジェクトでなければ(トークン有効)、ホームページへリダイレクト
if (isEmptyObject(response.data) === false) {
return {
redirect: {
destination: "/homepage",
permanent: false,
},
};
}
} catch (error) {
console.error(error);
}
return {
props: {},
};
};
具体的にリダイレクトをしているのは以下の部分
return {
redirect: {
destination: "/login",
permanent: false,
},
};
permanent
オプションは、永続的なリダイレクトかどうかを定義する。ページ移転やドメイン引越などでなければ基本はfalseで問題ない。
ページコンポーネント
// homepage.tsx
import React, { useEffect } from "react";
import Header from "@/components/Header";
import { useRecoilState } from "recoil";
import { userState } from "@/recoil/atoms";
import { TOKEN_KEY } from "@/utils/cookieConstants";
import Cookie from "js-cookie";
import { redirectToLogin } from "@/utils/redirect";
import axiosInstance from "@/utils/axios";
import { User } from "@/types";
interface HomePageProps {
user: User;
}
// getServerSiderPropsから渡されたユーザーデータをPropsとして受け取る
const HomePage: React.FC<HomePageProps> = ({ user: initialUser }) => {
const [user, setUser] = useRecoilState(userState);
const token = Cookie.get(TOKEN_KEY);
// コンポーネントがマウントされた時に、getServerSidePropsからユーザーデータをセットする
// これは、リロード時にユーザーデータがRecoilから消えてしまうのを防ぐため
useEffect(() => {
setUser(initialUser);
}, [initialUser, setUser]);
return (
<>
〜省略〜
</>
);
};
export default HomePage;
// 以下でリダイレクト処理を呼び出す
export const getServerSideProps = redirectToLogin;
export const getServerSideProps = redirectToLogin;
でリダイレクト処理を呼び出している。
getServerSidePropsからは、Propsでユーザーデータも受け取って、useEffectでRecoilのstateにセットしている。
Recoilはrecoil-persistなどのライブラリを使うか、ローカルストレージを使わないとリロード時の状態の永続化ができないため、このような処理にした。
逆に、ログインページではredirectToHomepageを呼び出す。
// login.tex
〜 省略 〜
export default Login;
export const getServerSideProps = redirectToHomepage;
嵌った点
getServerSideProps内では、js-cookieでCookieを参照できない。
どう困ったのかというと、axiosInstance内で自動でCookieからtokenを取得してヘッダーに付与しようとしていたのが、うまく機能しなかったこと。
import axios from "axios";
import Cookies from "js-cookie";
import { TOKEN_KEY } from "./cookieConstants";
const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_DOMAIN,
timeout: 1000,
});
// リクエストを行う直前に毎回Authorizationヘッダーを付与する
axiosInstance.interceptors.request.use(
function (config) {
const token = Cookies.get(TOKEN_KEY); // トークンを取得(この部分がgetServerSideProps内だとうまく動かない!!)
// トークンがあればリクエストヘッダーに添付
if (token) {
config.headers["Authorization"] = "Bearer " + token;
}
return config;
},
function (error) {
// リクエストエラー時の処理
return Promise.reject(error);
}
);
export default axiosInstance;
それに気づかず、無駄に時間を溶かした…
解決策として、getServerSideProps内で、contextを通してtokenを取得し、以下のようにヘッダーにtokenを付与している。
export const redirectToLogin: GetServerSideProps = async (context) => {
const token = context.req.cookies.token;
// リクエストヘッダーにトークンを添付してリクエストを送信(js-cookieライブラリはサーバーサイドで使えないので、
// axiosInstanceで自動でtoken付与されない。そのため、ここでtokenを付与している)
const response = await axiosInstance.get("/account", {
headers: { Authorization: `Bearer ${token}` },
});
~ 省略 〜
}