> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Laravel Sanctum（API token 認證）

> 說明使用 Laravel Sanctum 進行 API token 認證與 SPA 認證的實作方法。從簡潔輕量的認證套件導入到實務用法。

## 什麼是 Sanctum

Laravel Sanctum 是為 SPA（單頁應用程式）、行動應用程式與簡潔 API 而設計的輕量認證套件。無需複雜的 OAuth 知識，即可為每位使用者發放並管理多個 API token。

Sanctum 解決兩個問題：

| 認證模式             | 機制                                     | 主要用途              |
| ---------------- | -------------------------------------- | ----------------- |
| **API token 認證** | `Authorization: Bearer <token>` header | 行動應用、第三方整合        |
| **SPA 認證**       | Session Cookie + CSRF 保護               | 自家前端（Vue/React 等） |

<Info>
  自家 SPA 呼叫 API 時使用 SPA 認證。行動應用或第三方使用 API 時使用 API token 認證。也可以只使用其中一種。
</Info>

### 與 Passport 的分工

|              | Sanctum               | Passport               |
| ------------ | --------------------- | ---------------------- |
| **複雜度**      | 簡潔                    | 完整功能 OAuth2            |
| **適用用途**     | 自家 SPA、行動應用           | 對外部應用作為 OAuth provider |
| **Token 類型** | Personal Access Token | OAuth2 Access Token    |

若需對外部服務作為 OAuth2 provider，請選 Passport；但多數應用程式 Sanctum 就足夠。

***

## 安裝與設定

### 安裝

只要執行 `install:api` Artisan 指令即可設定 Sanctum。

```shell theme={null}
php artisan install:api
```

此指令會自動執行以下事項：

* 安裝 `laravel/sanctum` 套件
* 發布 `personal_access_tokens` 資料表的 migration 檔
* 執行 migration

### 加入 HasApiTokens trait

在 `User` model 加入 `HasApiTokens` trait。

```php theme={null}
// app/Models/User.php

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}
```

如此即可使用 `$user->createToken()`、`$user->tokens` 等方法。

***

## API Token 認證

### Token 流程

```mermaid theme={null}
sequenceDiagram
    participant Client as 用戶端
    participant API as Laravel API
    participant DB as 資料庫

    Client->>API: POST /login (email, password)
    API->>DB: 使用者認證
    DB-->>API: 使用者資訊
    API->>DB: 產生並儲存 token（雜湊）
    API-->>Client: 回傳 plainTextToken

    Note over Client: 儲存 token

    Client->>API: GET /api/user<br>Authorization: Bearer <token>
    API->>DB: 對照 token（SHA-256）
    DB-->>API: 已認證使用者
    API-->>Client: 回應
```

### 發行 Token

以 `createToken()` 方法發行 token。從 `plainTextToken` 屬性取得明文 token 值。**明文 token 不會保存到資料庫**，因此發行後必須立即回傳給使用者。

```php theme={null}
use Illuminate\Http\Request;

Route::post('/tokens/create', function (Request $request) {
    $token = $request->user()->createToken($request->token_name);

    return ['token' => $token->plainTextToken];
})->middleware('auth');
```

資料庫中儲存的是以 SHA-256 雜湊後的 token。

### 設定 Scope（Ability）

為 token 賦予 ability（scope），可限制該 token 能執行的操作。

```php theme={null}
// 發行帶 scope 的 token
$token = $user->createToken('mobile-app', ['server:update', 'server:read']);

return $token->plainTextToken;
```

處理請求時，檢查 token 的 scope。

```php theme={null}
if ($request->user()->tokenCan('server:update')) {
    // 執行更新操作
}

if ($request->user()->tokenCant('server:update')) {
    abort(403);
}
```

#### 以 Middleware 檢查 scope

在 `bootstrap/app.php` 註冊 middleware 別名：

```php theme={null}
use Laravel\Sanctum\Http\Middleware\CheckAbilities;
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->alias([
        'abilities' => CheckAbilities::class,  // 需擁有所有 ability
        'ability'   => CheckForAnyAbility::class, // 只要擁有其中之一
    ]);
})
```

在路由套用 middleware。

```php theme={null}
// 只允許同時擁有 check-status 與 place-orders 的 token
Route::get('/orders', function () {
    // ...
})->middleware(['auth:sanctum', 'abilities:check-status,place-orders']);

// 允許擁有 check-status 或 place-orders 其中之一的 token
Route::get('/orders', function () {
    // ...
})->middleware(['auth:sanctum', 'ability:check-status,place-orders']);
```

### Token 的有效期限

預設 Sanctum token 無有效期限。可透過 `config/sanctum.php` 的 `expiration` 選項設定以分鐘為單位的有效期限。

```php theme={null}
// config/sanctum.php
'expiration' => 525600, // 365 天（分鐘）
```

也可個別為每個 token 指定有效期限。

```php theme={null}
$token = $user->createToken(
    'token-name',
    ['*'],
    now()->addWeeks(1) // 1 週後過期
)->plainTextToken;
```

若有設定有效期限，請安排定期刪除過期 token 的排程。

```php theme={null}
use Illuminate\Support\Facades\Schedule;

Schedule::command('sanctum:prune-expired --hours=24')->daily();
```

### 使 Token 失效

```php theme={null}
// 刪除所有 token
$user->tokens()->delete();

// 刪除目前請求所使用的 token
$request->user()->currentAccessToken()->delete();

// 刪除特定 token
$user->tokens()->where('id', $tokenId)->delete();
```

***

## SPA 認證

SPA 認證使用 session Cookie，不需要發行或管理 token。適合自家前端（Vue、React、Next.js 等）呼叫 API 的情境。

<Warning>
  使用 SPA 認證時，SPA 與 API 需共用相同的 top-level domain（子網域可以不同）。此外，請求必須包含 `Accept: application/json` header 以及 `Referer` 或 `Origin` header。
</Warning>

### 啟用 Sanctum middleware

在 `bootstrap/app.php` 啟用 `statefulApi()` middleware。

```php theme={null}
->withMiddleware(function (Middleware $middleware): void {
    $middleware->statefulApi();
})
```

### 設定第一方網域

在 `config/sanctum.php` 的 `stateful` 選項設定 SPA 的網域。

```php theme={null}
// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
    '%s%s',
    'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
    Sanctum::currentApplicationUrlWithPort()
))),
```

### 設定 CORS

若從其他子網域呼叫 API，需要設定 CORS。

```shell theme={null}
php artisan config:publish cors
```

在 `config/cors.php` 將 `supports_credentials` 設為 `true`。

```php theme={null}
// config/cors.php
'supports_credentials' => true,
```

前端的 axios 也需要設定。

```js theme={null}
// resources/js/bootstrap.js
axios.defaults.withCredentials = true;
axios.defaults.withXSRFToken = true;
```

Session Cookie 的網域設定也不可忘。

```php theme={null}
// config/session.php
'domain' => '.example.com', // 開頭加上點
```

### 來自 SPA 的認證流程

```mermaid theme={null}
sequenceDiagram
    participant SPA as SPA 前端
    participant API as Laravel API
    participant Session as Session

    SPA->>API: GET /sanctum/csrf-cookie
    API-->>SPA: 設定 XSRF-TOKEN Cookie

    SPA->>API: POST /login<br>X-XSRF-TOKEN header
    API->>Session: 建立 session
    API-->>SPA: Set-Cookie: laravel_session

    SPA->>API: GET /api/user<br>Cookie: laravel_session
    API->>Session: 確認 session
    Session-->>API: 已認證使用者
    API-->>SPA: 使用者資訊
```

<Steps>
  <Step title="取得 CSRF cookie">
    在登入前呼叫 `/sanctum/csrf-cookie` 端點初始化 CSRF 保護。

    ```js theme={null}
    await axios.get('/sanctum/csrf-cookie');
    ```
  </Step>

  <Step title="送出登入請求">
    向 `/login` 端點送出 POST 請求。

    ```js theme={null}
    await axios.post('/login', {
        email: 'user@example.com',
        password: 'password',
    });
    ```
  </Step>

  <Step title="送出已認證的請求">
    登入後的請求會自動以 session Cookie 認證。

    ```js theme={null}
    const response = await axios.get('/api/user');
    console.log(response.data); // 已認證使用者資訊
    ```
  </Step>
</Steps>

***

## 保護已認證的路由

將 `auth:sanctum` middleware 套用於路由，未認證的請求會收到 `401 Unauthorized`。API token 認證與 SPA 認證都可由這一個 middleware 處理。

```php theme={null}
use Illuminate\Http\Request;

// 保護單一路由
Route::get('/user', function (Request $request) {
    return $request->user();
})->middleware('auth:sanctum');

// 保護路由群組
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/profile', [ProfileController::class, 'show']);
    Route::put('/profile', [ProfileController::class, 'update']);
    Route::get('/posts', [PostController::class, 'index']);
});
```

***

## 實用範例：登入 API 與 token 回傳

以下是為行動應用實作 API token 認證的範例。

<Steps>
  <Step title="建立登入端點">
    ```php theme={null}
    // routes/api.php

    use App\Models\User;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Hash;
    use Illuminate\Validation\ValidationException;

    Route::post('/sanctum/token', function (Request $request) {
        $request->validate([
            'email'       => ['required', 'email'],
            'password'    => ['required'],
            'device_name' => ['required', 'string'],
        ]);

        $user = User::where('email', $request->email)->first();

        if (! $user || ! Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['所提供的認證資訊不正確。'],
            ]);
        }

        return response()->json([
            'token' => $user->createToken($request->device_name)->plainTextToken,
        ]);
    });
    ```
  </Step>

  <Step title="建立已認證路由">
    ```php theme={null}
    // routes/api.php

    Route::middleware('auth:sanctum')->group(function () {
        // 回傳目前使用者資訊
        Route::get('/user', function (Request $request) {
            return $request->user();
        });

        // 使目前 token 失效並登出
        Route::post('/logout', function (Request $request) {
            $request->user()->currentAccessToken()->delete();

            return response()->json(['message' => '已登出。']);
        });

        // 從所有裝置登出
        Route::post('/logout/all', function (Request $request) {
            $request->user()->tokens()->delete();

            return response()->json(['message' => '已從所有裝置登出。']);
        });
    });
    ```
  </Step>

  <Step title="從用戶端送出請求">
    ```js theme={null}
    // 登入
    const { data } = await axios.post('/api/sanctum/token', {
        email: 'user@example.com',
        password: 'password',
        device_name: 'My iPhone',
    });

    const token = data.token;

    // 已認證的請求
    const response = await axios.get('/api/user', {
        headers: {
            Authorization: `Bearer ${token}`,
        },
    });
    ```
  </Step>
</Steps>

***

## 測試

在 Sanctum 的測試中，使用 `Sanctum::actingAs()` 認證使用者並指定授予的 ability。

<Tabs>
  <Tab title="Pest">
    ```php theme={null}
    use App\Models\User;
    use Laravel\Sanctum\Sanctum;

    test('可以取得任務清單', function () {
        Sanctum::actingAs(
            User::factory()->create(),
            ['view-tasks']
        );

        $response = $this->get('/api/tasks');

        $response->assertOk();
    });

    test('以具備所有 ability 的 token 可存取', function () {
        Sanctum::actingAs(
            User::factory()->create(),
            ['*']
        );

        $response = $this->get('/api/tasks');

        $response->assertOk();
    });
    ```
  </Tab>

  <Tab title="PHPUnit">
    ```php theme={null}
    use App\Models\User;
    use Laravel\Sanctum\Sanctum;

    public function test_task_list_can_be_retrieved(): void
    {
        Sanctum::actingAs(
            User::factory()->create(),
            ['view-tasks']
        );

        $response = $this->get('/api/tasks');

        $response->assertOk();
    }
    ```
  </Tab>
</Tabs>

***

## 總結

<AccordionGroup>
  <Accordion title="安裝步驟確認">
    ```shell theme={null}
    # 安裝 Sanctum 並執行 migration
    php artisan install:api
    ```

    在 User model 加入 `HasApiTokens` trait：

    ```php theme={null}
    use Laravel\Sanctum\HasApiTokens;

    class User extends Authenticatable
    {
        use HasApiTokens, HasFactory, Notifiable;
    }
    ```
  </Accordion>

  <Accordion title="常用 API 總覽">
    ```php theme={null}
    // 發行 token
    $token = $user->createToken('token-name')->plainTextToken;

    // 發行帶 scope 的 token
    $token = $user->createToken('token-name', ['read', 'write'])->plainTextToken;

    // 確認 scope
    $user->tokenCan('read');   // true/false
    $user->tokenCant('write'); // true/false

    // 使 token 失效
    $user->tokens()->delete();                        // 所有 token
    $request->user()->currentAccessToken()->delete(); // 目前 token
    $user->tokens()->where('id', $id)->delete();      // 特定 token
    ```
  </Accordion>

  <Accordion title="API token 認證 vs SPA 認證選擇">
    * **API token 認證**：行動應用、第三方、CLI 工具等從沒有 session 的用戶端使用時。
    * **SPA 認證**：自家 Vue/React/Next.js 前端等，從相同網域（或子網域）上的 SPA 使用時。更安全且無需管理 token。
  </Accordion>
</AccordionGroup>


## Related topics

- [Laravel Passport（OAuth2 伺服器實作）](/zh-TW/passport.md)
- [Laravel MCP](/zh-TW/mcp.md)
- [路由](/zh-TW/routing.md)
- [Laravel Socialite（社群認證）](/zh-TW/socialite.md)
- [用 Laravel 構建 MCP 伺服器](/zh-TW/advanced/mcp-server.md)
