Skip to content

安全

确保您的 Livewire 应用程序安全且不会暴露任何应用程序漏洞非常重要。Livewire 具有内部安全功能来处理许多情况,但是,有时需要您的应用程序代码来保护组件的安全。

授权操作参数

Livewire 操作非常强大,但是,传递给 Livewire 操作的任何参数都可以在客户端被修改,应该被视为不受信任的用户输入。

可以说,Livewire 中最常见的安全陷阱是在将更改持久化到数据库之前未能验证和授权 Livewire 操作调用。

以下是由于缺乏授权而导致的不安全示例:

php
<?php

use App\Models\Post;
use Livewire\Component;

class ShowPost extends Component
{
    // ...

    public function delete($id)
    {
        // 不安全!

        $post = Post::find($id);

        $post->delete();
    }
}
html
<button wire:click="delete({{ $post->id }})">删除文章</button>

上述示例不安全的原因是 wire:click="delete(...)" 可以在浏览器中被修改,以传递恶意用户希望的任何文章 ID。

操作参数(如本例中的 $id)应该与来自浏览器的任何不受信任的输入一样对待。

因此,为了保持此应用程序的安全并防止用户删除其他用户的文章,我们必须向 delete() 操作添加授权。

首先,让我们通过运行以下命令为 Post 模型创建一个 Laravel Policy

bash
php artisan make:policy PostPolicy --model=Post

运行上述命令后,将在 app/Policies/PostPolicy.php 中创建一个新的 Policy。然后我们可以更新其内容,添加如下的 delete 方法:

php
<?php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    /**
     * 确定给定的文章是否可以被用户删除。
     */
    public function delete(?User $user, Post $post): bool
    {
        return $user?->id === $post->user_id;
    }
}

现在,我们可以在 Livewire 组件中使用 $this->authorize() 方法来确保用户在删除文章之前拥有该文章:

php
public function delete($id)
{
    $post = Post::find($id);

    // 如果用户不拥有该文章,
    // 将抛出 AuthorizationException...
    $this->authorize('delete', $post); // [tl! highlight]

    $post->delete();
}

延伸阅读:

授权公共属性

与操作参数类似,Livewire 中的公共属性应该被视为来自用户的不受信任的输入。

以下是上面删除文章的相同示例,但以另一种不安全的方式编写:

php
<?php

use App\Models\Post;
use Livewire\Component;

class ShowPost extends Component
{
    public $postId;

    public function mount($postId)
    {
        $this->postId = $postId;
    }

    public function delete()
    {
        // 不安全!

        $post = Post::find($this->postId);

        $post->delete();
    }
}
html
<button wire:click="delete">删除文章</button>

如您所见,不是从 wire:click$postId 作为参数传递给 delete 方法,而是将其存储为 Livewire 组件上的公共属性。

这种方法的问题是任何恶意用户都可以在页面上注入自定义元素,例如:

html
<input type="text" wire:model="postId">

这将允许他们在按下"删除文章"之前自由修改 $postId。因为 delete 操作没有授权 $postId 的值,用户现在可以删除数据库中的任何文章,无论他们是否拥有它。

为了防范这种风险,有两种可能的解决方案:

使用模型属性

在设置公共属性时,Livewire 对模型的处理方式与字符串和整数等普通值不同。因此,如果我们将整个 post 模型存储为组件上的属性,Livewire 将确保 ID 永远不会被篡改。

以下是存储 $post 属性而不是简单的 $postId 属性的示例:

php
<?php

use App\Models\Post;
use Livewire\Component;

class ShowPost extends Component
{
    public Post $post;

    public function mount($postId)
    {
        $this->post = Post::find($postId);
    }

    public function delete()
    {
        $this->post->delete();
    }
}
html
<button wire:click="delete">删除文章</button>

此组件现在是安全的,因为恶意用户无法将 $post 属性更改为不同的 Eloquent 模型。

锁定属性

另一种防止属性被设置为不需要的值的方法是使用 #[Locked] 属性。锁定属性是通过应用 #[Locked] 属性来完成的。现在,如果用户尝试篡改此值,将抛出错误。

请注意,具有 Locked 属性的属性仍然可以在后端更改,因此仍然需要注意在您自己的 Livewire 函数中不要将不受信任的用户输入传递给该属性。

php
<?php

use App\Models\Post;
use Livewire\Component;
use Livewire\Attributes\Locked;

class ShowPost extends Component
{
    #[Locked] // [tl! highlight]
    public $postId;

    public function mount($postId)
    {
        $this->postId = $postId;
    }

    public function delete()
    {
        $post = Post::find($this->postId);

        $post->delete();
    }
}

授权属性

如果在您的场景中不希望使用模型属性,您当然可以退回到在 delete 操作中手动授权删除文章:

php
<?php

use App\Models\Post;
use Livewire\Component;

class ShowPost extends Component
{
    public $postId;

    public function mount($postId)
    {
        $this->postId = $postId;
    }

    public function delete()
    {
        $post = Post::find($this->postId);

        $this->authorize('delete', $post); // [tl! highlight]

        $post->delete();
    }
}
html
<button wire:click="delete">删除文章</button>

现在,即使恶意用户仍然可以自由修改 $postId 的值,当调用 delete 操作时,如果用户不拥有该文章,$this->authorize() 将抛出 AuthorizationException

延伸阅读:

中间件

当 Livewire 组件加载在包含路由级授权中间件的页面上时,例如:

php
Route::livewire('/post/{post}', App\Livewire\UpdatePost::class)
    ->middleware('can:update,post'); // [tl! highlight]

Livewire 将确保这些中间件在后续的 Livewire 网络请求中重新应用。这在 Livewire 核心中被称为"持久中间件"。

持久中间件保护您免受初始页面加载后授权规则或用户权限已更改的场景。

以下是这种场景的更深入示例:

php
Route::livewire('/post/{post}', App\Livewire\UpdatePost::class)
    ->middleware('can:update,post'); // [tl! highlight]
php
<?php

use App\Models\Post;
use Livewire\Component;
use Livewire\Attributes\Validate;

class UpdatePost extends Component
{
    public Post $post;

    #[Validate('required|min:5')]
    public $title = '';

    public $content = '';

    public function mount()
    {
        $this->title = $this->post->title;
        $this->content = $this->post->content;
    }

    public function update()
    {
        $this->post->update([
            'title' => $this->title,
            'content' => $this->content,
        ]);
    }
}

如您所见,can:update,post 中间件在路由级别应用。这意味着没有权限更新文章的用户无法查看该页面。

但是,考虑这样一个场景,用户:

  • 加载页面
  • 页面加载后失去更新权限
  • 在失去权限后尝试更新文章

因为 Livewire 已经成功加载了页面,您可能会问自己:"当 Livewire 发起后续请求来更新文章时,can:update,post 中间件会重新应用吗?还是未经授权的用户将能够成功更新文章?"

因为 Livewire 具有从原始端点重新应用中间件的内部机制,所以在这种场景中您是受保护的。

配置持久中间件

默认情况下,Livewire 在网络请求之间持久化以下中间件:

php
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Laravel\Jetstream\Http\Middleware\AuthenticateSession::class,
\Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\RedirectIfAuthenticated::class,
\Illuminate\Auth\Middleware\Authenticate::class,
\Illuminate\Auth\Middleware\Authorize::class,

如果上述任何中间件在初始页面加载时应用,它们将被持久化(重新应用)到任何未来的网络请求中。

但是,如果您在初始页面加载时从应用程序应用自定义中间件,并希望在 Livewire 请求之间持久化它,您需要像这样从应用中的 Service Provider 将其添加到此列表中:

php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Livewire;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Livewire::addPersistentMiddleware([ 
            App\Http\Middleware\EnsureUserHasRole::class,
        ]);
    }
}

如果 Livewire 组件加载在使用您应用程序中 EnsureUserHasRole 中间件的页面上,它现在将被持久化并重新应用到该 Livewire 组件的任何未来网络请求中。

不支持中间件参数

Livewire 目前不支持持久中间件定义的中间件参数。

php
// 错误...
Livewire::addPersistentMiddleware(AuthorizeResource::class.':admin');

// 正确...
Livewire::addPersistentMiddleware(AuthorizeResource::class);

应用全局 Livewire 中间件

或者,如果您希望将特定中间件应用于每个 Livewire 更新网络请求,可以通过注册您自己的 Livewire 更新路由并使用您希望的任何中间件来实现:

php
Livewire::setUpdateRoute(function ($handle) {
    return Route::post('/livewire/update', $handle)
        ->middleware(App\Http\Middleware\LocalizeViewPaths::class);
});

任何对服务器发起的 Livewire AJAX/fetch 请求将使用上述端点,并在处理组件更新之前应用 LocalizeViewPaths 中间件。

安装页面了解更多关于自定义更新路由

快照校验和

在每个 Livewire 请求之间,会对 Livewire 组件进行快照并发送到浏览器。此快照用于在下一次服务器往返时重建组件。

在 Hydration 文档中了解更多关于 Livewire 快照

因为 fetch 请求可以在浏览器中被拦截和篡改,Livewire 会为每个快照生成一个"校验和"并随之一起发送。

然后在下一次网络请求时使用此校验和来验证快照没有以任何方式被更改。

如果 Livewire 发现校验和不匹配,它将抛出 CorruptComponentPayloadException,请求将失败。

这可以防止任何形式的恶意篡改,否则会导致授予用户执行或修改不相关代码的能力。