本文檔說明 CBDB 系統的 API 認證架構。系統使用 Laravel Sanctum 提供簡潔、安全的 API 認證。
CBDB API 支持兩種認證方式:
| 認證方式 | 使用場景 | 認證頭 | 適用對象 |
|---|---|---|---|
| Session Cookie | Web 前端 SPA | 自動(Cookie) | 瀏覽器用戶 |
| Personal Access Token | 外部應用/腳本 | Authorization: Bearer {token} |
開發者、研究人員 |
當用戶通過 Web 界面登錄後,Laravel 會建立一個 Session。Sanctum 利用這個 Session 來認證 API 請求。
- ✅ 無需手動管理 Token
- ✅ 自動處理 CSRF 保護
- ✅ 與 Laravel 原生認證完美集成
- ✅ 適合單頁應用(SPA)
-
用戶登錄
// 使用標準的 Laravel Auth 登錄表單 await axios.post('/login', { email: 'user@example.com', password: 'password' });
-
調用 API
// 配置 Axios 以自動發送 Cookie window.axios.defaults.withCredentials = true; // 直接調用 API,Session 會自動處理認證 const response = await axios.get('/api/select/search/addr');
-
CSRF 保護
確保所有 POST/PUT/DELETE 請求包含 CSRF Token:
<meta name="csrf-token" content="{{ csrf_token() }}">
const token = document.head.querySelector('meta[name="csrf-token"]'); axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
在 resources/js/app.js 中已配置:
// 啟用 Cookie 認證
window.axios.defaults.withCredentials = true;
// 自動附加 CSRF Token
const token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
}- Python/R 數據分析腳本
- 外部應用集成
- 自動化工具
- 移動應用
- 登錄 CBDB 系統
- 進入個人資料頁面:導航至
/profile - 創建新 Token:
- 點擊「創建新 Token」
- 輸入 Token 名稱(例如:「我的 Python 腳本」)
- 選擇有效期限(可選)
- 點擊創建
- 複製 Token:Token 只會顯示一次,請妥善保存
import requests
# 配置 API Token
API_BASE_URL = 'https://cbdb.example.com/api'
API_TOKEN = 'your-personal-access-token-here'
headers = {
'Authorization': f'Bearer {API_TOKEN}',
'Accept': 'application/json'
}
# 調用 API
response = requests.get(
f'{API_BASE_URL}/select/search/addr',
headers=headers,
params={'keyword': '北京'}
)
data = response.json()
print(data)curl -X GET \
'https://cbdb.example.com/api/select/search/addr?keyword=北京' \
-H 'Authorization: Bearer your-personal-access-token-here' \
-H 'Accept: application/json'const axios = require('axios');
const API_BASE_URL = 'https://cbdb.example.com/api';
const API_TOKEN = 'your-personal-access-token-here';
axios.get(`${API_BASE_URL}/select/search/addr`, {
headers: {
'Authorization': `Bearer ${API_TOKEN}`,
'Accept': 'application/json'
},
params: {
keyword: '北京'
}
})
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error('API Error:', error.response?.data || error.message);
});用戶可以在 /profile 頁面管理自己的 API Token:
- 查看所有 Token:查看 Token 名稱、創建時間、最後使用時間、到期時間
- 撤銷單個 Token:不再需要的 Token 可以隨時撤銷
- 撤銷所有 Token:一次性撤銷所有 Token(安全考慮)
-
不要在代碼中硬編碼 Token
# ❌ 不推薦 API_TOKEN = 'your-token-here' # ✅ 推薦:使用環境變量 import os API_TOKEN = os.getenv('CBDB_API_TOKEN')
-
定期輪換 Token
- 定期創建新 Token 並撤銷舊 Token
- 特別是在懷疑 Token 洩漏時立即撤銷
-
使用適當的有效期限
- 短期任務使用 30-90 天
- 長期服務可使用 1 年,但建議定期更新
-
不要分享 Token
- 每個用戶/應用應有獨立的 Token
- 不要通過不安全的渠道(如電子郵件、即時消息)傳輸 Token
以下 API 端點需要認證:
| 端點 | 方法 | 描述 |
|---|---|---|
/api/user |
GET | 獲取當前用戶信息(需認證) |
/api/select/search/* |
GET | 所有選擇和搜索 API(可選認證) |
為了保護系統穩定性,api 群組的端點保留速率限制:
api群組(/api/v2/persons、/api/v2/operations、/api/v2/texts、/api/select/*、舊版/api/...等,/api/mcp除外):600 請求/分鐘,超過會返回429 Too Many Requests。/api/mcp:雖然也在api群組,但已排除上面那條 600,改用專屬額度(預設 120 請求/分鐘),兩者互不排擠。web群組的/api/v2端點(create/mutate/delete/batch_mutate/get/proposals/{id}/resubmit/relationship/opposite-edges):應用程式路由層未配置限流,不會由應用程式回 429(反向代理/WAF 仍可能)——節流責任在呼叫方,建議值與每批筆數見 API.md §1.3。- 未認證的表單端點(#1264):
POST /register30 次/分鐘、POST /password/email5 次/分鐘、POST /password/reset10 次/分鐘,都是每個來源 IP 各自一份額度(AUTH_THROTTLE_*_PER_MINUTE可調,實作在App\Http\Middleware\ThrottleGuestAuthRequests)。三條端點各有獨立的桶,互不排擠。- 超額回應依客戶端而定:瀏覽器/Inertia 表單得到 302 +
email欄位的錯誤訊息(與登入被鎖定時同一種形狀);JSON 客戶端得到429+Retry-After。刻意不對表單回 HTML 429——Inertia 收到非 Inertia 回應會彈全螢幕錯誤 modal,使用者看不到任何有意義的訊息。 POST /password/email另有一道按 email 的節流:同一個 email 60 秒內不會重發重設信(config/auth.php的passwords.users.throttle,錯誤訊息passwords.throttled)。換 IP 繞不過這一條,換 email 繞不過上面那條。- 並發:「檢查 + 計數」包在 cache lock 裡(file store 上是真的 flock),因為
FileStore::increment()是沒上鎖的 read-modify-write——不處理的話並發請求會讀到同一個計數再各自寫回同一個較小值,計數可以被持續壓在上限以下=這道閘被大量繞過。拿不到鎖時一律視為超額(鎖名含 IP 與端點,會競爭的只有同一個來源的並發突發本身)。 - 殘留限制:
filecache store 是每個節點一份目錄,多節點部署時每個節點各有自己的桶(要跨節點共享得改用 redis store,config/cache.php已備好但未啟用);passwords.users.throttle的 per-email 節流在框架內部仍不是原子的,不過上游已有這道 per-IP 閘壓住並發量。 - 部署條件:這道閘按
$request->ip()分桶,而專案沒有 TrustProxies。若前置 CDN/LB,必須同時設定 TrustProxies,否則所有使用者會塌進同一個桶(/password/email只有 5 次/分鐘=全站無法重設密碼)。應用層偵測到X-Forwarded-For而沒有受信任代理時會記一行Log::warning,讓這個設定漏洞不至於靜默存在。 POST /login不在此列——它本來就有ThrottlesLogins的 5 次/分鐘(key 為 email+IP,只在失敗時累加)。
- 超額回應依客戶端而定:瀏覽器/Inertia 表單得到 302 +
GET|POST /api/operations/token(眾包舊通道拿密碼換長期 token,#1264):每個 email+IP 5 次/分鐘、每個 IP 20 次/分鐘(AUTH_THROTTLE_CROWDSOURCING_TOKEN_*可調),超過回 429。成功的請求也計數,所以客戶端要「取一次 token 後快取重用」,不要每寫一筆就重取(token 長期有效)。與ThrottleGuestAuthRequests同樣按$request->ip()分桶,因此前置 CDN/LB 時必須設定 TrustProxies,否則 per-IP 那道會塌成全站一個桶。兩個維度都要:per-email 擋「針對某帳號猜密碼」,per-IP 擋「換帳號繼續猜」。同一次改動也把三條失敗路徑從 200 改成 401/403——原本回 200 會讓「把 200 的 body 當 token 用」的客戶端拿錯誤訊息當憑證。POST /api/v1/user/login已下架(回 410,不再驗證任何密碼):它從來不可能成功(轉發到早已不存在的oauth/token),失敗路徑還會 500,而且是一條沒有節流的密碼驗證端點。- 帶 Bearer token 但認證失敗的請求:不分端點,每個來源 IP 每分鐘 60 次(
FAILED_AUTH_THROTTLE_PER_MINUTE可調),超過後在認證之前就回 429 並帶Retry-After、X-RateLimit-*(#1254)。實作在全域 middlewareApp\Http\Middleware\ThrottleFailedAuthentication;它必須留在Kernel::$middleware(全域)才會跑在auth之前,因為框架的$middlewarePriority把認證排在限流之前。- 範圍刻意收窄到「帶 Bearer token 的認證嘗試」:累加與擋下都要求請求帶了
Authorization: Bearer …。沒帶憑證的請求(未登入的瀏覽器、公開端點、MCP 規範要求的未認證握手、session 過期的站內 XHR)既不累加也不會被擋——所以被擋期間該 IP 仍然可以開登入頁、可以登入。認證成功的請求也不累加。 - 「認證失敗」= 401,或「帶著 Bearer token 卻被導向登入頁」(
Accept不是 JSON 時未認證會 302 而不是 401,那是同一件事的另一種形狀)。403/404/422/419 都不算。 - 代價:額度是 per-IP,所以同一個出口 IP 後面壞掉的客戶端會連帶擋住其他 Bearer 客戶端(要判斷 token 有效與否就得先查一次資料庫,而那正是要封頂的成本)。
- 對客戶端的意義:token 失效時不要無限重試。連續失敗就停下來換 token,否則會從 401 變成 429。
- 範圍刻意收窄到「帶 Bearer token 的認證嘗試」:累加與擋下都要求請求帶了
| 狀態碼 | 錯誤 | 原因 | 解決方案 |
|---|---|---|---|
| 401 | Unauthenticated | Token 無效或過期 | 檢查 Token 是否正確,是否已過期 |
| 403 | Forbidden | 用戶無權限訪問 | 檢查用戶權限 |
| 419 | CSRF Token Mismatch | CSRF Token 無效(Session 認證) | 刷新頁面或重新獲取 CSRF Token |
| 429 | Too Many Requests | 超過速率限制 | 減少請求頻率,稍後重試 |
{
"message": "Unauthenticated."
}'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:600,1',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],Route::middleware('auth:sanctum')->get('/user', 'Api\UserController@show');
Route::group([
'prefix' => 'select',
'middleware' => ['auth.optional']
], function () {
// 所有 select 和 search API 端點
});'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
))),Sanctum 使用 personal_access_tokens 表來存儲 API Token:
CREATE TABLE personal_access_tokens (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tokenable_type VARCHAR(255),
tokenable_id BIGINT,
name VARCHAR(255),
token VARCHAR(64) UNIQUE,
abilities TEXT,
last_used_at TIMESTAMP NULL,
expires_at TIMESTAMP NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP
);本系統已從 Laravel Passport 遷移到 Laravel Sanctum。如果您之前使用 Passport 的 Personal Access Token:
- 舊 Token 已失效:Passport Token 不再有效
- 創建新 Token:請在
/profile頁面創建新的 Sanctum Token - 更新應用配置:更新外部應用以使用新 Token
- ✅ 更簡單:Sanctum 專注於 SPA 和 Token 認證,無 OAuth 複雜性
- ✅ 更輕量:不需要 OAuth2 服務器的開銷
- ✅ 更適合我們的需求:CBDB 不需要 OAuth2 的授權流程
- ✅ 更好的 SPA 支持:Session Cookie 認證與 Laravel 原生認證無縫集成
A: 可以!您可以為不同的應用或腳本創建多個 Token,方便管理和撤銷。
A: 創建 Token 時可以設置有效期限(30 天、90 天、180 天、1 年),也可以創建永久有效的 Token。建議為安全考慮設置合理的有效期限。
A: 立即登錄系統,前往 /profile 頁面,找到對應的 Token 並點擊「撤銷」按鈕。如果不確定哪個 Token 洩漏,可以點擊「撤銷所有 Token」。
A: 不需要!Web 前端使用 Session Cookie 認證,完全自動化。只需確保用戶已登錄即可。
A: 檢查以下幾點:
- Token 是否正確複製(沒有多餘的空格)
- Token 是否已過期
- Authorization 頭格式是否正確:
Bearer {token} - 對於 Session 認證,檢查用戶是否已登錄