上一篇我們把不合法的資料擋在外面了。
但是仔細想想,現在的 API 還有一個更根本的問題:
任何人只要知道網址,就可以對我們的資料做任何事。
隨便一個人打:
DELETE /tasks/1
資料就沒了,而且我們完全不知道是誰刪的。
今天就來認識 API 安全最基本的兩個概念。
這兩個字長得很像,但問的是不同的問題:
Authentication(驗證身分)
問的是:你是誰?
Authorization(授權)
問的是:你可以做什麼?
用進公司來想像:
刷卡進大門 → 證明「我是這間公司的人」= Authentication
卡片只能開 3 樓的門 → 決定「我可以去哪裡」= Authorization
順序是先驗證身分,再看權限。
如果沒有先知道你是誰,就沒辦法判斷你可以做什麼。
再把 Day 25 的 Validation 一起放進來比較,三個剛好是不同層次的問題:
Validation → 你傳的東西格式對不對?
Authentication → 你是誰?
Authorization → 你有沒有資格做這件事?
三個都要做,少一個都不行。
資料格式檢查得再完整,沒有身分驗證的話,任何人還是可以把資料刪光。
先簡單認識幾種,知道有這些東西就好:
API Key
每個使用者(或每個程式)拿到一把金鑰,每次請求都帶上。
最簡單,常用在「程式對程式」的串接。
Session + Cookie
登入之後,伺服器記住這個人,瀏覽器用 Cookie 帶著 session id。
傳統網站很常見。
JWT(JSON Web Token)
登入後拿到一段 token,之後每次請求都帶著它。
前後端分離的專案很常用。
OAuth
像是「使用 Google 帳號登入」那種,把驗證交給第三方。
這次因為是入門,我們用最單純的 API Key 來實作,重點是先體會「有沒有身分驗證」的差別。
目標是:
沒有帶金鑰的請求,一律不能新增、修改、刪除。
步驟 1:準備金鑰
先在程式最上面多匯入兩個東西:
import os
from functools import wraps
然後準備金鑰:
API_KEYS = {
os.environ.get('USER_API_KEY', 'user-key-12345'): 'user',
os.environ.get('ADMIN_API_KEY', 'admin-key-67890'): 'admin',
}
這裡用 os.environ.get() 從環境變數讀,讀不到才用預設值。
為什麼不直接寫死在程式裡?
因為程式通常會進版控(Git),金鑰寫在裡面就等於公開了。
正式的做法會放在環境變數或專門的設定管理工具,程式裡只留一個「讀取」的動作。
(我們現在寫預設值只是為了方便練習,真的要上線的話不能這樣。)
步驟 2:讓 Swagger 知道有這個機制
Flask-RESTX 可以在建立 Api 的時候設定:
authorizations = {
'apikey': {
'type': 'apiKey',
'in': 'header',
'name': 'X-API-KEY',
}
}
api = Api(
app,
title='Task API',
version='1.0',
description='30 天鐵人賽練習用的作業管理 API',
authorizations=authorizations,
)
這段的意思是:
我們的 API 會用一個叫做 X-API-KEY 的 Header 來驗證身分。
加上去之後,Swagger 首頁右上角會多出一顆 Authorize 按鈕,
而且需要金鑰的 API 右邊會出現一個小鎖頭。

右上角的 Authorize 按鈕,POST、PUT、DELETE 右邊都有鎖頭,GET 沒有
步驟 3:寫一個檢查金鑰的 decorator
前面幾天我們一直在用 @api.route、@api.expect 這些 decorator,
現在換我們自己寫一個。
def require_api_key(role=None):
"""檢查 X-API-KEY,role 可以指定需要的身分"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
key = request.headers.get('X-API-KEY')
# Authentication:你是誰?
if not key or key not in API_KEYS:
api.abort(401, "請提供有效的 X-API-KEY")
# Authorization:你可以做這件事嗎?
if role and API_KEYS[key] != role:
api.abort(403, "你沒有權限執行這個操作")
return func(*args, **kwargs)
return wrapper
return decorator
幾個重點:
request.headers.get('X-API-KEY')
從 Request 的 Header 取出金鑰。
Header 是 Request 裡面放「附加資訊」的地方,跟 Body 不一樣。
@wraps(func)
這行不能少。
它會把原本函式的名字和 docstring 複製到 wrapper 上。
少了它,Flask-RESTX 在產生 Swagger 文件的時候讀不到 docstring,
那幾個 API 的說明文字會整個消失(變成空白,不是變成別的東西)。
401 和 403 的差別
401 Unauthorized → 我不知道你是誰(沒帶金鑰、金鑰無效)
403 Forbidden → 我知道你是誰,但你不能做這件事
這是很多人會搞混的地方。
簡單記:401 是「你還沒證明身分」,403 是「你證明了,但不行」。
@api.route('/tasks')
class TaskList(Resource):
@api.marshal_list_with(task_response)
def get(self):
"""取得所有作業"""
...
@api.doc(security='apikey')
@api.expect(task_input, validate=True)
@api.marshal_with(task_response, code=201)
@api.response(400, '輸入資料有問題')
@api.response(401, '沒有提供有效的金鑰')
@require_api_key()
def post(self):
"""新增一筆作業"""
...
@api.route('/tasks/<int:task_id>')
class TaskDetail(Resource):
@api.doc(security='apikey')
@api.response(204, '刪除成功')
@api.response(403, '需要管理員權限')
@require_api_key(role='admin')
def delete(self, task_id):
"""刪除指定編號的作業(只有管理員可以)"""
...
這裡的設計是:
GET → 誰都可以看
POST、PUT → 要有金鑰(不管是 user 還是 admin)
DELETE → 只有 admin 可以
@api.doc(security='apikey') 是讓 Swagger 在這個 API 上顯示那個小鎖頭。
重新啟動,打開 Swagger。
測試 1:沒帶金鑰就新增
先不要按 Authorize,直接用 POST /tasks 送資料:
Code:401
{
"message": "請提供有效的 X-API-KEY"
}
沒有金鑰時新增失敗,回傳 401

測試 2:帶一把錯的金鑰
如果金鑰亂打(例如 wrong-key),結果一樣是 401。
因為對 API 來說,「沒有金鑰」和「金鑰不對」都是同一件事:我不認識你。
測試 3:帶上正確的金鑰
按右上角的 Authorize,輸入:
user-key-12345
按 Authorize → Close

在 Authorize 視窗輸入金鑰
關掉視窗之後會發現,剛才那些小鎖頭從「開著」變成「鎖上」了,代表 Swagger 之後會自動幫我們帶上這個 Header。
再送一次 POST /tasks:
這次成功了,回傳 201。

帶金鑰之後新增成功
測試 4:用一般使用者刪除
現在用 user-key-12345 去打 DELETE /tasks/{id}:
Code:403
{
"message": "你沒有權限執行這個操作"
}
一般使用者刪除時回傳 403

注意 Curl 那一區會看到 Swagger 確實有幫我們帶上:
所以這次不是「沒帶金鑰」,而是「帶了但身分不夠」—— 剛好就是 401 和 403 的差別。
測試 5:換成管理員
重新按 Authorize(如果已經登入過,會看到 Logout 按鈕,先登出再輸入新的),改成:
admin-key-67890
再刪一次,這次就成功了,Code 是 204。

管理員刪除成功
測試 6:GET 不受影響
最後確認一下,GET /tasks 不帶任何金鑰還是可以正常查詢(200)。
因為我們沒有在 get() 上面加 @require_api_key()。
整理一下這六個測試:
GET 不帶金鑰 → 200
POST 不帶金鑰 → 401
POST 錯誤金鑰 → 401
POST user 金鑰 → 201
DELETE user 金鑰 → 403
DELETE admin 金鑰 → 204
這樣就把 Authentication 和 Authorization 的差別實際跑過一次了:
401 → 你是誰我不知道
403 → 我知道你是誰,但你不行
API Key 只是入門,實際上還有很多,先知道有這些:
一定要用 HTTPS
如果是 HTTP,金鑰在網路上是「明文」傳送的,被攔截就沒意義了。
密碼絕對不能明文存進資料庫
如果之後要做使用者註冊登入,密碼要用雜湊(hash)存。
Python 可以用 werkzeug.security 的 generate_password_hash 和 check_password_hash。
敏感資訊不要放在網址上
像 /login?password=1234 這種寫法,密碼會被記錄在瀏覽器歷史和伺服器 log 裡。
所以 Day 13 才會說,要傳的資料放在 Request Body 比較好。
金鑰和密碼不要進版控
記得加 .gitignore,設定用環境變數。
錯誤訊息不要洩漏太多
例如登入失敗時,回「帳號或密碼錯誤」就好,不要回「這個帳號不存在」,
不然等於幫攻擊者確認哪些帳號是存在的。
不要自己發明驗證機制
如果要做完整的登入,建議用成熟的套件(例如 Flask-JWT-Extended),
自己手寫加密或 token 很容易出錯。
Authentication(你是誰)和 Authorization(你可以做什麼)的差別
用 API Key 實作最基本的身分驗證
401 和 403 的差別
自己寫 decorator,記得加 @wraps
用 Flask-RESTX 的 authorizations 讓 Swagger 也能測試
金鑰要放環境變數、要用 HTTPS、密碼要雜湊
不過安全這件事,不是只有「誰可以進來」而已。
就算是有權限的使用者,也可能在輸入的資料裡動手腳。
下一篇就來看一個很經典、而且到現在都還一直在發生的攻擊方式:SQL Injection。