> ## 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 Sentinel — 路由保護 Middleware 調查

> 解讀 laravel/sentinel 套件的原始碼。作為 driver 導向的路由保護 middleware，解說如何守護 Telescope、Horizon、Pulse 等管理工具。

<Info>
  本文根據原始碼調查（`1.x` 分支）整理。目前尚無官方文件，仍為正式發佈前的套件（2026 年 4 月時點）。
</Info>

## 什麼是 Sentinel

[Laravel Sentinel](https://github.com/laravel/sentinel) 是以 driver 為基礎控制路由存取的安全 middleware 套件。由 Taylor Otwell 與 Mior Muhammad Zaki 開發，支援 PHP ^8.0 / Laravel 8〜13。

主要用途是保護 Telescope、Horizon、Pulse 等管理工具路由。只要對路由套用 `SentinelMiddleware`，就能依 driver 定義的授權邏輯來控制存取。

```mermaid theme={null}
graph TD
    A["HTTP 請求"] --> B["SentinelMiddleware"]
    B --> C["Sentinel Facade"]
    C --> D["SentinelManager<br>（Illuminate\\Support\\Manager）"]
    D --> E["解析 driver<br>driverOrFallback()"]
    E --> F["Driver#authorize()"]
    F -->|true| G["進入下一個 middleware"]
    F -->|false| H["abort 401"]
```

## 與傳統方式的比較

Telescope 與 Horizon 各有以 `gate` 為基礎的授權機制。

```php theme={null}
// 傳統做法（TelescopeServiceProvider 等）
Gate::before(function ($user) {
    return $user->isAdmin() ? true : null;
});
```

此方式依賴使用者的認證狀態，有「沒登入就無法判定」的問題。Sentinel 作為 middleware 運作，不論是否已認證都能以請求為單位控制存取，並可為多個管理工具在同一處定義規則。

## 安裝

```bash theme={null}
composer require laravel/sentinel
```

由於 `composer.json` 的 `extra.laravel.providers` 已註冊 `SentinelServiceProvider`，Service Provider 會被自動偵測。也不必發佈額外設定檔。

## 套件架構

```mermaid theme={null}
classDiagram
    class Sentinel {
        +static getFacadeAccessor()
    }
    class SentinelManager {
        +createLaravelDriver() Laravel
        +getDefaultDriver() string
        +driverOrFallback(?string driver) mixed
        +extend(string driver, Closure callback) SentinelManager
    }
    class Driver {
        #Closure applicationResolver
        +authorize(Request request) bool*
        +authorizeOrFail(Request request) void
        #authorizeAccessingViaReverseProxies(Request request) bool
        #isRunningOnDockerLocally(Request request) bool
        #isPrivateIp(string requestIp) bool
        #app() Application
    }
    class Laravel {
        +authorize(Request request) bool
    }
    class SentinelMiddleware {
        +handle(Request request, Closure next, ?string driver) mixed
    }

    Sentinel --> SentinelManager : Facade
    SentinelManager --|> Manager
    Laravel --|> Driver
    SentinelMiddleware --> Sentinel : 使用
```

## 預設 driver（Laravel driver）的行為

預設的 `Laravel` driver 只在**本機環境（`APP_ENV=local`）下**才進行控制。

```php theme={null}
// src/Drivers/Laravel.php
public function authorize(Request $request): bool
{
    if (! $this->app()->environment('local')) {
        return true; // local 以外一律允許
    }

    // 對透過 tunnel 服務的存取警告
    if ($this->isPrivateIp($request->ip())
        && ! $request->isFromTrustedProxy()
        && Str::endsWith($request->host(), ['.sharedwithexpose.com', '.ngrok-free.app', '.ngrok.io'])) {
        throw new RuntimeException(
            'Unable to access "..." using "local" environment, ...'
        );
    }

    // Docker 本機環境允許
    if ($this->isRunningOnDockerLocally($request)) {
        return true;
    }

    // 檢查透過反向代理的存取
    return $this->authorizeAccessingViaReverseProxies($request);
}
```

整理流程如下：

```mermaid theme={null}
flowchart TD
    A[收到請求] --> B{是否為 local 環境？}
    B -->|No| C[回傳 true<br>（一律允許）]
    B -->|Yes| D{是否為私人 IP<br>且非 trustedProxy<br>且是 tunnel 服務？}
    D -->|Yes| E[拋出 RuntimeException<br>顯示設定引導]
    D -->|No| F{是否為 Docker 本機環境？<br>127.0.0.1 + .dockerenv}
    F -->|Yes| G[回傳 true<br>（允許）]
    F -->|No| H{是否為非私人 IP<br>且透過 trustedProxy？}
    H -->|Yes| I[回傳 false<br>（封鎖）]
    H -->|No| J[回傳 true<br>（允許）]
```

<Warning>
  `Laravel` driver 在 `local` 以外環境一律回傳 `true`（允許）。正式環境需要存取控制時請自訂 driver。
</Warning>

### 私人 IP 判斷

`Driver` 基底類別的 `isPrivateIp()` 使用 Symfony 的 `IpUtils` 判斷以下 IP 範圍為私人 IP：

| 範圍               | 說明                  |
| ---------------- | ------------------- |
| `127.0.0.0/8`    | Loopback（RFC1700）   |
| `10.0.0.0/8`     | Private（RFC1918）    |
| `192.168.0.0/16` | Private（RFC1918）    |
| `172.16.0.0/12`  | Private（RFC1918）    |
| `169.254.0.0/16` | Link-Local（RFC3927） |
| `::1/128`        | IPv6 Loopback       |
| `fc00::/7`       | IPv6 Unique Local   |
| `fe80::/10`      | IPv6 Link-Local     |

### Docker 本機環境偵測

```php theme={null}
// src/Drivers/Driver.php
protected function isRunningOnDockerLocally(Request $request): bool
{
    return $request->server->get('REMOTE_ADDR') === '127.0.0.1'
        && file_exists(base_path('.dockerenv'));
}
```

`REMOTE_ADDR` 為 `127.0.0.1` 且專案根目錄存在 `.dockerenv` 時，判斷為 Docker 本機環境。

## 作為 middleware 使用

### 基本用法

```php theme={null}
use Laravel\Sentinel\Http\Middleware\SentinelMiddleware;

Route::middleware(SentinelMiddleware::class)->group(function () {
    Route::get('/telescope', function () { /* ... */ });
    Route::get('/horizon', function () { /* ... */ });
    Route::get('/pulse', function () { /* ... */ });
});
```

### 指定 driver

可將 driver 名稱作為 middleware 引數。若指定不存在的 driver 會回退到預設 driver（`driverOrFallback()` 的行為）。

```php theme={null}
Route::middleware([SentinelMiddleware::class . ':custom-driver'])->group(function () {
    // 由自訂 driver 進行授權
});
```

### middleware 實作

```php theme={null}
// src/Http/Middleware/SentinelMiddleware.php
public function handle(Request $request, Closure $next, ?string $driver = null)
{
    abort_unless(Sentinel::driverOrFallback($driver)->authorize($request), 401);

    return $next($request);
}
```

`authorize()` 回傳 `false` 時以 HTTP 401 中止。使用 `Driver::authorizeOrFail()` 也能拋出 `AuthorizationException`（middleware 本身未使用）。

## 建立自訂 driver

繼承 `Driver` 抽象類別並實作 `authorize()` 方法就能建立自訂 driver。

```php theme={null}
use Laravel\Sentinel\Sentinel;
use Laravel\Sentinel\Drivers\Driver;
use Illuminate\Http\Request;

Sentinel::extend('admin-only', function ($app) {
    return new class extends Driver {
        public function authorize(Request $request): bool
        {
            // 只允許已認證且擁有 admin 角色的使用者
            $user = $request->user();

            return $user !== null && $user->hasRole('admin');
        }
    };
});
```

`extend()` 的註冊可放在 `AppServiceProvider::boot()` 等。

### 活用基底類別的 utility 方法

`Driver` 類別備有 IP 判斷等便利方法，可從自訂 driver 自由呼叫。

```php theme={null}
public function authorize(Request $request): bool
{
    // 正式環境僅允許私人 IP
    if ($this->app()->environment('production')) {
        return $this->isPrivateIp($request->ip());
    }

    // 本機僅允許 Docker 或直接存取
    return $this->isRunningOnDockerLocally($request)
        || $this->authorizeAccessingViaReverseProxies($request);
}
```

## SentinelManager 機制

`SentinelManager` 繼承 `Illuminate\Support\Manager`。`Manager` 是實作 driver pattern 的 Laravel 基礎類別，以 `driver()` 快取並解析 driver 實例。

```php theme={null}
// src/SentinelManager.php
class SentinelManager extends Manager
{
    protected function createLaravelDriver()
    {
        return new Laravel(fn () => $this->getContainer());
    }

    public function getDefaultDriver()
    {
        return 'laravel';
    }

    public function driverOrFallback(?string $driver)
    {
        return rescue(
            fn () => $this->driver($driver),
            value(fn () => $this->driver()),
            false  // 不記錄到 log
        );
    }
}
```

`driverOrFallback()` 即使找不到指定 driver 也會安靜地回退到預設 driver，因此把不存在的 driver 名稱傳給 middleware 引數也不會出錯。

`SentinelManager` 由 `SentinelServiceProvider` 註冊為 scoped singleton（`scoped`），因此在同一請求內 driver 實例會被重用。

```php theme={null}
// src/SentinelServiceProvider.php
public function register(): void
{
    $this->app->scoped(SentinelManager::class, fn ($app) => new SentinelManager($app));
}
```

## 總結

`laravel/sentinel` 雖小，但透過使用 `Illuminate\Support\Manager` 的 driver pattern 具高擴充性。

| 項目         | 內容                         |
| ---------- | -------------------------- |
| 版本         | 1.x                        |
| 支援 PHP     | ^8.0                       |
| 支援 Laravel | 8〜13                       |
| 預設 driver  | 僅 local 環境控制               |
| 自訂 driver  | 可用 `Sentinel::extend()` 新增 |

預設 `Laravel` driver 專注於防止本機開發時 tunnel 服務的存取，正式環境的保護需要自行實作自訂 driver。等官方文件完備後，會有更具體的使用模式。

<Card title="laravel/sentinel 儲存庫" icon="github" href="https://github.com/laravel/sentinel">
  原始碼與最新變更可在 GitHub 的 `1.x` 分支確認。
</Card>


## Related topics

- [部落格](/zh-TW/blog/index.md)
- [路由](/zh-TW/routing.md)
- [起始套件（Starter Kit）](/zh-TW/starter-kits.md)
- [CSRF 保護](/zh-TW/csrf.md)
- [認證入門](/zh-TW/authentication.md)
