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)
    {
        // INSECURE!

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

        $post->delete();
    }
}
html
<button wire:click="delete({{ $post->id }})">Delete Post</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
{
    /**
     * Determine if the given post can be deleted by the user.
     */
    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);

    // If the user doesn't own the post,
    // an AuthorizationException will be thrown...
    $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()
    {
        // INSECURE!

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

        $post->delete();
    }
}
html
<button wire:click="delete">Delete Post</button>

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

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

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

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

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

使用模型属性

设置公共属性时,Livewire 对模型的处理方式与字符串和整数等纯值不同。因此,如果我们将整个帖子模型作为组件的属性存储,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">Delete Post</button>

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

锁定属性

防止属性被设置为不需要的值的另一种方法是使用锁定属性。通过应用 #[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">Delete Post</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 请求之间持久化,则需要从应用程序中的服务提供者将其添加到此列表,如下所示:

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([ // [tl! highlight:2]
            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 组件进行快照并将其发送到浏览器。此快照用于在下一次服务器往返期间重新构建组件。

在水合文档中了解有关 Livewire 快照的更多信息。

由于获取请求可以在浏览器中被拦截和篡改,Livewire 为每个快照生成一个"校验和"以配合使用。

然后,此校验和在下一个网络请求中用于验证快照没有以任何方式更改。

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

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