概要
前回作成した React Router v7(フレームワークモード)での統合 windows 認証のコンポーネントを使って、middleware と layout で制御してみます。
middleware の機能を使用するには、React Router 7.9 以降が必要です。
参考にしたサイト
構成
app/
├── routes/
│ ├── _auth.tsx (レイアウト)
│ ├── home.tsx (認証成功時に表示される画面)
│ └── error.tsx (認証失敗時に表示される画面)
├── types/
│ └── interface.d.ts (グローバルで使用するインターフェース)
├── utilities/
│ └── windowsAuth.ts (windows認証する)
├── root.tsx
└── routes.ts
middleware の構成
- root.tsx - rootMiddleware - rootContext
middleware で windows 認証します - _auth.tsx - authMiddleware - authContext
windows 認証されていなければエラーページを表示、認証されていればユーザの情報を取得します - home.tsx - loader
_auth で取得したユーザ情報を使用してデータを取得します
middleware は並列実行ではなく、親 → 子の順に実行されます。
設定
react-router.config.ts に v8_middleware の記述を追加して有効化します。
import type { Config } from "@react-router/dev/config";
export default {
ssr: true,
future: {
v8_middleware: true,
},
} satisfies Config;
root.tsx
middleware を使って windows 認証されたユーザを取得します。
前回 loader に記述したものを middleware に移行します。
開発環境でも動かせるように、.env に VITE_DEBUG_USER=domain/username を追加するとそのユーザで認証されたことにしています。
context.set(rootContext, { userName: userInfo.user, domain: userInfo.domain });
この部分で、root の middleware で管理する rootContext に、ユーザ名とドメイン名を保存しています。
import {
createContext,
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "react-router";
import * as React from "react";
import type { Route } from "./+types/root";
import "./app.css";
// middleware用のコンテキスト作成
export const rootContext = createContext<AuthenticatedUser>();
/**
* windows認証ミドルウェア
* ユーザー名・ドメイン名を取得します
* サーバー専用として扱いたいので、この関数はexportしないこと
*/
const rootMiddleware: Route.MiddlewareFunction = async ({ request, context }) => {
try {
// DEV環境なら固定のユーザーを返す(.envから読み取り)
if (typeof process !== "undefined" && process.env.NODE_ENV === "development") {
const [domain, userName] = (process.env.VITE_DEBUG_USER || "unknown").split("\\");
context.set(rootContext, { userName, domain });
return;
}
// x-iis-windowsauthtoken ヘッダーを取得
const tokenHeader = request.headers.get("x-iis-windowsauthtoken");
if (!tokenHeader) {
console.log("No authentication token found in request headers.");
context.set(rootContext, { userName: "", domain: "" });
return;
}
// トークンを16進数から数値に変換
const handle = parseInt(tokenHeader, 16);
if (isNaN(handle)) {
console.log("Invalid authentication token format.");
context.set(rootContext, { userName: "", domain: "" });
return;
}
// Windows認証情報を取得(サーバーサイド専用)
const { getUserInfoFromToken } = await import("./utilities/windowsAuth");
const userInfo = getUserInfoFromToken(handle);
context.set(rootContext, { userName: userInfo.user, domain: userInfo.domain });
} catch (error) {
console.error("Error getting auth data from request:", error);
context.set(rootContext, { userName: "", domain: "" });
}
};
export const middleware: Route.MiddlewareFunction[] = [rootMiddleware];
// layoutなど...
interface AuthenticatedUser {
userName: string;
domain: string;
}
interface User {
id: number;
name: string;
permission: number;
}
routes.tsx
_auth.tsx を layout として、認証が必要なルートをその中に入れます。
windows 認証なので基本的には認証失敗はあまりないと思いますが、想定されたドメインのユーザでなかったり、特定のグループを拒否したりする場合はここで弾きます。
import { type RouteConfig, index, layout, route } from "@react-router/dev/routes";
export default [
// 認証が必要なルート
layout("routes/_auth.tsx", [
index("routes/home.tsx"),
route("/feature1", "routes/feature1.tsx"),
route("/feature2", "routes/feature1.tsx"),
...
]),
// エラーページ(認証不要)
route("error", "routes/error.tsx"),
] satisfies RouteConfig;
_auth.tsx
root.tsx の middleware で取得したユーザ名を使用し、ユーザの情報を API から取得した感じにしています。取得したデータは authContext に保存します。
import { Outlet, redirect, createContext } from "react-router";
import { rootContext } from "~/root";
import type { Route } from "./+types/_auth";
import type { User } from "~/types/api";
// middleware用のコンテキスト作成
export const authContext = createContext<User>();
// ユーザー情報を取得
async function fetchUserInfo(userName: string) {
// 実際のAPI呼び出しをシミュレート
await new Promise((resolve) => setTimeout(resolve, 100));
return { id: 123, name: userName, permission: 755 };
}
// 認証済みチェックミドルウェア
const authMiddleware: Route.MiddlewareFunction = async ({ context }) => {
const authenticatedUser = context.get(rootContext);
if (authenticatedUser.userName == "") {
throw redirect("/error");
}
// user情報取得処理
const user = await fetchUserInfo(authenticatedUser.userName);
// userについての情報が取得できなかった場合はエラーとする
if (!user || user.id === 0) {
throw redirect("/error");
}
context.set(authContext, user);
};
export const middleware: Route.MiddlewareFunction[] = [authMiddleware];
export default function AuthLayout() {
return <Outlet />;
}
home.tsx
_auth で取得したユーザ情報を元に home.tsx の loader でデータを取得します。
画面に対するアクセス権限のチェックもここでやると良さそうです。
import type { Route } from "./+types/home";
import { authContext } from "~/routes/_auth";
// サーバサイドloader
export async function loader({ context }: Route.LoaderArgs) {
// _authのmiddlewareで取得したユーザ情報を取得
const user: User = context.get(authContext)!;
// 何かしらのフェッチ処理
const retrieveData = await fetchExampleData(user.userName);
return { retrieveData, user };
}
// ユーザー名に対応するデータを取得
async function fetchExampleData(userName: string) {
// 実際のAPI呼び出しをシミュレート
await new Promise((resolve) => setTimeout(resolve, 100));
return { example: 123 };
}
export default function Home({ loaderData }: Route.ComponentProps) {
return (
<div className="flex flex-1 overflow-hidden">
<SidebarMenu />
<div className="flex-1 overflow-y-auto p-4">
<div className="mt-4 p-4 bg-gray-100 rounded">
{loaderData.user && (
<div className="mt-2 p-2 bg-blue-100 rounded">
<div>User Data:</div>
<div>ID: {loaderData.user.id}</div>
<div>Name: {loaderData.user.name}</div>
<div>ExampleValue: {loaderData.retrieveData.example}</div>
</div>
)}
</div>
</div>
</div>
);
}