跳至主要内容

Scenario 3: Mock 認證與後端權限檢查

User Story: US-002: 角色權限控制

概述

本場景說明前端權限架構的設計原則:

  1. Mock 只做認證:驗證 token 有效性,不做權限檢查
  2. 後端負責權限:Spring Security 在正式環境處理 API 權限控制
  3. 前端處理 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);
}
);

用戶體驗:

  1. Toast 通知顯示「您沒有權限執行此操作」(紅色,5 秒)
  2. 不創建任何訂單
  3. 停留在當前頁面

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

前端攔截器行為:

  1. 偵測到 Token 即將過期(< 5 分鐘)

  2. 自動發送刷新請求:

    POST /api/v1/auth/refresh HTTP/1.1
    Cookie: refreshToken=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
  3. 收到新 Access Token:

    {
    "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...[NEW]",
    "expiresIn": 900
    }
  4. 使用新 Token 重試原始請求

  5. 用戶感覺不到任何中斷


設計原則總結

環境認證權限說明
MockcheckAuth只驗證 token,不檢查權限
正式Spring SecuritySpring Security完整的認證與授權

為何 Mock 不做權限檢查?

  • Mock 是開發與測試環境
  • 簡化開發流程,減少阻礙
  • 真正的權限控制由後端負責
  • 前端只需處理 403 響應

最後更新: 2025-12-20