きっかけ
QRコードを使った注文フォームシステムを開発していたとき、こんな疑問が出ました。
「iPhoneのSafariでプライベートブラウズしたら、Cookieが消えてQRトラッキングが壊れるのでは?」
調べてみると、「Cookieが消える」という話は本当だが、それはCookieの種類による ということがわかりました。開発の現場でよく混同されるポイントなので、整理します。
Cookieには2種類ある
Cookieは誰が書くかによって性質が変わります。
1. JavaScriptが書くCookie(クライアントサイド)
// ブラウザ上で実行されるJavaScript
document.cookie = "user_id=abc123; path=/"
document.cookie でブラウザから書いたCookieです。JavaScriptから読み書き両方できます。
2. サーバーが書くCookie(HttpOnly)
HTTP/1.1 200 OK
Set-Cookie: session_token=xyz; HttpOnly; Secure; SameSite=Lax
サーバーがHTTPレスポンスヘッダーの Set-Cookie で送るCookieです。HttpOnly フラグが付いているとき、JavaScriptからは一切読み書きできません。
Safari ITPが制限するのはどちらか
SafariのITP(Intelligent Tracking Prevention)が主に制限するのはクロスサイトのトラッキングです。具体的には次のケースです。
document.cookieでJavaScriptが書いたCookie- サードパーティ(別ドメイン)から発行されたCookie
localStorageやIndexedDBへの第三者アクセス
同一ドメインのサーバーが Set-Cookie で発行した HttpOnly Cookie はITPの制限対象外 です。
Next.jsでの実装
今回のシステムでは、QRコードのトラッキングCookieをサーバーサイドで発行しています。
① middleware.ts(QRスキャン時)
// /porder/apply?src=<slug> へのアクセスをmiddlewareで捕捉
export default clerkMiddleware(async (auth, req) => {
const url = new URL(req.url)
if (url.pathname === "/porder/apply") {
const src = url.searchParams.get("src")
if (src) {
const response = NextResponse.next()
response.cookies.set("porder_qr_src", src, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 60 * 60 * 24 * 30, // 30日
})
return response
}
}
})
NextResponse.next() に対して cookies.set() しているので、これはサーバーが送る Set-Cookie ヘッダーです。JavaScriptは関与しません。
② Server Action(注文時)
"use server"
import { cookies } from "next/headers"
export async function submitOrder(formData: FormData) {
const cookieStore = await cookies()
// サーバーサイドでCookieを読み取る
const orderSource = cookieStore.get("porder_qr_src")?.value ?? null
await prisma.personalOrder.create({
data: {
// ... 他のフィールド
orderSource, // QRコードのスラッグを記録
},
})
}
next/headers の cookies() はサーバー専用APIです。"use server" がついたServer Action内でのみ動作します。
プライベートブラウズでの実際の動作
では、Safariのプライベートブラウズでは何が起きるのか。
| 操作 | 通常モード | プライベートモード |
|---|---|---|
| QRスキャン → Cookie受け取り | ✅ | ✅ |
| 同じタブで注文完了 | ✅ | ✅ |
| タブを閉じてCookieが消える | ||
| 別タブで同URLを開き直す | ✅ | △(Cookieなし) |
HttpOnly Cookieはプライベートモードでも受け取れます。 問題になるのは「タブを閉じた後」です。プライベートモードではセッション終了時(タブを閉じたとき)にすべてのCookieが削除されます。
実際に問題になるケース
今回のQRトラッキングで問題になりうるのは、次のシナリオだけです。
1. QRをスキャン(Cookieセット)
2. 何らかの理由でタブを閉じる
3. ブックマークやメールのリンクから開き直す
→ Cookieが消えているため orderSource が null になる
このシナリオはプライベートモード限定かつ「タブを閉じた場合」のみ。一連の操作を同じタブで完結するなら問題ありません。 チラシを手にしたお客様がカメラでQRを読んでその場で注文する、という想定フローでは実用上の支障はないと判断しました。
まとめ
| クライアントサイドCookie | HttpOnly Cookie | |
|---|---|---|
| 発行者 | JavaScript | サーバー(HTTPヘッダー) |
| JS から読み取り | ○ | × |
| XSS耐性 | 弱い | 強い |
| Safari ITP の影響 | 受ける | 受けない(同一ドメイン) |
| プライベートモードでの消去 | タブを閉じたとき | タブを閉じたとき |
セッション管理やトラッキングCookieは、可能であれば HttpOnly + SameSite=Lax でサーバーから発行するのがセキュリティ・ITP耐性ともにベターです。Next.jsの場合、middleware または Server Action の cookies() API を使えばクライアントJavaScriptを一切使わずにCookieを制御できます。