Learning Objectives
- 說明一個有登入、資料庫與 API 的網站,各個檔案與各層分別負責什麼
- 在現成的網站裡找到缺少的權限檢查與驗證,並把它補上
- 依照既有的寫法替 API 補上一個新路由
- 用 pytest 替權限與錯誤路徑寫測試,並說明為什麼值得
- 規劃接下來要學的工具,以及這個網站還可以怎麼延伸
要做出什麼
前面每一課都只處理一個觀念。這一課把它們組成一個能用的網站:小型課程討論區。你會先操作完整的網站,再把故意拿掉的幾段補回來。
| 功能 | 用到的觀念 | 課次 |
|---|---|---|
| 註冊、登入、登出 | 雜湊密碼、Session | 14、15 |
| 文章列表、發文、編輯 | 路由、模板、表單、PRG | 3、6~8 |
| 資料存進 SQLite、軟刪除 | 參數化查詢、commit |
9、10 |
| 只有作者能改、管理員可刪 | 資料擁有者檢查、角色 | 16 |
| 回覆走 JSON API,頁面不重新載入 | API、fetch、textContent |
11~13 |
| 拆成幾個檔案、自動測試 | 應用程式工廠、Blueprint、pytest | 18 |
檔案怎麼分:
| 檔案 | 內容 |
|---|---|
app.py |
create_app():把設定、資料庫、兩個 Blueprint 組起來 |
pages.py |
回傳 HTML 的路由,給瀏覽器 |
api.py |
回傳 JSON 的路由,給 fetch 和其他程式 |
common.py |
兩邊共用:get_db、current_user、login_required、get_post |
schema.sql、seed.sql |
三張資料表(users、posts、replies)與示範文章 |
templates/ |
Jinja2 模板,都繼承 base.html |
static/ |
forum.js(回覆用 fetch 送出)、style.css |
路由一覽:
| 方法與路徑 | 做什麼 | 誰可以 |
|---|---|---|
GET /、GET /posts/<id> |
看文章 | 任何人 |
POST /posts/new |
發文 | 登入者 |
POST /posts/<id>/edit |
編輯 | 只有作者 |
POST /posts/<id>/delete |
軟刪除 | 作者或管理員 |
GET /api/posts |
文章列表(JSON、分頁) | 任何人 |
GET /api/posts/<id> |
文章與回覆(JSON) | 任何人 |
POST /api/posts/<id>/replies |
新增回覆 | 登入者 |
逐步看:一則回覆要通過哪些檢查
這是把 add_reply 抽出來的簡化版。同一個路由送四次:還沒登入、文章不存在、內容空白、全部正確。注意每一次在哪一行離開函數,以及狀態碼有什麼不同。
示範帳號與操作方式
下面是完整的討論區,程式就在編輯器裡。有三個示範帳號,密碼都是 pw12345678:
| 帳號 | 角色 | 說明 |
|---|---|---|
[email protected] |
管理員 | 陳老師,可以刪除任何文章 |
[email protected] |
學生 | 文章 2 的作者 |
[email protected] |
學生 | 文章 1 的作者 |
送出回覆時,注意下面的「請求紀錄」只多了一行 POST /api/posts/…/replies,頁面沒有重新載入;開始操作前也可以先看一下「資料庫」面板。
回覆的按鈕沒有放在
<form>裡,是因為這個模擬瀏覽器會接手所有表單。真實的網站也常見另一種寫法:用表單的submit事件,先呼叫event.preventDefault()再fetch。
換你補上缺的部分
下一個練習的網站看起來一樣,但少了三段程式,都標了 TODO。這三個問題光看畫面不一定發現得了,所以先重現問題,再修:
pages.py的edit_post:畫面上沒有編輯連結,但沒有檢查作者(第 16 課的漏洞)api.py的add_reply:沒有驗證內容,空白回覆也會寫進資料庫api.py:少了GET /api/posts/<id>這個路由
每次改完按「存檔並重新啟動」,資料庫和登入狀態都會保留。程式出錯時,錯誤訊息在「伺服器紀錄」。卡住時可以按「還原程式」重來,也可以問右下角的陪讀 AI 助教。
修權限時,只擋別人還不夠,要確認自己的文章還能編輯:任務會檢查這兩件事。驗證也一樣,空白要擋,正常的回覆要能通過。
逐步看:讓測試幫你記住這些規則
上面的權限與驗證,你是用手操作確認的。手動測試很容易下次就忘了做,所以把它寫成測試。這裡真的執行 pytest,測的就是上一個練習的完整版程式。按到最後一步看輸出,測試內容在輸出區的 test_forum.py 分頁。想看一個測試怎麼逐行執行,可以回到第 18 課。
這幾個測試在保護什麼
打開輸出區的 test_forum.py,它做了幾件值得照抄的事:
clientfixture 用tmp_path:每個測試都有一個全新的資料庫,彼此不影響,也不會弄髒開發用的site.dblogin_as:用session_transaction直接設定身分。登入流程本身另外測一次就好,不必每個測試都走表單- 錯誤路徑比正常路徑多:沒登入、空白內容、別人的文章。安全問題幾乎都在這裡,也是手動操作時最容易略過的地方
- 同一個測試裡比較兩種身分:
test_only_author_can_edit同時確認「別人不行」和「自己可以」,防止你把程式改成全部擋掉
如果有人日後改了 edit_post、不小心刪掉作者檢查,test_only_author_can_edit 會立刻失敗。這就是測試的價值:規則寫下來之後,由程式負責記住。
輸出裡的 0.00s 是這個環境固定的值,在你的電腦上會是實際的秒數。
動手做:刪除文章的權限
完成 delete_post,檢查順序很重要:
- 沒登入(
user是None):abort(401) - 文章不存在或已刪除(
post是None):abort(404) - 不是作者,也不是
admin:abort(403) - 通過才軟刪除:
is_deleted設成 1,資料列留著
起始的寫法任何人都能刪,而且真的把資料刪掉。程式最下面的測試不要改。
想一想
文章頁已經用 {% if me.id == post.user_id %} 把編輯連結藏起來了。edit_post 裡還需要檢查作者嗎?
回覆內容是 <img src=x onerror=alert(1)>。前端寫 item.innerHTML = data.body 會發生什麼?
api.py 為什麼要有 @api.errorhandler(404)?
新增回覆時,user_id 為什麼從 session 取,不讓前端放在 JSON 裡傳過來?
測試的 client fixture 為什麼要用 tmp_path 建立資料庫,不直接用 site.db?
延伸:接下來可以學什麼
把這個討論區做得更完整:
- 文章列表加上分頁與搜尋(
GET /api/posts已經有page,前端還沒用) - 回覆也能編輯與刪除:套用同樣的擁有者檢查,並補上測試
- 加上 CSRF token(第 17 課)。這個專題目前只靠
SameSite=Lax - 登入嘗試限流、註冊時驗證信箱
- 照第 19 課的檢查清單實際部署一次
這個專題刻意自己寫的部分,實務上常用現成的工具:
| 工具 | 取代什麼 |
|---|---|
| Flask-Login | 自己寫的 login_required、current_user、記住我 |
| Flask-WTF | 表單驗證與 CSRF token |
| Flask-SQLAlchemy | 手寫 SQL 與連線管理 |
| Flask-Migrate | 手動改資料表結構:schema.sql 只能建表,改欄位要靠遷移工具 |
先自己寫過一次,你才知道這些工具幫你做了什麼。
其他軌道:
- SQL:
JOIN、索引、交易,換成 MySQL 時用得上 - JavaScript:把前端做得更完整
- Docker、雲端:另一種部署方式
- 生成式 AI 應用:在網站裡接上 LLM API
重點整理:
- 路由分成頁面與 API 兩個 Blueprint,共用的邏輯放一起,每個檔案只做一件事
- 每個寫入都依序檢查:登入、資料存在、權限、內容,全部由伺服器判斷
- 身分來自 session,不來自請求內容;SQL 一律參數化;放進頁面的內容用 Jinja2 自動跳脫或
textContent - 手動試過的規則寫成測試,尤其是錯誤路徑
你已經走完後端開發軌:從一個 HTTP 請求開始,到一個有登入、資料庫、API、測試,也知道怎麼上線的網站。