Next.jsで複数のmiddlewareの関数を連結させる
はじめに
-
middleware.ts で複数の middleware の関数を連結させる方法を紹介します。
-
下記が実装コードです。
前提
middleware について触ったことがない場合は、middleware の紹介記事をまずご参照ください。
あるいは、英語ですが公式サイトをご参照ください。
結論
-
middleware.ts
を起点に、middleware の関数を定義したファイルを作成する。 - Higher-order function を使って、middleware の関数を定義する。
参考とした記事/動画
以下の動画、GitHub のリポジトリを参考にしました。
- https://youtu.be/fmFYH_Xu3d0
- https://github.com/jmarioste/next-middleware-guide
- https://github.com/HamedBahram/next-middleware-chain
実装して確認
実装して確認して行きます。
Next.jsプロジェクトの新規作成し環境構築
作業するプロジェクトを新規に作成していきます。
長いので、折り畳んでおきます。
新規プロジェクト作成と初期環境構築の手順詳細
$ pnpm create next-app@latest nextjs-middleware-chain-sample --typescript --eslint --import-alias "@/*" --src-dir --use-pnpm --tailwind --app
$ cd nextjs-middleware-chain-sample
以下の通り不要な設定を削除し、プロジェクトの初期環境を構築します。
$ mkdir src/styles
$ mv src/app/globals.css src/styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
export default function Home() {
return (
<main className="text-lg">
テストページ
</main>
)
}
import '@/styles/globals.css'
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body className="">{children}</body>
</html>
);
}
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
plugins: [],
};
export default config;
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
+ "baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
コミットします。
$ pnpm build
$ git add .
$ git commit -m "新規にプロジェクトを作成し, 作業環境を構築"
ステップ1;Miiddleware で機能を実装
Middleware で2つの要素を実装します。
要素 | 説明 |
---|---|
IP制限 | 全てのアクセスに対して特定のIP制限実施します。 |
ログ出力 | アクセスに対してユーザーのリクエスト情報を出力する |
環境変数をファイルを作成します。アクセスを許可する IP アドレスを設定します。
$ touch .env.local
IP_WHITE_LIST=xxx.xxx.xxx.xxx, yyy.yyy.yyy.yyy
Middleware を実装します。
$ touch src/middleware.ts
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// ##################################################
// ログ出力
//
// パスが以下の場合にログを出力します。
// ・"/"から始まり、"."を含まない任意のパス
// ・"/_nextから始まらない任意のパス
// ・"/"のルートパス。
// ・"/api"から始まる任意のパス
// ・"/trpc"から始まる任意のパス
// ##################################################
if (
request.nextUrl.pathname.match(/\/(?!.*\..*|_next).*/) ||
request.nextUrl.pathname.match(/\/(api|trpc)(.*)/) ||
request.nextUrl.pathname === "/"
) {
// リクエストの情報をJSON形式で出力します。
const log = {
ip: request.ip,
geo: request.geo,
url: request.nextUrl.pathname,
};
console.log(JSON.stringify(log, (k, v) => (v === undefined ? null : v)));
}
// ##################################################
// IP制限
//
// 全てのパスに対してIP制限を実行します。
// ##################################################
// ホワイトリストに登録されたIPリストを取得します。
const ipWhiteList = new Set(
process.env.IP_WHITE_LIST?.split(",").map((item: string) => {
return item.trim();
})
);
// ホワイトリストに登録されていないIPアドレスからのアクセスは拒否します。
if (request.ip && !ipWhiteList.has(request.ip as string)) {
const log = {
message: `許可されていないIPアドレスからのアクセスのためアクセスを拒否しました`,
ip: request.ip,
};
console.log(log);
return new NextResponse(null, { status: 401 });
}
return NextResponse.next();
}
コミットします。
$ pnpm build
$ git add .
$ git commit -m "Miiddleware で機能を実装"
GitHub にリポジトリを作成し、Vercel へデプロイします。Vercel へデプロイする際に、環境変数に IP_WHITE_LIST
を定義し IP アドレスを設定します。
Vercel 上で動作確認します。なお、ローカル環境で動作確認する場合は、IP アドレスなど取得できない変数があるため、必ず Vercel 上で確認してください。
まず、ホワイトリストに登録されていない IP からアクセスします。すると、401
が返却されます。想定どおりです。
Vercel の管理画面のサーバーログを確認すると、IP 制限がかかっていることが分かります。
Time | Status Code | Host | HTTPメソッド | ログ |
---|---|---|---|---|
AUG 14 07:29:22.33 | - | nextjs-middleware-chain-sample.vercel.app | [GET] / | { message: '許可されていないIPアドレスからのアクセスのためアクセスを拒否しました', ip: 'xxx.xxx.xxx.xxx' } |
AUG 14 07:29:22.33 | - | nextjs-middleware-chain-sample.vercel.app | [GET] / | {"ip":"xxx.xxx.xxx.xxx","geo":{"city":"Honcho","country":"JP","latitude":"xx.xxxx","longitude":"xx.xxxx","region":"13"},"url":"/"} |
AUG 14 07:29:22.00 | 401 | nextjs-middleware-chain-sample.vercel.app | [GET] / | |
AUG 14 07:29:21.99 | 401 | nextjs-middleware-chain-sample.vercel.app | [GET] / | [GET] [middleware: "src/middleware"] / status=401 |
AUG 14 07:29:17.27 | - | nextjs-middleware-chain-sample.vercel.app | [GET] /favicon.ico | { message: '許可されていないIPアドレスからのアクセスのためアクセスを拒否しました', ip: 'xxx.xxx.xxx.xxx' } |
AUG 14 07:29:17.15 | 401 | nextjs-middleware-chain-sample.vercel.app | [GET] /favicon.ico | [GET] [middleware: "src/middleware"] /favicon.ico status=401 |
AUG 14 07:29:17.00 | 401 | nextjs-middleware-chain-sample.vercel.app | [GET] /favicon.ico |
続いて、ホワイトリストに登録されている IP からアクセスします。正しく動作しています。
Vercel の管理画面のサーバーログからも問題なくアクセスできていることが分かります。
Time | Status Code | Host | HTTPメソッド | ログ |
---|---|---|---|---|
AUG 14 07:33:40.00 | 304 | nextjs-middleware-chain-sample.vercel.app | [GET] /favicon.ico | |
AUG 14 07:33:39.67 | 200 | nextjs-middleware-chain-sample.vercel.app | [GET] /favicon.ico | [GET] [middleware: "src/middleware"] /favicon.ico status=200 |
AUG 14 07:33:39.00 | 200 | nextjs-middleware-chain-sample.vercel.app | [GET] / | |
AUG 14 07:33:38.37 | - | nextjs-middleware-chain-sample.vercel.app | [GET] / | {"ip":"xxx.xxx.xxx.xxx","geo":{"city":"Tokyo","country":"JP","latitude":"xxx.xxx","longitude":"xxx.xxx","region":"13"},"url":"/"} |
AUG 14 07:33:37.67 | 200 | nextjs-middleware-chain-sample.vercel.app | [GET] / | [GET] [middleware: "src/middleware"] / status=200 |
ステップ2:Higher-Order Function(高階関数)でファイルを分割
Higher-order function(高階関数)を利用しファイルを分割します。Higher-order function(高階関数)とは、関数を引数として受け取り、関数を返す、関数です。言葉だとわかりにくいので、実装して行きます。
import {
NextFetchEvent,
NextMiddleware,
NextRequest,
NextResponse,
} from "next/server";
function widthLogging(middleware: NextMiddleware) {
return async (request: NextRequest, event: NextFetchEvent) => {
// ##################################################
// ログ出力
//
// パスが以下の場合にログを出力します。
// ・"/"から始まり、"."を含まない任意のパス
// ・"/_nextから始まらない任意のパス
// ・"/"のルートパス。
// ・"/api"から始まる任意のパス
// ・"/trpc"から始まる任意のパス
// ##################################################
if (
request.nextUrl.pathname.match(/\/(?!.*\..*|_next).*/) ||
request.nextUrl.pathname.match(/\/(api|trpc)(.*)/) ||
request.nextUrl.pathname === "/"
) {
// リクエストの情報をJSON形式で出力します。
const log = {
ip: request.ip,
geo: request.geo,
url: request.nextUrl.pathname,
method: request.method,
};
console.log(JSON.stringify(log, (k, v) => (v === undefined ? null : v)));
}
return middleware(request, event);
};
}
function widthIpRestriction(middleware: NextMiddleware) {
return async (request: NextRequest, event: NextFetchEvent) => {
// ##################################################
// IP制限
//
// 全てのパスに対してIP制限を実行します。
// ##################################################
// ホワイトリストに登録されたIPリストを取得します。
const ipWhiteList = new Set(
process.env.IP_WHITE_LIST?.split(",").map((item: string) => {
return item.trim();
})
);
// ホワイトリストに登録されていないIPアドレスからのアクセスは拒否します。
if (request.ip && !ipWhiteList.has(request.ip as string)) {
const log = {
message: `許可されていないIPアドレスからのアクセスのためアクセスを拒否しました`,
ip: request.ip,
url: request.nextUrl.pathname,
method: request.method,
};
console.log(log);
return new NextResponse(null, { status: 401 });
}
return middleware(request, event);
};
}
export default widthLogging(widthIpRestriction(() => NextResponse.next()));
コミットします。
$ pnpm build
$ git add .
$ git commit -m "Higher-Order Function(高階関数)を定義し、ファイルを分割"
$ git push origin main
Vercel 上で正しく動作します。
ステップ3:ファイルを分割
1 つのファイルに全てを記述しても良いですが、メンテナンス性も考慮してファイルを分割します。
$ mkdir src/middlewares
$ touch src/middlewares/widthIpRestriction.ts
$ touch src/middlewares/withLogging.ts
import {
NextFetchEvent,
NextMiddleware,
NextRequest,
NextResponse,
} from "next/server";
export function widthIpRestriction(middleware: NextMiddleware) {
return async (request: NextRequest, event: NextFetchEvent) => {
// ##################################################
// IP制限
//
// 全てのパスに対してIP制限を実行します。
// ##################################################
// ホワイトリストに登録されたIPリストを取得します。
const ipWhiteList = new Set(
process.env.IP_WHITE_LIST?.split(",").map((item: string) => {
return item.trim();
})
);
// ホワイトリストに登録されていないIPアドレスからのアクセスは拒否します。
if (request.ip && !ipWhiteList.has(request.ip as string)) {
const log = {
message: `許可されていないIPアドレスからのアクセスのためアクセスを拒否しました`,
ip: request.ip,
url: request.nextUrl.pathname,
method: request.method,
};
console.log(log);
return new NextResponse(null, { status: 401 });
}
return middleware(request, event);
};
}
import {
NextFetchEvent,
NextMiddleware,
NextRequest,
} from "next/server";
export function widthLogging(middleware: NextMiddleware) {
return async (request: NextRequest, event: NextFetchEvent) => {
// ##################################################
// ログ出力
//
// パスが以下の場合にログを出力します。
// ・"/"から始まり、"."を含まない任意のパス
// ・"/_nextから始まらない任意のパス
// ・"/"のルートパス。
// ・"/api"から始まる任意のパス
// ・"/trpc"から始まる任意のパス
// ##################################################
if (
request.nextUrl.pathname.match(/\/(?!.*\..*|_next).*/) ||
request.nextUrl.pathname.match(/\/(api|trpc)(.*)/) ||
request.nextUrl.pathname === "/"
) {
// リクエストの情報をJSON形式で出力します。
const log = {
ip: request.ip,
geo: request.geo,
url: request.nextUrl.pathname,
method: request.method,
};
console.log(JSON.stringify(log, (k, v) => (v === undefined ? null : v)));
}
return middleware(request, event);
};
}
import { NextResponse } from "next/server";
import { widthLogging } from "@/middlewares/withLogging";
import { widthIpRestriction } from "@//middlewares/widthIpRestriction";
export default widthLogging(widthIpRestriction(() => NextResponse.next()));
コミットします。
$ pnpm build
$ git add .
$ git commit -m "ファイルを分割"
$ git push origin main
Vercel 上で正しく動作します。
ステップ4:関数をチェイン
以下のような記述も良いですが、より簡潔に記載するために関数をチェインする方法を実装します。
...
export default widthLogging(widthIpRestriction(() => NextResponse.next()));
以下がチェインした場合の実装です。配列の順序には意味があり、配列の先頭から実行されます。
...
export default chainMiddlewares([widthLogging, widthIpRestriction]);
では作成します。
$ mkdir -p src/types
$ touch src/types/middleware.d.ts \
src/middlewares/chain.ts
import { NextMiddleware } from "next/server";
export type MiddlewareFactory = (middleware: NextMiddleware) => NextMiddleware;
import { NextMiddleware, NextResponse } from "next/server";
import { MiddlewareFactory } from "@/types/middleware";
export function chainMiddlewares(
functions: MiddlewareFactory[] = [],
index = 0
): NextMiddleware {
const current = functions[index];
if (current) {
const next = chainMiddlewares(functions, index + 1);
return current(next);
}
return () => NextResponse.next();
}
import { widthLogging } from "@/middlewares/withLogging";
import { widthIpRestriction } from "@//middlewares/widthIpRestriction";
import { chainMiddlewares } from "@/middlewares/chain";
// 関数は配列の順番に実行されます。
// ・全てのアクセスに対してログを出力します。
// ・IP制限を実行します。
export default chainMiddlewares([widthLogging, widthIpRestriction]);
コミットします。
$ pnpm build
$ git add .
$ git commit -m "より簡潔に記載するために関数をチェインする方法を実装"
$ git push origin main
Vercel 上で正しく動作します。
まとめ
-
middleware.ts で複数の middleware の関数を連結させる方法を紹介しました。
-
下記が実装コードです。
Discussion