Next.js API 認証とセキュリティ:JWT からレート制限まで完全実践ガイド

午前3時、私のスマホが震えました。クラウドプロバイダーからの請求アラートでした。金額は7800ドル。
目をこすりながら見直しました。先月の請求額は120ドルだったはず。詳細を見ると、API 呼び出し回数が1800万回を超えていました。私の個人プロジェクトで、普段は1日数百回程度なのに。
原因は、私が何の保護もかけていなかった API エンドポイントを特定したあるボットが、3日間ぶっ通しでアクセスし続けたことでした。その瞬間、私は「API セキュリティはオプションではない」という言葉の重みを痛感しました。
正直なところ、多くの人(以前の私も含め)が Next.js プロジェクトを立ち上げる際、ページの見た目やインタラクションの滑らかさばかりを気にかけ、API のセキュリティはバックエンド担当の仕事だと思いがちです。しかし、Next.js の API Routes は本質的にあなたのバックエンドそのものです。あなたが守らなければ、誰も守ってくれません。
この記事では、私が数年かけて踏んできた地雷と、調査したソリューションを体系的に整理しました。JWT 認証から CORS 設定、レート制限から入力検証まで、単なる理論ではなく、プロジェクトですぐに使える実践的なコードとして紹介します。
なぜ API セキュリティが重要なのか
API セキュリティに対する一般的な脅威
昨年12月、React 公式から重大なセキュリティ勧告が出されました。CVE-2025-55182、CVSS スコアは満点の 10.0 です。
これは、攻撃者が特定の HTTP リクエストを作成するだけで、あなたのサーバー上で任意のコードを実行できることを意味します。React Server Components を使用していて更新を怠っていれば、実質的に丸裸の状態です。
これだけではありません。今年3月には認可バイパス脆弱性 (CVE-2025-29927) も発見されました。スコアは 9.1。攻撃者はリクエストヘッダーを偽装するだけでミドルウェアの認証を回避できました。
日常的な脅威も無視できません:
悪質なクローラーと DDoS 攻撃:レート制限のない API は一瞬でダウンさせられます。私の知人が運営するサイトのログイン API は、1秒間に3000回のパスワード総当たり攻撃(ブルートフォース)を受け、サーバーがダウンしました。
データ漏洩:権限管理が不十分で、ユーザーAがユーザーBの注文情報を見れてしまう。これが発覚すれば、ブランドの評判は地に落ちます。
インジェクション攻撃:SQL インジェクション、XSS、コマンドインジェクション…古臭く聞こえるかもしれませんが、今でも多くのプロジェクトが餌食になっています。「React が自動エスケープしてくれるから大丈夫」と思っていても、API 側で検証していなければ無意味です。
Next.js API Routes の特徴とリスク
Next.js の API Routes は従来のバックエンドとは少し異なります。
サーバーレスファースト:Vercel にデプロイする場合、各 API リクエストは独立したサーバーレス関数として実行されます。自動スケーリングは便利ですが、ステートレスであるため、従来のメモリ内セッションは使えず、JWT や DB セッションが必要です。
フロントエンドとの混在:コードが同じリポジトリにあるため、環境変数が誤ってクライアントに漏洩しやすいです。DATABASE_URL を .env に入れたつもりが、バンドルに含まれて GitHub で公開されてしまったケースを見たことがあります。
エッジコンピューティングの制限:Edge Runtime を使用する場合、一部の Node.js API が使用できず、暗号化ライブラリや DB 接続方法を選び直す必要があります。
API 認証の実戦
認証方式の選び方
現実的な問題として、JWT と Session、どちらを使うべきでしょうか?
JWT が適しているケース:
- 複数のサーバー(サーバーレス、エッジノード)にデプロイする場合
- 異なるドメイン間で認証が必要な場合(app.com と api.com)
- セッション管理をできるだけシンプルにしたい場合
JWT の本質は、ユーザー情報をトークンにエンコードし、サーバー側で状態を持たないことです。水平スケーリングが非常に容易です。
Session が適しているケース:
- サーバー側で能動的に制御したい場合(特定ユーザーの強制ログアウト、権限のリアルタイム変更)
- セキュリティ要件が極めて高く、クライアントに一切ユーザー情報を持たせたくない場合
- 既に Redis や DB があり、セッション管理が負担にならない場合
私の選択基準:個人プロジェクトや小規模なアプリなら JWT(手軽だから)。企業レベルや細かい制御が必要なら Session。
JWT 認証の完全実装
では、JWT を採用する場合の Next.js での実装方法を見ていきましょう。
ステップ1:トークンの生成と検証
まずライブラリをインストールします:
npm install joseなぜ jsonwebtoken ではなく jose か? Edge Runtime をサポートしているからです。
lib/auth.ts:
import { SignJWT, jwtVerify } from 'jose';
const secret = new TextEncoder().encode(
process.env.JWT_SECRET || 'your-secret-key-at-least-32-characters'
);
export async function createToken(payload: { userId: string }) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('15m') // 15分で期限切れ
.sign(secret);
}
export async function verifyToken(token: string) {
try {
const { payload } = await jwtVerify(token, secret);
return payload;
} catch {
return null;
}
}注意すべきは 15m という短い有効期限です。多くの人が7日や30日に設定しますが、トークンが漏れた場合のリスクが高まります。短期の Access Token + 長期の Refresh Token が正解です。
ステップ2:トークンの保存——localStorage は絶対ダメ
ここが最重要ポイントです。多くのチュートリアルが localStorage への保存を推奨していますが、XSS 攻撃を受ければ一発で盗まれます。
正解は HttpOnly Cookie です。
// app/api/login/route.ts
import { NextResponse } from 'next/server';
import { createToken } from '@/lib/auth';
export async function POST(request: Request) {
// ユーザー検証ロジック...
const token = await createToken({ userId: user.id });
const response = NextResponse.json({ success: true });
response.cookies.set('token', token, {
httpOnly: true, // JS からアクセス不可(XSS対策)
secure: true, // HTTPS 通信のみ
sameSite: 'lax', // CSRF対策
maxAge: 900, // 15分
});
return response;
}ステップ3:ミドルウェアによる API 保護
Next.js の Middleware を使って、認証が必要な API を一括で保護します。
middleware.ts:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyToken } from './lib/auth';
export async function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
const payload = await verifyToken(token);
if (!payload) {
return NextResponse.json(
{ error: 'Invalid token' },
{ status: 401 }
);
}
// 検証成功後、ユーザー情報をヘッダーに付与して API へ渡す
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', payload.userId as string);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
export const config = {
matcher: '/api/protected/:path*',
};これで /api/protected/* 以下のすべての API が自動的に保護されます。
NextAuth.js (Auth.js) による高速統合
もし上記の実装(特にリフレッシュトークンやOAuth)を自分で書くのが大変だと感じるなら、NextAuth.js (現在は Auth.js) を使いましょう。
セキュリティのベストプラクティス(CSRF保護、セッション暗号化など)がデフォルトで組み込まれており、Google や GitHub ログインも数行で実装できます。
CORS 設定の詳細解説
CORS の本質と問題
CORS(Cross-Origin Resource Sharing)は多くの開発者を悩ませます。開発環境(localhost)では問題ないのに、本番にデプロイするとエラーが出る…。
ブラウザは、あるドメイン(app.example.com)のページから別のドメイン(api.example.com)の API を叩く際、「このリクエストを許可しますか?」と API 側に確認します。これが CORS です。
Next.js での3つの設定方法
1. next.config.js でのグローバル設定
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: 'https://app.example.com' },
{ key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE' },
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
],
},
];
},
};2. Middleware での設定(動的制御向け)
Middleware を使えば、リクエストの送信元に応じて動的に許可を与えることができます。
3. API Route 内での設定
特定の API だけ公開したい場合などに使います。
CORS セキュリティのベストプラクティス
ワイルドカード * を乱用しない:Access-Control-Allow-Origin: * は「誰でも私の API を叩いていいよ」という意味です。公開データならいいですが、ユーザー情報や操作 API でこれをやると、誰でもあなたの API を利用したフィッシングサイトを作れてしまいます。
Preflight (OPTIONS) リクエストの処理:Authorization ヘッダーや Content-Type: application/json を含むリクエストは、ブラウザが自動的に OPTIONS メソッドで事前確認(Preflight)を送ります。API がこれを適切に処理(200 OK と許可ヘッダーを返す)しないと、CORS エラーになります。
API レート制限(Rate Limiting)
冒頭の話に戻りますが、もし私がレート制限を導入していれば、被害は数万回のアクセス拒否ログだけで済み、7800ドルも請求されることはなかったでしょう。
おすすめのソリューション:@upstash/ratelimit
サーバーレス環境(Vercel 等)では、メモリ内にカウントを保存する方法は機能しません(リクエストごとに新しいインスタンスが立ち上がる可能性があるため)。Redis などの外部ストアが必要です。
Upstash は Vercel と相性が良い Serverless Redis で、専用のライブラリがあります。
npm install @upstash/ratelimit @upstash/redis実装例:
// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
// 10秒間に10回まで
export const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, '10 s'),
analytics: true,
});API での使用:
// app/api/protected/route.ts
import { NextResponse } from 'next/server';
import { ratelimit } from '@/lib/rate-limit';
export async function GET(request: Request) {
const ip = request.headers.get('x-forwarded-for') || 'unknown';
const { success } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
return NextResponse.json({ data: 'Success' });
}入力検証と防御
「ユーザー入力を決して信頼するな」——これはセキュリティの鉄則です。フロントエンドでの検証は UX のためであり、セキュリティのためではありません(簡単にバイパスできるからです)。
Zod による型安全な検証
Zod は TypeScript と相性抜群のスキーマ検証ライブラリです。
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
age: z.number().int().min(18),
});
export async function POST(request: Request) {
const body = await request.json();
const result = userSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: 'Input validation failed', details: result.error.format() },
{ status: 400 }
);
}
// result.data は型安全
const { email } = result.data;
// ...
}これにより、SQL インジェクションのリスクを減らし(ORM と併用)、不正なデータ形式によるサーバーエラーを防ぐことができます。
まとめ:完全セキュリティチェックリスト
最後に、ご自身のプロジェクトを診断するためのチェックリストを提供します。
認証
- トークンは HttpOnly Cookie に保存されているか(localStorage は不可)
- Access Token の有効期限は短いか(例:15-30分)
- Refresh Token の仕組みはあるか
- JWT シークレットは十分長く、環境変数に保存されているか
CORS
- 敏感な API に
Access-Control-Allow-Origin: *を使っていないか - 許可するオリジンを明示的に指定しているか
- OPTIONS リクエストを適切に処理しているか
レート制限
- ログイン、登録などの重要エンドポイントに厳格なレート制限があるか
- 認証済みユーザーと未認証ユーザーで制限を分けているか
- 429 ステータスコードを返しているか
入力検証
- すべてのユーザー入力をサーバー側で検証しているか
- Zod などのライブラリを使用しているか
- 文字列や配列の最大長を制限しているか(DoS 対策)
その他
-
npm auditを定期的に実行し、依存関係の脆弱性を修正しているか - 秘密鍵(API Keyなど)が
NEXT_PUBLIC_プレフィックス付きで公開されていないか
API セキュリティは地道な作業ですが、一度設定してしまえば、あとはテンプレートとして使い回せます。7800ドルの請求書が届く前に、今すぐ対策を始めましょう。
FAQ
Next.js API では JWT と Session どちらを使うべきですか?
なぜ JWT トークンを localStorage に保存してはいけないのですか?
開発環境では CORS エラーが出ないのに、本番環境で出るのはなぜですか?
サーバーレス環境(Vercel 等)でレート制限を実装するには?
API へのブルートフォース攻撃(総当たり攻撃)を防ぐには?
5 min read · 公開日: 2026年1月5日 · 更新日: 2026年1月22日
関連記事
Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践

Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践
Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド

Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド
Next.js ユニットテスト実践:Jest + React Testing Library 完全設定ガイド


コメント
GitHubアカウントでログインしてコメントできます