
這章要學會什麼
- 寫一個共用的授權判斷服務,讓「列出工具」跟「呼叫工具」用同一套規則判斷會員能不能用某個工具。
- 在每一次呼叫工具時,都重新檢查通行證的權限範圍、會員狀態、使用權狀態與期限,而且說明為什麼這裡不能用快取結果。
- 用一個原子性的資料庫更新指令做出「額度預留」,實測 20 個同時發生的呼叫搶最後 10 次額度,剛好只有 10 個成功。
- 讓「從未獲准使用」的工具,跟「根本不存在」的工具,回應內容完全一模一樣。
- 加上「每分鐘呼叫次數上限」的保護,並且在撤銷使用權時,讓工具清單的快取立刻失效。
先備知識
要先完成第 10 章:/mcp 已經用正式通行證驗證,會員身分由對應表查出來。要記得第 7 章的使用權狀態(有效/已撤銷/已到期)跟後臺撤銷功能。
觀念白話講
為什麼每一次呼叫都要重新檢查
通行證簽發後 10 分鐘內都有效,AI 助理也可能把工具清單記在自己那邊。如果只在簽發通行證,或列工具的那一刻檢查一次,這段時間內發生的事平台完全擋不住:
| 這段時間可能發生 | 只檢查一次的後果 | 本章的做法 |
|---|---|---|
| 管理員撤銷使用權 | 舊通行證還能繼續用滿 10 分鐘 | 每次呼叫都重新查資料庫裡的使用權 |
| 會員被停權 | 已經發出的通行證照樣可以用 | 每次都重新檢查會員是否啟用 |
| 使用權到期 | 過了期限還能繼續呼叫 | 每次都重新比對有效期間 |
| 額度用完 | 可以超量呼叫 | 每次呼叫前都原子地預留一次額度 |
| 版本被下架 | 呼叫已經下架的服務 | 只找目前狀態是已發布的版本 |
通行證回答的是「你是誰、哪個 AI 助理」;能不能用某個工具,是平台資料庫此時此刻的即時狀態,每一次都要重新看一遍。
一次呼叫,要通過的檢查順序
其中①③④如果不通過,會被拒絕但不計入額度——因為根本沒有用到任何上游資源;而⑤跟⑥都通過之後才會真正扣一次額度,就算後面上游失敗了,這次額度也不會退還。
「從未獲准」要跟「不存在」回應一模一樣
如果會員從沒申請過某個服務,呼叫時系統回「你沒有權限使用這個工具」,等於告訴他這個工具確實存在,只是他不能用——攻擊者可以拿這個線索,一個一個猜工具名稱,逐步拼湊出平台上到底有哪些服務。所以「從未獲准」跟「這個工具根本不存在」對外必須顯示完全相同的協定層級錯誤訊息。差別只在平台自己的內部紀錄裡:「從未獲准」記的是明確的原因代碼,讓管理員事後查得出來「有人在嘗試猜測工具名稱」;如果是「曾經獲准、後來被撤銷」,則會直接把撤銷這個原因告訴呼叫者——因為那本來就是他自己知道的事,講清楚反而比較好處理。
額度:怎麼確保「兩個人不會同時搶到最後一次」
最直覺、但錯誤的寫法是:先讀出目前已用次數,如果小於額度就加一再存回去。兩個同時發生的請求可能都讀到「還剩最後 1 次」,兩邊都判斷「可以用」、都寫回去,結果額度就被突破了。正確做法是讓「比較」跟「加一」發生在同一個資料庫指令裡:資料庫在真正更新那一列的當下會自動鎖住它,第二個請求必須等第一個做完,這時候它看到的已經是新的數字,條件不成立、影響筆數是 0——程式只要看「這次真的更新到幾筆」就能判斷預留成功還是已經用完,不需要自己額外處理鎖定的細節。
清單可以快取,但「呼叫」永遠不能信任快取
「列出工具」這個動作容許使用短時間快取(AI 助理常常反覆問這件事,每次都查資料庫太浪費),核准或撤銷時會立刻清掉快取,所以正常操作下清單幾乎是即時的。但「呼叫工具」絕對不使用快取——萬一使用權是被別的途徑直接改掉的(例如直接改資料庫,沒有經過正常的撤銷功能),快取可能要等一段時間才會更新,這時候清單「晚一點才消失」只是顯示上的小問題,但「呼叫」這個動作必須在當下就被擋住,不能因為快取還沒過期就放行。額外還加上每位會員每分鐘的呼叫次數上限,防止失控的 AI 迴圈瞬間用光額度或拖垮上游服務。
老師示範做了什麼
示範情境:一位會員只看得到自己獲准的工具;猜測別的工具名稱得到跟不存在完全相同的回應;20 個同時發生的呼叫搶最後 10 次額度;撤銷使用權後,就算沒有換掉手上的通行證,也立刻被拒絕;停權、權限範圍不足、呼叫太密集也都會被擋下。
老師實測把某筆使用權的今日已用次數手動設成「只剩 10 次額度」,然後同時發送 20 個呼叫請求,結果剛好是10 個成功、10 個額度用完,一次都沒有多。接著示範撤銷使用權:手上的通行證完全沒有換過,撤銷後同一張通行證的「列出工具」立刻變成 0 個,呼叫也立刻得到「使用權已被撤銷」;停權該會員後同樣立刻被擋下;把通行證的權限範圍改掉,一樣立刻被拒絕;把每分鐘上限調成一個很小的數字,連續呼叫到第幾次就開始被拒絕,換一位不同的會員呼叫則完全不受影響,各自獨立計算。
自己動手的步驟
步驟一:共用的授權判斷服務
寫一個服務,讓「列出工具」跟「呼叫工具」都呼叫同一套規則:先確認通行證有正確的權限範圍、會員沒有被停權,再一定查資料庫、不查快取取得這個版本的使用權清單;完全沒有使用權就回傳「當作沒有這個工具」;有使用權但目前狀態不能用(已撤銷、已到期、尚未生效),回傳最新一筆的具體原因;都通過才進入額度預留這一步。
步驟二:原子的額度預留
用一個「更新且同時比較條件」的資料庫指令去嘗試預留:更新到 1 筆代表成功;沒更新到的話,先確認今天是不是已經有這一列資料,有的話代表已達上限;沒有的話代表今天第一次呼叫,新增一筆初始值。如果剛好兩個請求同時走到「新增第一筆」這一步,其中一個會撞到唯一性限制而失敗,這時候只要重新跑一次前面的更新指令即可,因為另一個請求已經把這一列建好了。
步驟三:處理常式與回應格式一致化
呼叫工具時,先問共用授權服務要不要放行;如果判定為「當作沒有這個工具」,紀錄照樣要寫,但回應要跟「協定層級找不到工具」完全相同;判定為放行才真正交給安全閘道呼叫上游,並且把用到的使用權編號一起記進紀錄裡;被拒絕的話,經過同一個地方寫紀錄,但不會真的送出請求。核准跟撤銷使用權的動作結束時,都要記得清除這位會員的工具清單快取,讓「列出工具」能立刻反映最新狀態。
步驟四:每分鐘呼叫次數上限
依「已經對應出來的會員編號」分別計算每分鐘的呼叫次數,超過上限直接回傳「請求過於頻繁」,不排隊、不等待——因為如果是失控的 AI 迴圈,應該讓它立刻知道被擋下,而不是越排越久。這個檢查要放在「已經驗證出使用者身分」之後才生效,否則所有沒登入的請求會被誤判成同一個匿名使用者、共用同一個額度。
常見錯誤
| 症狀 | 原因 | 怎麼處理 |
|---|---|---|
| 設定每分鐘上限為 5 次,測試腳本卻在第 5 次就被擋下 | 取得通行證的腳本本身最後也送出了一次「列出工具」,已經先用掉一次額度 | 這是正確行為:/mcp 的每一種請求(包含列出工具)都算 |
| 直接用資料庫工具把使用權改成已撤銷,工具清單卻沒有立刻消失 | 沒有經過正常的撤銷功能,快取沒有被清除,要等快取自然過期 | 一律用後臺撤銷功能;「呼叫」動作本來就不受快取影響,安全性不受影響 |
| 並行測試時全部得到「連不上上游」,而且額度已經被扣掉了 | 示範用的上游服務沒有啟動;額度預留發生在真正呼叫上游之前,上游失敗不會退還 | 啟動示範服務;這正是「上游失敗也計次」的既定規則 |
怎麼驗收+反向驗證
- 越權猜測工具名稱仍然無法呼叫:呼叫一個從未獲准的工具,跟呼叫一個根本不存在的工具,兩者回應完全相同;但呼叫紀錄裡能查到明確的原因代碼。
- 撤銷後使用舊通行證也會被拒絕:取得通行證後撤銷使用權,同一張通行證立刻被擋下,工具清單立刻變成 0 個。
- 兩個同時發生的呼叫不能突破最後一筆額度:把今天已用次數設成「額度減 1」,同時發送兩個呼叫,一個成功、一個「額度用完」;加碼測試多個同時發生的呼叫搶最後幾次額度,成功的數量剛好等於剩餘額度,一次都不多。
- 反向驗證:停權後被擋下;權限範圍不足被擋下;每分鐘上限測試中,第一次超過上限的請求得到「請求過於頻繁」。