跳至主要内容

認證 API

概述

認證 API 提供用戶登入、Token 刷新、用戶資訊獲取和登出功能。採用 JWT Token 機制,支援帳號鎖定(Rate Limiting)和「記住我」功能。


端點列表

方法路徑說明權限
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!",
"remember": false
}

欄位說明:

欄位類型必填說明
usernamestringYes用戶名(通常為 email)
passwordstringYes密碼
rememberbooleanNo是否記住我(預設 false)

響應範例 (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 分鐘
  • 密碼區分大小寫,用戶名不區分大小寫
  • remember=true 時,Refresh Token 有效期為 30 天(否則 7 天)
  • Access Token 有效期固定 15 分鐘

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 無效

備註:

  • 前端應在登出後清除 localStorage 中的 access_token
  • Refresh Token 應從 Cookie 中清除(如使用 HttpOnly Cookie)

Token 管理

Access Token

  • 有效期: 15 分鐘
  • 儲存位置: Memory (Redux Store) 或 localStorage
  • 用途: 所有 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 天
    • 記住我: 30 天
  • 儲存位置: HttpOnly Cookie(防止 XSS 攻擊)或 localStorage
  • 用途: 刷新 Access Token
  • 安全性:
    • 無法透過 JavaScript 訪問(如使用 HttpOnly Cookie)
    • 一次性使用(刷新後舊 Token 失效)

帳號鎖定機制 (Rate Limiting)

鎖定規則

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

實作細節

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

安全性考量

防止用戶枚舉攻擊

  • 錯誤訊息統一: 用戶不存在和密碼錯誤返回相同訊息「用戶名或密碼錯誤」
  • 避免洩漏: 不透露帳號是否存在

CSRF 防護

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

XSS 防護

  • Refresh Token 使用 HttpOnly Cookie(可選)
  • Access Token 儲存在 Memory 或 localStorage(需前端防護)

使用範例

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!',
remember: false
})
});

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

// 2. 儲存 Token
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', refresh_token);

// 3. 儲存用戶資訊
localStorage.setItem('user', JSON.stringify(user));

2. 攜帶 Token 訪問受保護 API

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

3. 刷新 Token

// Access Token 過期時(收到 401 錯誤)
const response = await fetch('/api/v1/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refresh_token: localStorage.getItem('refresh_token')
})
});

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

// 重試原請求

4. 登出

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

// 清除本地儲存
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user');

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

相關 User Stories


最後更新: 2025-12-20