Scenario 3: Mock 認證與後端權限檢查
User Story: US-002: 角色權限控制
概述
本場景說明前端權限架構的設計原則:
- Mock 只做認證:驗證 token 有效性,不做權限檢查
- 後端負責權限:Spring Security 在正式環境處理 API 權限控制
- 前端處理 403:收到 403 響應時顯示錯誤訊息
Scenario A: Mock 環境(開發/測試)
Given: 已登入花藝設計師
{
"userId": "designer-001",
"email": "designer@florist.com",
"name": "李花藝",
"roles": ["ROLE_FLORIST"],
"tenantId": "tenant-abc",
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
When: 花藝設計師發送 API 請求
POST /api/v1/orders HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{
"customerId": "cust-123",
"items": [{"productId": "prod-001", "quantity": 2, "price": 1200}],
"deliveryAddress": "台北市信義區信義路五段 7 號",
"deliveryPhone": "0912-345-678",
"deliveryDate": "2025-11-01",
"deliveryTimeSlot": "14:00-18:00"
}
Then: Mock 只檢查認證,不檢查權限
Mock 中間件行為 (src/mocks/middleware/auth-middleware.ts):
export function checkAuth(request: Request): AuthCheckResult {
// 1. 檢查 Authorization header
const token = extractToken(request);
if (!token) {
return { success: false, error: HttpResponse.json({ error: 'UNAUTHORIZED' }, { status: 401 }) };
}
// 2. 驗證 token 有效性
const payload = verifyToken(token);
if (!payload) {
return { success: false, error: HttpResponse.json({ error: 'INVALID_TOKEN' }, { status: 401 }) };
}
// 3. 不做任何權限檢查 - 直接返回成功
return { success: true, userId: payload.userId, tenantId: payload.tenantId };
}
結果:請求成功,返回 201 Created
{
"id": "order-new-001",
"status": "PENDING_CONFIRMATION",
"createdAt": "2025-10-31T10:30:00Z"
}
說明:在 Mock 環境中,只要 token 有效,所有 API 請求都會成功。這簡化了開發與測試流程。
Scenario B: 正式環境(Spring Security)
Given: 相同的花藝設計師
{
"userId": "designer-001",
"roles": ["ROLE_FLORIST"],
"tenantId": "tenant-abc"
}
When: 花藝設計師發送相同的 API 請求
Then: 後端 Spring Security 檢查權限並拒絕
Spring Security 配置:
@PreAuthorize("hasRole('STAFF') or hasRole('SHOP_OWNER') or hasRole('SYSTEM_ADMIN')")
@PostMapping("/api/v1/orders")
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
// ...
}
響應 (403 Forbidden):
{
"error": "FORBIDDEN",
"message": "您沒有權限執行此操作",
"requiredRole": "ROLE_STAFF",
"currentRole": "ROLE_FLORIST",
"timestamp": "2025-10-31T10:30:00Z"
}
審計日誌:
{
"id": "log-001",
"tenantId": "tenant-abc",
"userId": "designer-001",
"userRole": "ROLE_FLORIST",
"action": "ORDER_CREATE_DENIED",
"resourceType": "Order",
"status": "FAILED",
"details": {
"reason": "INSUFFICIENT_PERMISSIONS",
"requiredRole": "ROLE_STAFF"
},
"timestamp": "2025-10-31T10:30:00Z"
}
前端處理 403 響應
API 攔截器行為:
// src/services/apiClient.ts
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 403) {
showToast('您沒有權限執行此操作', 'error');
}
return Promise.reject(error);
}
);
用戶體驗:
- Toast 通知顯示「您沒有權限執行此操作」(紅色,5 秒)
- 不創建任何訂單
- 停留在當前頁面
Scenario C: Token 過期自動刷新
When: Access Token 即將過期
Token 狀態:
exp: 1714570900(剩餘 2 分鐘)iat: 1714570000
API 請求(任意受保護端點):
GET /api/v1/orders HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Then: 自動刷新 Token
前端攔截器行為:
-
偵測到 Token 即將過期(< 5 分鐘)
-
自動發送刷新請求:
POST /api/v1/auth/refresh HTTP/1.1
Cookie: refreshToken=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... -
收到新 Access Token:
{
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...[NEW]",
"expiresIn": 900
} -
使用新 Token 重試原始請求
-
用戶感覺不到任何中斷
設計原則總結
| 環境 | 認證 | 權限 | 說明 |
|---|---|---|---|
| Mock | checkAuth | 無 | 只驗證 token,不檢查權限 |
| 正式 | Spring Security | Spring Security | 完整的認證與授權 |
為何 Mock 不做權限檢查?
- Mock 是開發與測試環境
- 簡化開發流程,減少阻礙
- 真正的權限控制由後端負責
- 前端只需處理 403 響應
最後更新: 2025-12-20