🤖

Clerk x Next.js でmiddlewareで関数を連結させる

2023/09/09に公開

はじめに

以下の記事で middleware.ts で複数の middleware の関数を連結させる方法を紹介しました。

https://zenn.dev/hayato94087/articles/8f6496e0b8936f

ここでは、Clerk の authMiddleware を連結させる方法を紹介します。

下記が作業コードです。

https://github.com/hayato94087/nextjs-clerk-middleware-chain-sample

実施内容

Middleware で3つの要素を実装します。

要素 説明
IP制限 全てのアクセスに対して特定のIP制限実施します
ログ出力 特定のパスへのアクセスに対してユーザーのリクエスト情報を出力する
Clerk認証処理 特定のパスへのアクセスに対して認証情報を確認します

作業環境を作成

以下で作成した環境を復元します。

https://zenn.dev/hayato94087/articles/8f6496e0b8936f

Next.jsプロジェクトの新規作成し環境構築

作業するプロジェクトを新規に作成していきます。

長いので、折り畳んでおきます。

新規プロジェクト作成と初期環境構築の手順詳細
$ pnpm create next-app@latest nextjs-clerk-middleware-chain-sample --typescript --eslint --import-alias "@/*" --src-dir --use-pnpm --tailwind --app
$ cd nextjs-clerk-middleware-chain-sample 

以下の通り不要な設定を削除し、プロジェクトの初期環境を構築します。

$ mkdir src/styles
$ mv src/app/globals.css src/styles/globals.css
src/styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
src/app/page.tsx
export default function Home() {
  return (
    <main className="text-lg">
      テストページ
    </main>
  )
}
src/app/layout.tsx
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>
  );
}
tailwind.config.js
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;
tsconfig.json
{
  "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 "新規にプロジェクトを作成し, 作業環境を構築"

GitHub & Vercel にデプロイ

GitHub & Vercel にデプロイします。以降動作確認は、Vercel で行います。

IP制限、ログ出力を実装

IP 制限、ログ出力を middleware に実装します。

長いので、折り畳んでおきます。

IP制限、ログ出力を実装

環境変数をファイルを作成します。アクセスを許可する IP アドレスを設定します。

$ touch .env.local
.env.local
IP_WHITE_LIST=xxx.xxx.xxx.xxx, yyy.yyy.yyy.yyy

Middleware を実装します。

$ mkdir -p src/middlewares
$ touch    src/middlewares/widthIpRestriction.ts \
           src/middlewares/withLogging.ts \
           src/middleware.ts

middlewaresの配下にIP制限とログ出力機能を実装します。

src/middlewares/widthIpRestriction.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);
  };
}
src/middlewares/withLogging.ts
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);
  };
}

middleware.tsを定義します。

src/middleware.ts
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 "IP制限、ログ出力を実装"
$ git push origin main

Clerkのアカウント作成

Clerk のアカウント作成はこちらを参照ください。

https://zenn.dev/hayato94087/articles/effd7e94ae8302

Clerkを設定

Next.js で Clerk が提供する SDK を利用するために、Clerk のパッケージをインストールします。

$ pnpm add @clerk/nextjs

環境変数を .env.local に設定します。APIKEY はダッシュボードから取得します。

APIKEY を .env.local に設定します。

.env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key
CLERK_SECRET_KEY=your_secret_key

どこからでもセッション、認証済みのユーザー情報などの認証情報にアクセスできるように、<ClerkProvider> コンポーネントを設定します。layout.tsx<ClerkProvider> を設定します。

src/app/layout.tsx
+import { ClerkProvider } from "@clerk/nextjs";
import "./globals.css";

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
+   <ClerkProvider>
      <html lang="ja">
        <body className="bg-white">{children}</body>
      </html>
+   </ClerkProvider>
  );
}

アプリケーション全体を保護するため、認証用の middleware の関数を定義します。

  • 「/」は公開用にします。
  • 「/_next」から始まるパスは無視します。
  • 「/」から「.」を含むファイルパスは無視します。
$ touch src/middlewares/withAuth.ts
src/middlewares/withAuth.ts
import { authMiddleware } from "@clerk/nextjs";

export default authMiddleware({
  publicRoutes: ["/"],
  ignoredRoutes: ["/(.*\\..*)(.*)", "/(_next)(.*)"],
});
src/middleware.ts
import { widthLogging } from "@/middlewares/withLogging";
import { widthIpRestriction } from "@//middlewares/widthIpRestriction";
import withAuth from "@/middlewares/withAuth";

export default widthLogging(widthIpRestriction(withAuth));

コミットします。

$ pnpm build
$ git add .
$ git commit -m "Clerkを設定"
$ git push origin main

Vercel 上で動作確認するときは、Clerk の環境変数ファイルを登録をもれないようにします。

まとめ

  • authMiddleware を他の middleware 関数と連結させる方法を紹介しました。

下記が作業コードです。

https://github.com/hayato94087/nextjs-clerk-middleware-chain-sample

Discussion