まえがき
前回はSpring Boot側での認証を設定しました。
お次は、Next.js側での認証管理ですね。
2026/09/27 @honey32様の指摘を参考にブラッシュアップ
変更1:checkAuthを共通部品からuseEffect内に移動
変更2:isLoadingを追加し、ロード時間中に認証情報なしにならないように
本文
この記事の対象者
ここも第1回とほぼ同じですね。
要するに、完全未経験を除いた初心者です。
前提
以下インストール済(インストール方法は省略)
・React 19.0.0
・Next.js 16.0.0
・App Router
1.共通部品の作成
まず、以下のようなソースを用意します。
'use client';
import { createContext, useContext, useState, useEffect } from 'react';
type AuthContextType = {
isLoggedIn: boolean;
isLoading: boolean;
login: () => Promise<void>;
};
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({
children,
}: {
children: React.ReactNode;
}) {
// ログイン状態を全画面で共有する
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [isLoading, setIsLoading] = useState(false);
// ログイン成功後に認証状態を更新する
const login = async () => {
setIsLoggedIn(true);
setIsLoading(false);
};
// アプリ起動時にもCookieを確認する(F5対策)
// レンダーのたびに確認
useEffect(() => {
// Spring Bootへ認証状態を問い合わせる
const checkAuth = async () => {
try {
const response = await fetch("/api/auth/me", {
credentials: "include", // Cookieを自動送信する
});
setIsLoggedIn(response.ok);
} catch {
setIsLoggedIn(false);
} finally {
setIsLoading(false);
}
};
checkAuth();
},[]);
return (
<AuthContext.Provider
value={{ isLoading, isLoggedIn, login}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("AuthProviderの中でuseAuthを使ってください");
}
return context;
}
細かく解説すると、以下のようになります。
①型定義
※2026/09/27 @honey32様の指摘を参考に修正
type AuthContextType = {
isLoggedIn: boolean;
isLoading: boolean;
login: () => Promise<void>;
};
booleanは返り値が真偽であること、
voidは返り値なしであること、
Promiseは非同期結果を返り値なしで返す(完了通知だけでなにも返さない)ことを意味します。
②Context作成
const AuthContext = createContext<AuthContextType | null>(null);
ログイン情報を入れる箱です。
初期値はなにも入っていないのでnullです。
③useState
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [isLoggedIn, setIsLoggedIn] = useState(false);
初期値falseで設定。今まで解説したので省略。
④checkAuth()
※2026/09/27 @honey32様の指摘を参考に⑥に統合したため、この項目の記述は混乱防止のために削除
⑤login()
※2026/09/27 @honey32様の指摘を参考に修正
// ログイン成功後に認証状態を更新する
const login = async () => {
setIsLoggedIn(true);
};
ログイン状態をtrueにする。
⑥useEffect
※2026/09/27 @honey32様の指摘を参考に修正
// アプリ起動時にもCookieを確認する(F5対策)
// レンダーのたびに確認
useEffect(() => {
// Spring Bootへ認証状態を問い合わせる
const checkAuth = async () => {
try {
const response = await fetch("/api/auth/me", {
credentials: "include", // Cookieを自動送信する
});
setIsLoggedIn(response.ok);
} catch {
setIsLoggedIn(false);
} finally {
setLoading(false);
}
};
checkAuth();
},[]);
useEffectにおいて第二引数の有無と中身によって第一引数の実行タイミングが異なる意味になります。
※1.第二引数[]なし→レンダーのたびに(=Reactがコンポーネント関数を呼び出して差分を反映して画面を再描画するたびに)実行
2.第二引数[]が空配列→マウント時(そのコンポーネントが画面に使われ始める時)のみ
3.第二引数[]に値あり→マウント時および第二引数の値が変動した際に実行
checkAuthの中身としては、ブラウザで保存されているJWTのCookieをいっしょに送るという意味になります。
JWTが有効(ステータス:200)ならばtrue、無効(ステータス:401)ならばfalseになります。
通信失敗だったらcatchでログイン状態をfalseにします。
つまり、(寿命1時間の)Cookieが消えていなければログイン状態を保持します。
逆に消えていれば自動的にログイン状態がfalseとなり、「5.ログイン後のlayout.tsx」にある通りログイン画面に戻されます。
⑦Provider
※2026/09/27 @honey32様の指摘を参考に修正
export function AuthProvider({
children,
}: {
children: React.ReactNode;
}) {
return (
<AuthContext.Provider
value={{ isLoading, isLoggedIn, login }}
>
{children}
</AuthContext.Provider>
);
}
layout.tsxと同じ書き方です。
⑧useAuth()
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("AuthProviderの中でuseAuthを使ってください");
}
return context;
}
カスタムHookです。
上記のソースをまとめたもので、これがあるおかげで他ソースで呼び出せます。
例)
const { useAuth } = useAuth();
2.API
app/
api/
auth/
me/
route.ts
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const springResponse = await fetch(
`${process.env.SPRING_BOOT_API_BASE_URL}/api/auth/me`,
{
method: "GET",
// ブラウザから受け取ったCookieをSpring Bootへ渡す
headers: {
Cookie: request.headers.get("cookie") ?? "",
},
}
);
return new NextResponse(null, {
status: springResponse.status,
});
}
3.ログイン画面のlayout.tsx
これを反映させているソースを見てみましょう。
import { AuthProvider } from '../context/AuthContext'
function RootLayout({children,}:{children :React.ReactNode;})
{
return (
<html lang='ja'>
<body>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
)
}
ここでAuthProviderを使用することで認証情報を得ることができます。
4.ログイン画面のpage.tsx
以下のような関数を設定します。
const { login } = useAuth();
const router = useRouter()
const handleLogin = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault() // GET /?userId=... を防ぐ
const response = await fetch("/api/request/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId,
password,
}),
});
const result = await response.json();
if (result.result) {
await login();
router.push("/home");
} else {
alert(result.reason);
}
}
router.push("/home")でログイン後/homeに移ります。
5.ログイン後のlayout.tsx
function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
// AuthContextで共有しているログイン状態を取得する
const { isLoading,isLoggedIn } = useAuth();
const { isLoading,setLoading } = useState(false);
const router = useRouter();
// 未ログインならログイン画面へ戻す
useEffect(() => {
if (!isLoggedIn %% !isLoading) {
router.replace("/");
}
}, [isLoading, isLoggedIn, router]);
// 認証が完了するまで保護画面を表示しない
if (!isLoggedIn) {
return null;
}
return (
<div>
<div>
{children}
</div>
</div>
);
}
export default RootLayout;
6.Spring Boot側の制御
Controllerを以下のように設定
@RestController
@RequestMapping("/api/auth")
public class AuthController {
// ログイン状態を確認
@GetMapping("/me")
public ResponseEntity<?> me(Authentication authentication) {
if (authentication == null) {
return ResponseEntity.status(401).build();
}
return ResponseEntity.ok().build();
}
}
また、SecurityConfig.javaファイルを以下のように修正します。
.authorizeHttpRequests(auth -> auth
// /api/request(設定したAPIのリンク)からを全許可
.requestMatchers(HttpMethod.POST, "/api/request/*").permitAll()
// /api/auth/logout(ログアウト用のリンク)を全許可
.requestMatchers(HttpMethod.POST, "/api/auth/logout").permitAll()
// 現在H2DBを使用しているので全許可
.requestMatchers("/h2-console/**").permitAll()
// その他は認証必須=JWTがないとアクセス不可
.anyRequest().authenticated()
ここがないと許可されないので動かされないです。
あとがき
さて、フロントエンドとバックエンドの両方で認証情報を共有し、
問題があれば強制的にログイン画面("/")に遷移します。
ですが、これだとログイン後にログアウトしたいとき、内部で画面遷移したいときに対応できないですね。
次回はログアウトとサイドバーによる画面遷移制御を紹介します。
追記:
フロントエンド難しいです……。
最初に触れたのがバックエンドのJavaなのでフロントエンドの単語が難解に感じます……。
一段落したら本を1冊買ったのち公式ドキュメントも読んでみようかな。