Skip to content

Latest commit

 

History

History
340 lines (251 loc) · 13.8 KB

File metadata and controls

340 lines (251 loc) · 13.8 KB

API 認證架構說明

本文檔說明 CBDB 系統的 API 認證架構。系統使用 Laravel Sanctum 提供簡潔、安全的 API 認證。

認證方式概覽

CBDB API 支持兩種認證方式:

認證方式 使用場景 認證頭 適用對象
Session Cookie Web 前端 SPA 自動(Cookie) 瀏覽器用戶
Personal Access Token 外部應用/腳本 Authorization: Bearer {token} 開發者、研究人員

1. Session Cookie 認證(推薦用於 Web 前端)

工作原理

當用戶通過 Web 界面登錄後,Laravel 會建立一個 Session。Sanctum 利用這個 Session 來認證 API 請求。

優點

  • ✅ 無需手動管理 Token
  • ✅ 自動處理 CSRF 保護
  • ✅ 與 Laravel 原生認證完美集成
  • ✅ 適合單頁應用(SPA)

使用方式

  1. 用戶登錄

    // 使用標準的 Laravel Auth 登錄表單
    await axios.post('/login', {
      email: 'user@example.com',
      password: 'password'
    });
  2. 調用 API

    // 配置 Axios 以自動發送 Cookie
    window.axios.defaults.withCredentials = true;
    
    // 直接調用 API,Session 會自動處理認證
    const response = await axios.get('/api/select/search/addr');
  3. 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;
}

2. Personal Access Token 認證(用於外部應用)

使用場景

  • Python/R 數據分析腳本
  • 外部應用集成
  • 自動化工具
  • 移動應用

獲取 API Token

  1. 登錄 CBDB 系統
  2. 進入個人資料頁面:導航至 /profile
  3. 創建新 Token
    • 點擊「創建新 Token」
    • 輸入 Token 名稱(例如:「我的 Python 腳本」)
    • 選擇有效期限(可選)
    • 點擊創建
  4. 複製 Token:Token 只會顯示一次,請妥善保存

使用 Token 調用 API

Python 範例

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 範例

curl -X GET \
  'https://cbdb.example.com/api/select/search/addr?keyword=北京' \
  -H 'Authorization: Bearer your-personal-access-token-here' \
  -H 'Accept: application/json'

JavaScript 範例

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);
});

Token 管理

用戶可以在 /profile 頁面管理自己的 API Token:

  • 查看所有 Token:查看 Token 名稱、創建時間、最後使用時間、到期時間
  • 撤銷單個 Token:不再需要的 Token 可以隨時撤銷
  • 撤銷所有 Token:一次性撤銷所有 Token(安全考慮)

Token 安全最佳實踐

  1. 不要在代碼中硬編碼 Token

    # ❌ 不推薦
    API_TOKEN = 'your-token-here'
    
    # ✅ 推薦:使用環境變量
    import os
    API_TOKEN = os.getenv('CBDB_API_TOKEN')
  2. 定期輪換 Token

    • 定期創建新 Token 並撤銷舊 Token
    • 特別是在懷疑 Token 洩漏時立即撤銷
  3. 使用適當的有效期限

    • 短期任務使用 30-90 天
    • 長期服務可使用 1 年,但建議定期更新
  4. 不要分享 Token

    • 每個用戶/應用應有獨立的 Token
    • 不要通過不安全的渠道(如電子郵件、即時消息)傳輸 Token

需認證或可選認證的 API 端點

以下 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 端點createmutatedeletebatch_mutategetproposals/{id}/resubmitrelationship/opposite-edges):應用程式路由層未配置限流,不會由應用程式回 429(反向代理/WAF 仍可能)——節流責任在呼叫方,建議值與每批筆數見 API.md §1.3。
  • 未認證的表單端點(#1264):POST /register 30 次/分鐘、POST /password/email 5 次/分鐘、POST /password/reset 10 次/分鐘,都是每個來源 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.phppasswords.users.throttle,錯誤訊息 passwords.throttled)。換 IP 繞不過這一條,換 email 繞不過上面那條。
    • 並發:「檢查 + 計數」包在 cache lock 裡(file store 上是真的 flock),因為 FileStore::increment() 是沒上鎖的 read-modify-write——不處理的話並發請求會讀到同一個計數再各自寫回同一個較小值,計數可以被持續壓在上限以下=這道閘被大量繞過。拿不到鎖時一律視為超額(鎖名含 IP 與端點,會競爭的只有同一個來源的並發突發本身)。
    • 殘留限制file cache 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,只在失敗時累加)。
  • 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-AfterX-RateLimit-*(#1254)。實作在全域 middleware App\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。

錯誤處理

常見錯誤碼

狀態碼 錯誤 原因 解決方案
401 Unauthenticated Token 無效或過期 檢查 Token 是否正確,是否已過期
403 Forbidden 用戶無權限訪問 檢查用戶權限
419 CSRF Token Mismatch CSRF Token 無效(Session 認證) 刷新頁面或重新獲取 CSRF Token
429 Too Many Requests 超過速率限制 減少請求頻率,稍後重試

錯誤響應範例

{
  "message": "Unauthenticated."
}

技術實現細節

後端配置

Middleware (app/Http/Kernel.php)

'api' => [
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    'throttle:600,1',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],

API 路由 (routes/api.php)

Route::middleware('auth:sanctum')->get('/user', 'Api\UserController@show');

Route::group([
    'prefix' => 'select',
    'middleware' => ['auth.optional']
], function () {
    // 所有 select 和 search API 端點
});

Sanctum 配置 (config/sanctum.php)

'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 Passport 遷移到 Laravel Sanctum。如果您之前使用 Passport 的 Personal Access Token:

  1. 舊 Token 已失效:Passport Token 不再有效
  2. 創建新 Token:請在 /profile 頁面創建新的 Sanctum Token
  3. 更新應用配置:更新外部應用以使用新 Token

為什麼選擇 Sanctum?

  • 更簡單:Sanctum 專注於 SPA 和 Token 認證,無 OAuth 複雜性
  • 更輕量:不需要 OAuth2 服務器的開銷
  • 更適合我們的需求:CBDB 不需要 OAuth2 的授權流程
  • 更好的 SPA 支持:Session Cookie 認證與 Laravel 原生認證無縫集成

常見問題

Q: 我可以同時有多個 API Token 嗎?

A: 可以!您可以為不同的應用或腳本創建多個 Token,方便管理和撤銷。

Q: Token 會過期嗎?

A: 創建 Token 時可以設置有效期限(30 天、90 天、180 天、1 年),也可以創建永久有效的 Token。建議為安全考慮設置合理的有效期限。

Q: 如何撤銷洩漏的 Token?

A: 立即登錄系統,前往 /profile 頁面,找到對應的 Token 並點擊「撤銷」按鈕。如果不確定哪個 Token 洩漏,可以點擊「撤銷所有 Token」。

Q: Web 前端需要手動管理 Token 嗎?

A: 不需要!Web 前端使用 Session Cookie 認證,完全自動化。只需確保用戶已登錄即可。

Q: API 調用失敗,返回 401 錯誤?

A: 檢查以下幾點:

  1. Token 是否正確複製(沒有多餘的空格)
  2. Token 是否已過期
  3. Authorization 頭格式是否正確:Bearer {token}
  4. 對於 Session 認證,檢查用戶是否已登錄

相關資源