跳至主要内容

認證 API

概述

認證 API 提供用戶登入、Token 刷新、用戶資訊獲取和登出功能。採用 JWT Token 機制並支援帳號鎖定(Rate Limiting)。「記住我」是前端 Web Storage 政策,不屬於 API 契約。


端點列表

方法路徑說明權限
POST/api/v1/auth/login用戶登入公開
POST/api/v1/auth/refresh刷新 Access Token需要 Refresh Token
GET/api/v1/auth/me獲取當前用戶資訊需要認證
POST/api/v1/auth/logout用戶登出需要認證

端點詳細規格

POST /api/v1/auth/login

描述: 用戶登入,返回 Access Token 和 Refresh Token

請求 Body:

{
"username": "staff@florist.com",
"password": "Password123!"
}

欄位說明:

欄位類型必填說明
usernamestringYes用戶名(通常為 email)
passwordstringYes密碼

響應範例 (200 OK):

店員登入:

{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 900,
"user": {
"id": "user-001",
"email": "staff@florist.com",
"name": "王小明",
"role": "ROLE_SALES",
"roles": ["ROLE_SALES"],
"tenantId": "tenant-abc"
}
}

店主登入(角色繼承扁平化):

{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 900,
"user": {
"id": "user-002",
"email": "owner@florist.com",
"name": "林老闆",
"role": "ROLE_OWNER",
"roles": ["ROLE_OWNER", "ROLE_MANAGER", "ROLE_SALES", "ROLE_ACCOUNTANT", "ROLE_PURCHASER", "ROLE_FLORIST", "ROLE_DELIVERY"],
"tenantId": "tenant-abc"
}
}

錯誤響應:

狀態碼錯誤碼說明
401AUTH_INVALID_CREDENTIALS用戶名或密碼錯誤
423AUTH_ACCOUNT_LOCKED帳號已被鎖定(連續失敗 5 次)

錯誤響應範例 (401):

{
"message": "用戶名或密碼錯誤",
"error": "AUTH_INVALID_CREDENTIALS",
"path": "/api/v1/auth/login",
"timestamp": "2025-11-04T10:30:00Z"
}

業務規則:

  • 連續登入失敗 5 次後,帳號鎖定 15 分鐘
  • 密碼區分大小寫,用戶名不區分大小寫
  • Refresh Token 有效期固定為 7 天
  • Access Token 有效期固定 15 分鐘
  • 前端依「記住我」選擇 localStoragesessionStorage;不得將該偏好送入 API

POST /api/v1/auth/refresh

描述: 使用 Refresh Token 刷新 Access Token

請求 Body:

{
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}

響應範例 (200 OK):

{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 900
}

錯誤響應:

狀態碼錯誤碼說明
401AUTH_TOKEN_INVALIDRefresh Token 無效或過期

GET /api/v1/auth/me

描述: 獲取當前已登入用戶的詳細資訊

請求 Headers:

Authorization: Bearer {access_token}

響應範例 (200 OK):

{
"id": "user-001",
"email": "staff@florist.com",
"name": "王小明",
"role": "ROLE_SALES",
"roles": ["ROLE_SALES"],
"tenantId": "tenant-abc",
"createdAt": "2025-10-01T08:00:00Z"
}

欄位說明:

欄位類型說明
idstring用戶 ID
emailstring電子郵件
namestring用戶名稱
rolestring主要角色(ROLE_XXX 格式,第一個角色)
rolesstring[]扁平化角色陣列(包含繼承的角色,第一個為主要角色)
tenantIdstring租戶 ID
createdAtstring帳號建立時間(ISO 8601)

角色繼承說明:

後端 Spring Security 使用角色繼承機制,roles 欄位回傳扁平化的角色陣列:

主要角色roles 陣列
ROLE_ADMIN[ADMIN, OWNER, MANAGER, SALES, ACCOUNTANT, PURCHASER, FLORIST, DELIVERY]
ROLE_OWNER[OWNER, MANAGER, SALES, ACCOUNTANT, PURCHASER, FLORIST, DELIVERY]
ROLE_MANAGER[MANAGER, SALES, ACCOUNTANT]
ROLE_SALES[SALES]
ROLE_FLORIST[FLORIST]

錯誤響應:

狀態碼錯誤碼說明
401AUTH_TOKEN_MISSING缺少 Authorization Header
401AUTH_TOKEN_INVALIDToken 無效或過期

POST /api/v1/auth/logout

描述: 用戶登出(清除 Token)

請求 Headers:

Authorization: Bearer {access_token}

響應範例 (200 OK):

{
"message": "登出成功"
}

錯誤響應:

狀態碼錯誤碼說明
401AUTH_TOKEN_INVALIDToken 無效

備註:

  • 前端應在登出後清除 localStoragesessionStorage 中的 access/refresh token
  • 同時移除 auth_storage_type,避免下一次啟動讀取錯誤的儲存區

Token 管理

Access Token

  • 有效期: 15 分鐘
  • 儲存位置: Redux Store,並同步至登入模式選定的 Web Storage
  • 用途: 所有 API 請求的身份驗證
  • 格式: JWT (JSON Web Token)
  • 包含資訊:
Claim說明範例
sub用戶 ID"user-001"
email電子郵件"staff@florist.com"
tenantId租戶 ID"tenant-abc"
roles角色陣列(ROLE_XXX 格式)["ROLE_SALES"]
iat簽發時間(Unix timestamp)1702987200
exp過期時間(Unix timestamp)1702988100

Refresh Token

  • 有效期: 固定 7 天
  • 儲存位置: 由前端選擇 localStorage(記住我)或 sessionStorage(一般登入)
  • 用途: 刷新 Access Token
  • 安全性:
    • Reference implementation 使用 Web Storage,Token 可由 JavaScript 存取,必須搭配 CSP、 輸入輸出編碼及依賴更新降低 XSS 風險
    • 一次性使用(刷新後舊 Token 失效)

帳號鎖定機制 (Rate Limiting)

鎖定規則

  • 連續登入失敗 5 次,觸發帳號鎖定
  • 鎖定時間: 15 分鐘
  • 鎖定期間: 即使輸入正確密碼也無法登入
  • 鎖定解除: 15 分鐘後自動解除

實作細節

  • 失敗次數計數器: 儲存在用戶記錄中(loginFailedCount
  • 鎖定時間戳: lockedUntil (ISO 8601 格式)
  • 審計日誌: Console 記錄所有登入失敗與帳號鎖定事件

安全性考量

錯誤揭露政策

  • 參考實作預設最大揭露:用戶不存在與密碼錯誤回不同 typed error 與實際原因。
  • 應用若需要防枚舉的最小公開契約,設定 app.api.error-disclosure.mode=minimal

CSRF 防護

  • Token 不儲存在 Cookie
  • 使用 Custom Header (Authorization: Bearer) 而非 Cookie 認證

XSS 防護

  • Access/Refresh Token 儲存在 Web Storage,因此前端必須落實 CSP、避免不受信任 HTML, 並限制第三方 script

使用範例

1. 登入流程

// 1. 登入
const response = await fetch('/api/v1/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: 'staff@florist.com',
password: 'Password123!'
})
});

const { access_token, refresh_token, user } = await response.json();

// 2. 「記住我」只決定瀏覽器儲存位置,不送至 API
const storage = remember ? localStorage : sessionStorage;
storage.setItem('access_token', access_token);
storage.setItem('refresh_token', refresh_token);
localStorage.setItem('auth_storage_type', remember ? 'local' : 'session');

// 3. 用戶資訊保留在 Redux Store
dispatch(setCurrentUser(user));

2. 攜帶 Token 訪問受保護 API

const storage = localStorage.getItem('auth_storage_type') === 'session'
? sessionStorage
: localStorage;

const response = await fetch('/api/v1/orders', {
headers: {
'Authorization': `Bearer ${storage.getItem('access_token')}`
}
});

3. 刷新 Token

// Access Token 過期時(收到 401 錯誤)
const storage = localStorage.getItem('auth_storage_type') === 'session'
? sessionStorage
: localStorage;

const response = await fetch('/api/v1/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refresh_token: storage.getItem('refresh_token')
})
});

const { access_token } = await response.json();
storage.setItem('access_token', access_token);

// 重試原請求

4. 登出

const storage = localStorage.getItem('auth_storage_type') === 'session'
? sessionStorage
: localStorage;

await fetch('/api/v1/auth/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${storage.getItem('access_token')}`
}
});

// 清除本地儲存
for (const target of [localStorage, sessionStorage]) {
target.removeItem('access_token');
target.removeItem('refresh_token');
}
localStorage.removeItem('auth_storage_type');

// 重定向至登入頁
window.location.href = '/login';

相關 User Stories


最後更新: 2025-12-20