> ## 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 — 路由保护中间件调查

> 解读 laravel/sentinel 包的源代码。作为基于驱动的路由保护中间件,它是如何守护 Telescope、Horizon、Pulse 等管理工具的?本文进行解析。

<Info>
  本文基于源代码(`1.x` 分支)整理。目前尚无官方文档,是正式发布前的包(截至 2026 年 4 月)。
</Info>

## 什么是 Sentinel

[Laravel Sentinel](https://github.com/laravel/sentinel) 是一个基于驱动、控制路由访问的安全中间件包。由 Taylor Otwell 和 Mior Muhammad Zaki 开发,支持 PHP ^8.0 / Laravel 8\~13。

主要用途是保护 Telescope、Horizon、Pulse 等管理工具类路由。只要给路由挂上 `SentinelMiddleware`,就能用驱动定义的授权逻辑控制访问。

```mermaid theme={null}
graph TD
    A["HTTP 请求"] --> B["SentinelMiddleware"]
    B --> C["Sentinel Facade"]
    C --> D["SentinelManager<br>(Illuminate\\Support\\Manager)"]
    D --> E["解析驱动<br>driverOrFallback()"]
    E --> F["Driver#authorize()"]
    F -->|true| G["进入下一个中间件"]
    F -->|false| H["abort 401"]
```

## 与传统做法的对比

Telescope 和 Horizon 各自都提供了基于 `gate` 的授权机制。

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

这种方式依赖用户的认证状态,存在“不登录就无法判定”的问题。Sentinel 作为中间件运行,无论是否已认证,都可以按请求维度控制访问。同时可以在一处定义规则,应用于多个管理工具。

## 安装

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

`composer.json` 的 `extra.laravel.providers` 中已经注册了 `SentinelServiceProvider`,服务提供者会被自动发现。也不需要额外发布配置文件。

## 包的架构

```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 : 使用
```

## 默认驱动(Laravel 驱动)的行为

默认的 `Laravel` 驱动\*\*只在本地环境(`APP_ENV=local`)\*\*才会做控制。

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

    // 通过隧道服务访问时给出警告
    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>使用隧道服务吗?}
    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` 驱动在非 `local` 环境总是返回 `true`(允许)。若需要在生产环境做访问控制,请自定义驱动。
</Warning>

### 私有 IP 判定

`Driver` 基类的 `isPrivateIp()` 使用 Symfony 的 `IpUtils`,把以下 IP 范围视为私有 IP。

| 范围               | 说明            |
| ---------------- | ------------- |
| `127.0.0.0/8`    | 回环(RFC1700)   |
| `10.0.0.0/8`     | 私有(RFC1918)   |
| `192.168.0.0/16` | 私有(RFC1918)   |
| `172.16.0.0/12`  | 私有(RFC1918)   |
| `169.254.0.0/16` | 链路本地(RFC3927) |
| `::1/128`        | IPv6 回环       |
| `fc00::/7`       | IPv6 唯一本地     |
| `fe80::/10`      | IPv6 链路本地     |

### 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 本地环境。

## 作为中间件使用

### 基本用法

```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 () { /* ... */ });
});
```

### 指定驱动

可以通过中间件参数传入驱动名。如果传入不存在的驱动名,会回退到默认驱动(`driverOrFallback()` 的行为)。

```php theme={null}
Route::middleware([SentinelMiddleware::class . ':custom-driver'])->group(function () {
    // 使用自定义驱动做授权
});
```

### 中间件实现

```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`(不过中间件本身没走这个分支)。

## 编写自定义驱动

继承 `Driver` 抽象类并实现 `authorize()` 方法,就能写出自定义驱动。

```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()` 里。

### 利用基类的实用方法

`Driver` 类里内置了 IP 判定等实用方法,自定义驱动可以直接使用。

```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` 是 Laravel 用来实现驱动模式的基础类,`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  // 不记录日志
        );
    }
}
```

`driverOrFallback()` 在找不到指定驱动时会静默回退到默认驱动,即便你给中间件参数传入不存在的驱动名,也不会报错。

`SentinelManager` 被 `SentinelServiceProvider` 注册为作用域单例(`scoped`),因此在同一次请求中会复用驱动实例。

```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` 的驱动模式,具备很高的可扩展性。

| 项目         | 内容                         |
| ---------- | -------------------------- |
| 版本         | 1.x                        |
| 支持 PHP     | ^8.0                       |
| 支持 Laravel | 8\~13                      |
| 默认驱动       | 仅在 local 环境做控制             |
| 自定义驱动      | 通过 `Sentinel::extend()` 添加 |

默认的 `Laravel` 驱动专门用来防止本地开发时通过隧道服务的意外访问,生产环境的保护需要你自己写自定义驱动。等官方文档完善后,应该会有更具体的使用模式。

<Card title="laravel/sentinel 仓库" icon="github" href="https://github.com/laravel/sentinel">
  源代码和最新变更可以在 GitHub 的 `1.x` 分支查看。
</Card>


## Related topics

- [CSRF 保护](/zh/csrf.md)
- [认证入门](/zh/authentication.md)
- [中间件](/zh/middleware.md)
- [路由](/zh/routing.md)
- [Laravel Cashier (Stripe)](/zh/cashier.md)
