第 3 週 · 資料庫與 D1
Worker 連接 D1、prepared statements、防止 SQL injection
用 env.DB.prepare 與問號占位,不把客人的字拼進 SQL。
正在打開這一關…
學習目標
- 用 prepare、bind、all、first、run 讀寫 D1
- 指出把使用者輸入拼進 SQL 的危險
- 讀得懂一筆查詢回傳的 results
資料庫已經有表。今天讓 Worker 對 env.DB 說話。重點只有一個:SQL 句子與客人的值要分開送。這叫 prepared statement。
用生活例子理解
你準備一張填空題:
SELECT * FROM messages WHERE author = ?問號是空格。bind("Ada") 是把答案放進空格,資料庫知道這是值,不是句子的一部分。
若你自己組字串:
// 不要這樣寫。
const sql = `SELECT * FROM messages WHERE author = '${author}'`;客人只要把 author 設成 ' OR 1=1 -- 這一類文字,句子的意思就被改掉。這就是 SQL injection。輕則偷看全部資料,重則刪表。
生活比喻:填空題的空格只能填單字。若你允許客人改題目本身,他就會把「寫出你的名字」改成「並且把檔案櫃打開」。
D1 的四個動作:
| 方法 | 何時用 |
|---|---|
all() |
期待很多列,例如清單 |
first() |
期待零列或一列 |
run() |
新增、更新、刪除,看影響了幾列 |
bind(...) |
依序填進問號 |
all() 的回傳裡,列在 results。run() 之後,新增列的編號常在 meta.last_row_id。
動手做
大約 25 分鐘。沿用昨天的資料庫,記得先 --local apply。
- 把案例貼進
src/index.js,只留GET /api/messages與帶?author=的篩選。 npx wrangler dev,打開/api/messages。- 用
d1 execute插入一列固定資料:
npx wrangler d1 execute guestbook-db --local \
--command "INSERT INTO messages (author, body) VALUES ('Ada', '哈囉');"- 再整理
/api/messages與/api/messages?author=Ada。 - 在筆記寫下「危險拼接」與「問號填空」各一行,標出哪一行絕對不能進專案。
- 試著把作者改成含有單引號的
O'Neil的查詢字串。填空版應該當成普通名字,而不是 SQL 錯誤。
案例
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname !== "/api/messages" || request.method !== "GET") {
return Response.json({ error: "找不到" }, { status: 404 });
}
const author = url.searchParams.get("author");
if (author) {
const { results } = await env.DB.prepare(
"SELECT id, author, body, created_at FROM messages WHERE author = ? ORDER BY id DESC",
).bind(author).all();
return Response.json(results);
}
const { results } = await env.DB.prepare(
"SELECT id, author, body, created_at FROM messages ORDER BY id DESC",
).all();
return Response.json(results);
},
};沒有任何使用者的字被加進引號裡。問號的順序就是 bind 的順序。兩個問號就 bind(第一個, 第二個)。
靜態的句子(完全沒有外來的值)仍建議走 prepare,習慣一致。exec 適合一次跑你自己寫死的多句 SQL,不適合接客人的輸入。
常見錯誤
- 覺得「我有先檢查長度,所以拼接沒關係」。長度檢查擋不住精心構造的短句子。
- 自己把單引號替換成兩個單引號,以為就安全。規則一多就會漏。用問號。
- 忘了
await。你拿到的會是 Promise,不是列。 - 對
first()的結果直接讀.author,卻沒想過它可能是null(沒有這列)。
自我檢查
- 問號與
bind各做什麼? - 為什麼「把輸入裡的引號拿掉」不是完整的防護?
- 清單該用
all、first還是run?