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

# 包的授权 Policy 注册与覆盖

> 解读 Laravel 13 的 Gate 实现，讲解包的 Policy 注册、避免依赖用户模型的设计、解析优先级，以及在使用方应用中的覆盖与测试。

有时你希望为包提供的模型添加授权，同时又给使用该包的应用留出替换为自有规则的余地。如果明确声明模型与 Policy 的对应关系，并在执行授权时才判定用户，就能将注册处理与每个请求的判断分离开来。

本页参考了 Laravel 官方文档的默认分支 `13.x`，以及 2026 年 11 月 10 日确认的 Framework 最新版本 `v13.42.0` 的官方实现与测试。授权的基础请参阅[授权指南](https://kawax.biz/zh-CN/authorization.md)，提供者的加载请参阅[包自动发现](https://kawax.biz/zh-CN/advanced/package-discovery.md)。

## 不依赖使用方应用的 User 类

以为包中的 `Acme\Reports\Models\Report` 添加 `update` 授权为例。假设 `acme_reports` 表中有一个保存所有者认证标识符的 `owner_id` 列。迁移的提供方式请参阅[包的迁移](https://kawax.biz/zh-CN/advanced/package-migrations.md)。

`src/Models/Report.php` 是普通的 Eloquent 模型。

```php theme={null}
<?php

namespace Acme\Reports\Models;

use Illuminate\Database\Eloquent\Model;

class Report extends Model
{
    protected $table = 'acme_reports';
}
```

如果包中的 Policy 将类型指定为 `App\Models\User`，就会依赖使用方应用的命名空间和模型结构。如果只需要用户的认证标识符，请使用 Laravel 的 `Authenticatable` 契约。

在 `src/Policies/ReportPolicy.php` 中放置以下代码。

```php theme={null}
<?php

namespace Acme\Reports\Policies;

use Acme\Reports\Models\Report;
use Illuminate\Contracts\Auth\Authenticatable;

class ReportPolicy
{
    public function update(Authenticatable $user, Report $report): bool
    {
        $userId = $user->getAuthIdentifier();
        $ownerId = $report->getAttribute('owner_id');

        return $userId !== null
            && $ownerId !== null
            && (string) $userId === (string) $ownerId;
    }
}
```

先排除 `null` 标识符，再将两者转换为字符串进行比较，以消除从数据库获取的整数与字符串之间的类型差异。在本例中只允许所有者更新。在处理多种用户类型或 Guard 的应用中，还要确认仅凭标识符能否唯一确定主体，必要时替换为包含用户类型或租户边界的自有 Policy。

> [!NOTE]
>
> `Authenticatable` 是关于认证标识符等的契约，不提供 `isAdministrator()` 或获取角色的方法。如果需要包特有的权限信息，请定义另一个契约，并设计成由使用方应用注入实现。

## 在服务提供者中注册对应关系

在 `src/ReportsServiceProvider.php` 的 `boot()` 中，只注册包所拥有的模型。

```php theme={null}
<?php

namespace Acme\Reports;

use Acme\Reports\Models\Report;
use Acme\Reports\Policies\ReportPolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

class ReportsServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Gate::policy(Report::class, ReportPolicy::class);
    }
}
```

`Gate::policy()` 以模型的类名为键保存 Policy 的类名。注册本身不会创建 Policy，也不会查询数据库或判定用户。当 `Gate::getPolicyFor()` 等需要 Policy 时，才通过容器的 `make()` 创建。因此可以向 Policy 的构造函数注入依赖服务。

`Report` 与 `ReportPolicy` 的放置位置也符合标准的自动发现，但这里为了明确包所提供的对应关系而进行显式注册。仅注册并不会对 HTTP 端点应用授权。请在更新处理之前，在控制器等位置调用以下代码。

```php theme={null}
use Acme\Reports\Models\Report;
use Illuminate\Support\Facades\Gate;

$report = Report::query()->findOrFail($id);

Gate::authorize('update', $report);
```

被拒绝时会抛出 `AuthorizationException`，在通常的 HTTP 处理中会成为 403 响应。传入模型的 `update` 与传入模型类名的 `create` 等，其 Policy 方法的参数不同，请按照[授权指南](https://kawax.biz/zh-CN/authorization.md)中的调用方式。

## 解读 Policy 解析的优先级

`Gate::getPolicyFor()` 在目标为对象时会将其转换为类名，并按以下顺序查找。这是**选择 Policy 的顺序**，而不是整个授权流程中钩子的执行顺序。

| 顺序 | 查找对象 |
| - | - |
| 1 | 针对目标类本身的 `Gate::policy()` 显式注册 |
| 2 | 目标类自身的 `#[UsePolicy]` |
| 3 | 基于标准命名规则或 `guessPolicyNamesUsing()` 的推测 |
| 4 | 针对父类、所实现接口的显式注册 |
| 5 | 从父类继承查找的 `#[UsePolicy]` |

因此，在存在针对同一 `Report` 类的显式注册时，仅修改 `#[UsePolicy]` 或命名规则并不能完成替换。另一方面，如果使用方应用使用继承自 `Report` 的模型，则通过子类自身的属性或推测找到的 Policy 优先于针对父类的显式注册。请同时用实际使用的模型类进行确认。

> [!WARNING]
>
> `guessPolicyNamesUsing()` 并不是向回调列表追加，而是替换 Gate 持有的唯一推测回调。如果一个独立的包仅为自己的模型设置它，也会影响使用方应用和其他包的推测。请以针对包所拥有模型的 `policy()` 注册为基础。

## 在使用方应用中显式覆盖

对同一模型再次调用 `policy()` 时，对应关系会被后一次注册替换。仅仅放在另一个提供者的 `boot()` 中会依赖加载顺序，因此可以在使用方应用的 `AppServiceProvider::boot()` 中注册应用启动完成回调。

```php theme={null}
use Acme\Reports\Models\Report;
use App\Policies\ApplicationReportPolicy;
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    $this->app->booted(static function (): void {
        Gate::policy(Report::class, ApplicationReportPolicy::class);
    });
}
```

`ApplicationReportPolicy` 是使用方应用实现的 Policy。如果包在通常的 `boot()` 中注册，该回调会在其之后执行。但它并不是一种无条件最优先的机制，其他启动完成回调中的再次注册或之后加载的提供者都可能覆盖它。在本例中，包的注册提供者不设为延迟提供者。

另一种方法是不改变对应表，而是在容器中替换 `ReportPolicy::class` 的解析目标。`resolvePolicy()` 每次都会请求容器解析，但实际是否得到新实例取决于该 Policy 的绑定。请避免在构造函数中获取用户或请求信息并保存在 singleton Policy 中的设计。

## 区分影响整个授权的钩子

在通常的 Gate 检查中，全局的 `Gate::before()` 会先执行。如果它返回 `null` 以外的值，就不会执行 Policy 的判定。

```mermaid theme={null}
flowchart TD
    A["Gate::authorize / allows"] --> B["全局 Gate::before"]
    B --> C{"返回值为 null?"}
    C -->|No| D["使用该结果"]
    C -->|Yes| E["查找目标 Policy 与<br>可调用的方法"]
    E --> F["若有 Policy 的 before 则调用"]
    F --> G["若 before 为 null<br>则由 Policy 方法判定"]
```

此图摘取了找到 Policy 且可以调用对应方法的路径。省略了之后的全局 `after` 钩子，以及 Policy 中没有目标方法时回退到 Gate 定义的情况。

不要为了在包内实现管理员处理而注册允许所有能力的全局 `Gate::before()`。这会连其他包和使用方应用的授权也一并允许。Policy 内的 `before()` 仅作用于该 Policy，但当对应能力的方法不存在时不会被调用。如果要交给后续方法判断，请返回 `null` 而不是 `false`。

此外，本例中的 `update()` 要求非 nullable 的 `Authenticatable`，因此访客不会执行 Policy 方法。如果没有全局钩子等覆盖结果，就会被拒绝。若要在公开浏览等场景下允许访客，请另外准备 nullable 的用户参数和显式判定。

## 分别测试注册与判定

这里假设使用[包测试](https://kawax.biz/zh-CN/advanced/package-testing.md)中介绍的 Testbench 基类，并在 `getPackageProviders()` 中加载 `ReportsServiceProvider::class`。以下是 `tests/Feature/ReportPolicyTest.php` 的示例。用户使用 Laravel 的通用认证模型，无需保存到数据库即可判定。

```php theme={null}
<?php

namespace Acme\Reports\Tests\Feature;

use Acme\Reports\Models\Report;
use Acme\Reports\Policies\ReportPolicy;
use Acme\Reports\Tests\TestCase;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Auth\User;

class ReportPolicyTest extends TestCase
{
    public function test_provider_registers_the_policy(): void
    {
        $gate = $this->app->make(GateContract::class);

        $this->assertInstanceOf(
            ReportPolicy::class,
            $gate->getPolicyFor(Report::class),
        );
    }

    public function test_only_the_owner_can_update(): void
    {
        $gate = $this->app->make(GateContract::class);
        $report = (new Report)->forceFill(['owner_id' => 7]);
        $owner = (new User)->forceFill(['id' => 7]);
        $other = (new User)->forceFill(['id' => 8]);

        $this->assertTrue($gate->forUser($owner)->allows('update', $report));
        $this->assertFalse($gate->forUser($other)->allows('update', $report));
        $this->assertFalse($gate->forUser(null)->allows('update', $report));
        $this->assertFalse($gate->forUser($owner)->allows('update', new Report));
    }

    public function test_an_override_does_not_change_an_existing_user_gate(): void
    {
        $gate = $this->app->make(GateContract::class);
        $owner = (new User)->forceFill(['id' => 7]);
        $report = (new Report)->forceFill(['owner_id' => 7]);
        $existingUserGate = $gate->forUser($owner);

        $gate->policy(Report::class, DenyReportPolicy::class);

        $this->assertInstanceOf(
            DenyReportPolicy::class,
            $gate->getPolicyFor(Report::class),
        );
        $this->assertFalse($gate->forUser($owner)->allows('update', $report));
        $this->assertTrue($existingUserGate->allows('update', $report));
    }
}

class DenyReportPolicy
{
    public function update(Authenticatable $user, Report $report): bool
    {
        return false;
    }
}
```

`forUser()` 并不会改写共享 Gate 的用户，而是传入当时的对应表、能力和回调来生成另一个 Gate。因此，已生成的按用户 Gate 不会反映之后对共享 Gate 的 Policy 注册变更。请在启动时完成注册，并避免将按用户 Gate 保存在长生命周期的服务中。

在使用方应用中，还需要用实际的 Guard、用户模型进行判定，以及针对更新端点的 HTTP 测试。仅凭上述测试，无法验证路由是否应用了授权，以及数据库中所有者标识符的保存格式。

### 更新时需要维持的契约

* 所有者、非所有者、访客、未设置所有者时的结果是否保持不变。
* 公开的能力名称与 Policy 方法的参数是否一致。
* 使用方应用对 Policy 的替换是否仍然有效。
* 如果支持子模型，子类实际选择的 Policy 是否正确。
* 包括应用的全局钩子在内，实际的更新请求是否按预期被允许或拒绝。

删除现有能力或改变默认允许、拒绝行为的更新会影响使用方应用的行为。请结合[版本兼容性管理](https://kawax.biz/zh-CN/advanced/package-versioning.md)，将授权的变更也视为公开契约。

## 参考的一手资料

* [Laravel 官方文档：授权](https://github.com/laravel/docs/blob/13.x/authorization.md)
* [Laravel 官方文档：包开发](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Framework v13.42.0：Gate 的注册、解析与 forUser](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Auth/Access/Gate.php)
* [Framework v13.42.0：将 Gate 注册为 singleton 的 AuthServiceProvider](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Auth/AuthServiceProvider.php)
* [Framework v13.42.0：Application 的 boot 与 booted](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Foundation/Application.php)
* [Framework v13.42.0：授权 Gate 的官方测试](https://github.com/laravel/framework/blob/v13.42.0/tests/Auth/AuthAccessGateTest.php)


## Related topics

- [授权（Gate 与 Policy）](https://kawax.biz/zh-CN/authorization.md)
- [进阶主题](https://kawax.biz/zh-CN/advanced/index.md)
- [包的路由注册与缓存](https://kawax.biz/zh-CN/advanced/package-routes.md)
- [包的中间件注册与适用范围](https://kawax.biz/zh-CN/advanced/package-middleware.md)
- [包的定时任务注册与维护](https://kawax.biz/zh-CN/advanced/package-scheduling.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.