技術 7 分鐘閱讀

API 安全性最佳實踐:保護你的應用免受攻擊

API 安全性最佳實踐:保護你的應用免受攻擊

前言

API 安全性是很多開發者容易忽略的環節,特別是在接案時趕時程的情況下。但安全漏洞的代價可能是毀滅性的:資料外洩、客戶損失、甚至法律責任。這篇文章整理了最重要的 API 安全實踐。

認證與授權

使用安全的認證機制

// Laravel Sanctum Token 認證
// 不要把 Token 放在 URL 中
// 不好:GET /api/users?token=abc123
// 好:使用 Authorization Header

// 前端
fetch('/api/users', {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Accept': 'application/json',
  },
});

Token 安全性

// 設定 Token 過期時間
// config/sanctum.php
'expiration' => 60 * 24, // 24 小時

// 限制 Token 權限
$token = $user->createToken('api', ['read:products', 'write:orders']);

// 在 Controller 中檢查權限
if (!$request->user()->tokenCan('write:orders')) {
    abort(403, '沒有權限');
}

實作 RBAC(角色權限控制)

// 使用 Middleware 檢查角色
Route::middleware(['auth:sanctum', 'role:admin'])->group(function () {
    Route::delete('/users/{user}', [UserController::class, 'destroy']);
});

// Role Middleware
class RoleMiddleware
{
    public function handle(Request $request, Closure $next, string $role): Response
    {
        if ($request->user()->role !== $role) {
            return response()->json([
                'message' => '權限不足',
            ], 403);
        }

        return $next($request);
    }
}

輸入驗證與清理

永遠不要信任使用者輸入

// Laravel Form Request 驗證
class StoreProductRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'price' => ['required', 'numeric', 'min:0', 'max:999999'],
            'description' => ['nullable', 'string', 'max:5000'],
            'category_id' => ['required', 'integer', 'exists:categories,id'],
            'tags' => ['nullable', 'array', 'max:10'],
            'tags.*' => ['string', 'max:50'],
        ];
    }
}

SQL Injection 防護

// 不好:直接拼接 SQL
$users = DB::select("SELECT * FROM users WHERE name = '$name'");

// 好:使用參數綁定
$users = DB::select('SELECT * FROM users WHERE name = ?', [$name]);

// 最好:使用 Eloquent ORM
$users = User::where('name', $name)->get();

Laravel 的 Eloquent 和 Query Builder 內建參數綁定,只要正確使用就能防止 SQL Injection。但如果你寫 Raw Query,一定要用參數綁定。

XSS 防護

// Blade 模板預設會跳脫 HTML
{{ $user->name }}  // 自動跳脫,安全

{!! $content !!}   // 不跳脫,危險!只在信任的內容使用

在 API 回應中,也要確保不會輸出未清理的 HTML:

// 清理使用者輸入的 HTML
use Illuminate\Support\Str;

$cleanContent = Str::of($request->input('content'))
    ->stripTags(['p', 'br', 'strong', 'em']); // 只允許特定標籤

速率限制

防止暴力破解和 DDoS

// routes/api.php
Route::middleware('throttle:api')->group(function () {
    // 一般 API:每分鐘 60 次
    Route::apiResource('products', ProductController::class);
});

// 登入 API:更嚴格的限制
Route::post('/login', [AuthController::class, 'login'])
    ->middleware('throttle:5,1'); // 每分鐘 5 次

// 自訂限制器
RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by(
        $request->user()?->id ?: $request->ip()
    )->response(function () {
        return response()->json([
            'message' => '請求過於頻繁,請稍後再試',
        ], 429);
    });
});

CORS 設定

// config/cors.php
return [
    'paths' => ['api/*'],
    'allowed_origins' => [
        'https://myapp.com',
        'https://admin.myapp.com',
    ],
    'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'],
    'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With'],
    'max_age' => 3600,
    'supports_credentials' => true,
];

不要使用 'allowed_origins' => ['*'],這等於對所有來源開放。

HTTPS 強制

// app/Providers/AppServiceProvider.php
public function boot(): void
{
    if ($this->app->environment('production')) {
        URL::forceScheme('https');
    }
}

在 Nginx 中強制 HTTPS:

server {
    listen 80;
    server_name myapp.com;
    return 301 https://$server_name$request_uri;
}

敏感資料保護

不要在回應中暴露敏感資訊

// API Resource 控制輸出欄位
class UserResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            // 不要回傳密碼、Token 等敏感資訊
            // 'password' => $this->password,  // 絕對不要
        ];
    }
}

隱藏錯誤細節

// 生產環境不要顯示詳細錯誤
// .env
APP_DEBUG=false

// 自訂錯誤回應
public function render($request, Throwable $e)
{
    if ($request->expectsJson() && app()->environment('production')) {
        return response()->json([
            'message' => '系統發生錯誤,請稍後再試',
        ], 500);
    }

    return parent::render($request, $e);
}

保護 .env 檔案

# Nginx 阻擋存取 .env
location ~ /\.env {
    deny all;
    return 404;
}

日誌與監控

記錄重要的安全事件

// 記錄登入失敗
Log::warning('登入失敗', [
    'email' => $request->email,
    'ip' => $request->ip(),
    'user_agent' => $request->userAgent(),
]);

// 記錄敏感操作
Log::info('使用者資料匯出', [
    'user_id' => auth()->id(),
    'ip' => $request->ip(),
    'exported_count' => $count,
]);

設定警示

當偵測到異常行為(例如短時間內大量登入失敗),自動發送通知給管理員。

安全性檢查清單

  • 使用 HTTPS
  • 實作認證和授權
  • 所有輸入都有驗證
  • 使用參數綁定防止 SQL Injection
  • 設定速率限制
  • CORS 限制只允許信任的來源
  • API 回應不包含敏感資訊
  • 生產環境關閉 DEBUG 模式
  • 記錄安全相關的日誌
  • 定期更新套件修補漏洞

結語

安全性不是做一次就好的事情,而是需要持續關注的過程。養成在開發階段就考慮安全性的習慣,比上線後才來補救要好得多。對於接案工程師來說,幫客戶做好安全防護不只是責任,也是你專業度的展現。

#API #安全性 #Laravel #資安
Thousand Lab

Thousand Lab