maxAge と expires の違い
Cookie の有効期限は maxAge(秒数指定)または expires(日時指定)で設定します。
maxAge | expires | |
|---|---|---|
| 指定方法 | 秒数(相対) | Date オブジェクト(絶対) |
| 基準時刻 | ブラウザが受信した時刻から計算 | クライアントの時計に依存 |
| 推奨 | ✅ こちらを使う | クライアント時計のずれに影響される |
// ✅ maxAge を使う(相対指定。クライアント時計の影響を受けない)
cookieStore.set("session_id", token, {
maxAge: 60 * 60 * 24 * 7, // 7日(秒単位)
});
// ⚠️ expires を使う場合(絶対日時。サーバーとクライアントの時計差に注意)
cookieStore.set("session_id", token, {
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
});
maxAge と expires が両方指定された場合、maxAge が優先されます。基本は maxAge のみで十分です。
固定期限とスライディング期限
固定期限(Fixed Expiry)
ログイン時に設定した期限がそのまま続き、更新されません。
10:00 ログイン → Cookie の期限: 翌 10:00(24 時間後)
10:30 アクセス → 期限変わらず翌 10:00
23:50 アクセス → 翌 10:00 まであと 10 分。操作中にセッション切れになりうる
翌 10:01 アクセス → セッション切れ → ログイン画面へ
シンプルで実装コストが低い反面、「作業中にセッションが切れる」体験が発生しやすい。
スライディング期限(Sliding Expiry)
アクセスのたびに期限を延長します。最後のアクセスから N 時間操作がなかった場合にセッション切れになります。
10:00 ログイン → Cookie の期限: 翌 10:00(24 時間後)
14:00 アクセス → Cookie の期限: 翌 14:00 に延長
23:00 アクセス → Cookie の期限: 翌々日 23:00 に延長
↓(以降 24 時間アクセスなし)
翌々日 23:01 → セッション切れ
「使っている限りログインが切れない」体験になるが、absolute 上限がないと永続ログイン状態になる。実務では「最大○日」の上限を別途設ける。
スライディング延長: アクセスのたびに +24 時間
Absolute 上限: ログインから 30 日後に強制セッション切れ
どのタイミングで Cookie を更新するか
毎リクエスト更新
// middleware.ts — 全リクエストで Cookie を延長する
export function middleware(request: NextRequest) {
const token = request.cookies.get("session_id")?.value;
if (!token) return NextResponse.next();
const response = NextResponse.next();
// アクセスのたびに期限を延長(スライディング)
response.cookies.set("session_id", token, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 24, // 24 時間リセット
});
return response;
}
常に最新の期限になる反面、すべてのリクエストで Set-Cookie ヘッダーが付く。
閾値更新(残り期限が X 以下になったら更新)
// middleware.ts — 残り 1 時間を切ったら延長する
export function middleware(request: NextRequest) {
const sessionExpires = request.cookies.get("session_expires")?.value;
if (!sessionExpires) return NextResponse.next();
const expiresAt = new Date(sessionExpires);
const remainingMs = expiresAt.getTime() - Date.now();
const oneHour = 60 * 60 * 1000;
const response = NextResponse.next();
// 残り 1 時間以下のときだけ更新する
if (remainingMs < oneHour) {
const newExpires = new Date(Date.now() + 24 * 60 * 60 * 1000);
response.cookies.set("session_id", request.cookies.get("session_id")!.value, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 24,
});
response.cookies.set("session_expires", newExpires.toISOString(), {
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 24,
});
}
return response;
}
更新頻度を抑えられるが、判定用に期限情報を別 Cookie に持たせる必要がある。
JWT と DB Session で更新処理はどう変わるか
JWT の場合
JWT の期限は ペイロードの exp フィールドに埋め込まれます。期限を延長するには新しい JWT を再発行する必要があります。
[期限延長の流れ(JWT)]
1. リクエスト時に JWT を検証
2. 残り期限が閾値以下なら新しい JWT を署名して発行
3. 新しい JWT を Cookie にセット(古い JWT は捨てる)
// lib/session.ts
import { SignJWT, jwtVerify } from "jose";
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);
export async function refreshJwtIfNeeded(token: string): Promise<string | null> {
try {
const { payload } = await jwtVerify(token, SECRET);
const exp = payload.exp as number;
const remainingSec = exp - Math.floor(Date.now() / 1000);
// 残り 1 時間以下なら再発行
if (remainingSec < 3600) {
return await new SignJWT({ userId: payload.userId, role: payload.role })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("24h")
.sign(SECRET);
}
return null; // 更新不要
} catch {
return null;
}
}
DB Session の場合
Session データは DB 側に expiresAt カラムとして持ちます。Cookie と DB の両方を更新します。
[期限延長の流れ(DB Session)]
1. Cookie の Session ID で DB を検索
2. expiresAt が閾値以下なら DB の expiresAt を更新
3. Cookie の maxAge も同じ期限に延長
// lib/session.ts
export async function refreshSessionIfNeeded(sessionId: string): Promise<boolean> {
const session = await db.session.findUnique({ where: { id: sessionId } });
if (!session) return false;
const remainingMs = session.expiresAt.getTime() - Date.now();
const oneHour = 60 * 60 * 1000;
if (remainingMs < oneHour) {
await db.session.update({
where: { id: sessionId },
data: { expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) },
});
return true; // Cookie の maxAge も更新が必要
}
return false;
}
DB Session では Cookie と DB の期限を必ず同期させます。どちらか一方だけ更新するとずれが生じます。
よくある誤用
Cookie の期限だけ更新してサーバー側を更新しない
❌ Cookie: maxAge を延長した(ブラウザ上では有効に見える)
DB Session: expiresAt は古いまま → 次のリクエストでサーバーが期限切れと判断
→ ログインしているのに操作できない状態になる
✅ Cookie と DB Session(または JWT の再発行)を必ずセットで更新する
JWT を毎リクエスト再発行する
JWT の署名には CPU コストがかかります。毎リクエスト再発行すると不要な処理が増えます。「残り期限が X 分以下になったら更新」の閾値方式が現実的です。
スライディング期限に absolute 上限を設けない
スライディング期限のみ: 毎日アクセスしていると永続ログイン状態になる
✅ 「最後のアクセスから 24 時間」+「ログインから最大 30 日」の 2 層構造にする
→ どちらかの期限に達したらセッション切れ
期限を長くしすぎる
Cookie の期限を長くするほど、トークンが窃取された場合に有効期間が長く残ります。即時無効化できない JWT では特にリスクが高まります。要件に必要な最短の期限を設定することが基本です。
Cookie のセキュリティ属性(httpOnly / sameSite / secure) → Cookie セキュリティオプション 3点セット
Session 取得 API の責務分離 → 型安全な Session 管理パターン
JWT と DB Session の保存方式比較 → JWT vs DB Session 比較