2026年5月15日8 分鐘Claude API · Next.js · AI Chat · 資安強化 · DoS 防護 · Anthropic · rate limit · Web 開發

公開站點的 AI Chat widget 怎麼防被燒 token — 我們的 5 層防護

公司形象站開「跟 AI 對話」widget 是 2026 年的標配 — 但公開站台的 chat endpoint 是 DoS / token-burning 攻擊的高價值目標。攻擊者打 1000 次「請寫一萬字小說」就能燒掉你 NT$ 上千 API 額度。這篇拆我們做的 5 層防護架構,惡意攻擊者一天造成的成本上限壓在 NT$ 個位數。

陳先生 (Henry)

「我們公司形象站想加個 AI Chat widget — 客戶可以直接問問題,AI 回答介紹我們服務、給聯絡資訊。會不會被亂打?」

這是上個月一個客戶問我們的。我們自己 gomylife.tw 也有一樣的 widget — 公開、無登入、任何人可用。會不會被打?會。所以我們做了 5 層防護。

這篇拆完整架構:從 Apache vhost 強制覆寫 X-Forwarded-For 防 IP 偽造、到 in-memory rate limit、到 max_tokens 的 cost cap、到 SSE pass-through 串流。

實際效果:單一惡意攻擊者一天造成的成本上限壓在 NT$ 個位數(具體數字依設定而異)。

為什麼 AI Chat widget 是高價值攻擊目標 #

跟一般 Web 服務不同,AI Chat endpoint 有兩個獨特風險:

風險 一般 API AI Chat API
攻擊成本 DoS 才有意義 每個 request 可以燒掉真實 API 費用
攻擊代價放大 1:1 數量級的不對稱(攻擊者付的流量費 ≪ 你付的 API 費)
防護重點 阻止 request 阻止 + 限制每 request 成本

這就是為什麼只做 rate limit 不夠 — 還要限制「每次 request 最多花多少」。

我們的整體架構 #

[browser]
    ↓ POST /api/chat (JSON, expect SSE)
[Apache reverse proxy] ← 第 1 層
    ↓ ProxyPass
[Next.js /api/chat]   ← 第 2-4 層
    ↓ POST /chat/stream
[claude-proxy backend] ← 內網 only
    ↓ SSE
[Anthropic API]
    ↑ 逐字回串

5 層防護分布在這 4 個元件裡。

Layer 1:Apache 強制覆寫 X-Forwarded-For(IP 偽造防護) #

問題:攻擊者送 X-Forwarded-For: 8.8.8.8 偽造 IP,繞過 rate limit。

解法:在 Apache vhost 把 client 送來的 client-IP header 全部清掉,由 Apache mod_proxy 自己帶真實連線 IP:

<VirtualHost *:443>
    ServerName your-site.example.tw

    # 防 IP 偽造:清掉訪客送來的 client-IP headers
    RequestHeader set X-Forwarded-For "expr=%{REMOTE_ADDR}"
    RequestHeader unset X-Real-IP early
    RequestHeader unset CF-Connecting-IP early

    ProxyPass / http://nextjs-internal:3000/
    ProxyPassReverse / http://nextjs-internal:3000/
</VirtualHost>

關鍵點:

  • RequestHeader set X-Forwarded-For "expr=%{REMOTE_ADDR}" — 用 Apache expression 把 client 送的 X-Forwarded-For 覆寫成真實連線 IP(%{REMOTE_ADDR}
  • unset X-Real-IP earlyunset CF-Connecting-IP earlyearly 意思是「在 ProxyPass 之前清掉」,後面 Apache 自己重新帶
  • 沒 Cloudflare 在前面時這 3 行才是真的「不能被偽造的 IP」

如果你站台有 Cloudflare 在前面:用 mod_remoteip + RemoteIPHeader CF-Connecting-IP 信任 CF IPs,不要 unset CF-Connecting-IP(那個就是真實 IP)。

Layer 2:In-memory Rate Limit #

單實例場景用 in-memory Map 就夠(多實例後再上 Redis):

// lib/rate-limit.ts
// 具體數值依站台流量規模調整 — 範例用 N 代表
const MINUTE_LIMIT = N;   // 每 IP 每分鐘合理上限(個位數~雙位數)
const DAY_LIMIT = N * M;  // 每 IP 每日上限
const minuteMap = new Map<string, Bucket>();
const dayMap = new Map<string, Bucket>();

export function checkRateLimit(ip: string) {
  const m = check(minuteMap, ip, MINUTE_LIMIT, 60_000);
  if (!m.ok) return { ok: false, scope: "minute", resetAt: m.resetAt };

  const d = check(dayMap, ip, DAY_LIMIT, 86_400_000);
  if (!d.ok) return { ok: false, scope: "day", resetAt: d.resetAt };

  return { ok: true };
}

OPSEC 提醒:實際數值不要公開(包含這篇 blog 沒寫具體數字)。攻擊者拿到精確數值會「打到剛好不觸發」長期消耗。設定要對應你站台的真實流量規模。

抓 IP 的邏輯,順序很重要 — 先看 X-Forwarded-For(已被 Apache 覆寫成真實 IP),再 fallback:

export function getClientIp(req: Request): string {
  const xff = req.headers.get("x-forwarded-for");
  if (xff) return xff.split(",")[0]!.trim();  // 第一個 = 最近的 trusted proxy 帶的真實 IP

  return req.headers.get("x-real-ip")?.trim()
    ?? req.headers.get("cf-connecting-ip")?.trim()
    ?? "unknown";
}

加 sweep 機制避免長時間運行 memory 一直漲:

let lastSweep = Date.now();
function sweep() {
  const now = Date.now();
  if (now - lastSweep < 5 * 60_000) return;
  lastSweep = now;
  for (const [k, v] of minuteMap) if (now >= v.resetAt) minuteMap.delete(k);
  for (const [k, v] of dayMap) if (now >= v.resetAt) dayMap.delete(k);
}

Layer 3:Body Size + Message Length Guard #

防止「丟一個 100MB 的 message 過來」這種類 DoS:

const MAX_INPUT_LEN = 1500;  // 訊息上限 1500 字

if (message.length > MAX_INPUT_LEN) {
  return new Response(JSON.stringify({
    error: `訊息太長(上限 ${MAX_INPUT_LEN} 字)`
  }), { status: 400 });
}

配合 req.json() 預設的 1MB body 上限,雙保險。

為什麼設四位數上限:實測一般人問問題很少超過幾百字,多給足夠 buffer。攻擊者就算把每次都拉到頂,input tokens 也是有限的 — 對應 cost 很低

Layer 4:max_tokens 限制 — 最關鍵的 cost cap #

這是最關鍵的一層 — 限制 Anthropic 回應 token 數:

await fetch(`${PROXY_URL}/chat/stream`, {
  body: JSON.stringify({
    model: "haiku",       // 公開 endpoint 用 haiku 控成本
    message,
    max_turns: 1,         // 單回合,不開放對話 multi-turn
    max_tokens: TOKEN_CAP,  // ← 設定合理上限(如四位數)→ 防無限產文 → 控 DoS 成本
    system_prompt: SYSTEM_PROMPT,
    session_id: body.sessionId,
  }),
});

max_tokens cap 是關鍵 cost cap。算法:

單 IP 一天上限成本
= 模型每 token 單價 × max_tokens × 每日 rate limit 上限

依目前 Anthropic Haiku 定價跟我們的設定,單個攻擊者打滿一天,成本壓在 NT$ 個位數攻擊者付的流量費 vs 我們付的 API 計算費,量級不對稱 — 但實際金額不大,經濟上不划算

具體數字隨模型 / 設定 / 站台不同,重點是有 cap、cap 算得出來

如果要更小心可以再加:

max_turns: 1,  // 不允許 multi-turn 對話(避免上下文累積到上千 tokens)

Layer 5:SSE Pass-through(讓使用者體驗好 + 不重 buffer) #

讓 Claude 的回應逐字串流到瀏覽器,使用者一邊看一邊打字:

// 直接 pass-through 上游 SSE body,不要在 Next.js 端 buffer
return new Response(upstream.body, {
  status: 200,
  headers: {
    "content-type": "text/event-stream; charset=utf-8",
    "cache-control": "no-cache, no-transform",
    "x-accel-buffering": "no",  // ← 告訴 Apache / Nginx 別 buffer
    connection: "keep-alive",
  },
});

Apache vhost 也要配合:

ProxyTimeout 300
SetEnv proxy-sendchunked 1

# 關 ModSecurity(如果有開)— 它會 buffer 完整 response 才送
<IfModule security2_module>
    SecRuleEngine Off
</IfModule>

ModSecurity 不關掉的話 SSE 變一次性吐完,使用者體驗破壞 — 也是常見坑。

整體攻擊面評估 #

做完 5 層後的攻擊向量分析:

攻擊向量 我們的防護
IP 偽造繞 rate limit Apache RequestHeader set X-Forwarded-For "expr=%{REMOTE_ADDR}" — 攻擊者送的 header 被清掉
同一 IP 暴力呼叫 in-memory rate limit(每分鐘 + 每日雙窗口)
多 IP 分散攻擊(botnet) max_tokens cap 限單次成本,總成本可控
大 body / prompt injection message ≤ 1500 字 + system prompt 固定在 server 端
無限產文 token burning max_tokens cap + max_turns=1
越過 Next.js 直接打 backend claude-proxy 內網 only、公網 reach 不到
Prompt leak system_prompt 在 server 端注入,從不從 client 帶

Cost 攻擊評估 #

成本上限算式(不公開具體數字):

每次 cost ≈
  (input tokens × 模型 input 單價)
+ (max_tokens cap × 模型 output 單價)

每日上限 ≈ 每次 cost × 每日 rate limit

依目前我們的設定 + Anthropic Haiku 定價,攻擊者單日 cost 壓在 NT$ 個位數

Anthropic 每月額度給的話,這量級完全可吸收。

要再保險可以申請 Anthropic API 設 monthly spend cap,到頂就停 — 但實務上沒必要,我們上線數月還沒看過接近上限。

監控 #

我們在 Mattermost 頻道接 alert,三個觸發條件:

# 1. rate limit 觸發數
docker logs your-app 2>&1 | grep -E "rate limit|429" | wc -l
# 異常 = >100/小時

# 2. honeypot 觸發數(contact form 那邊)
grep "honeypot triggered" | wc -l

# 3. Anthropic API 帳單監測
curl https://anthropic.com/api/usage  # 假設有這個 endpoint

我們刻意沒做的 #

沒做的 為什麼
Cloudflare Turnstile 5 層防護應該擋掉大部分 bot,等實際被打爆再加
reCAPTCHA v3 同上 + 隱私顧慮(會打 Google)
Cloudflare WAF 我們架構是直接 server,沒過 CF
Anthropic monthly spend cap max_tokens 已 cap,雙保險過頭了

重點總結 #

Layer 元件 防什麼
1 Apache vhost RequestHeader IP 偽造 5 分鐘
2 in-memory rate limit 暴力打 30 分鐘
3 Body / message length guard 巨大 payload 5 分鐘
4 max_tokens cost cap Token burning 1 行設定
5 SSE pass-through UX + buffer 防護 15 分鐘

總工 1 小時左右。換來「攻擊者一天造成的成本壓在 NT$ 個位數」— 經濟上不值得攻擊。

想做類似的 AI Chat 整合 #

我們做 Web 應用整合 Claude / Anthropic API,包含:

  • 公司形象站 AI Chat widget(hero 加 AI 對話)
  • 客服自動回覆系統(接 LINE OA / 表單)
  • 內部知識庫問答(接公司資料 + Claude RAG)
  • AI 助手嵌入既有 Web 系統

聯絡:0912852835 / henryccy@icloud.com / LINE @3q3tw / 線上表單

延伸閱讀:

想聊類似的東西?

諮詢免費,依工時報價。

聯絡我們