安全性
确保你的 Livewire 应用安全、不暴露任何应用漏洞非常重要。Livewire 有内部安全特性来处理许多情况,但有时仍需由你的应用代码来保障组件安全。
授权操作参数
Livewire 操作非常强大,但传给 Livewire 操作的任何参数在客户端都是可变的,应视为不可信的用户输入。
可以说,Livewire 中最常见的安全陷阱是:在将变更持久化到数据库之前,未能验证和授权 Livewire 操作调用。
下面是一个因缺乏授权而导致不安全的示例:
<?php
use App\Models\Post;
use Livewire\Component;
class ShowPost extends Component
{
// ...
public function delete($id)
{
// INSECURE!
$post = Post::find($id);
$post->delete();
}
}<button wire:click="delete({{ $post->id }})">Delete Post</button>上述示例不安全的原因是:wire:click="delete(...)" 可在浏览器中被修改,以传入恶意用户想要的任意文章 ID。
操作参数(如此处的 $id)应与来自浏览器的任何不可信输入同等对待。
因此,为保障应用安全并防止用户删除他人的文章,我们必须为 delete() 操作添加授权。
首先,运行以下命令为 Post 模型创建 Laravel Policy:
php artisan make:policy PostPolicy --model=Post运行上述命令后,会在 app/Policies/PostPolicy.php 中创建新的 Policy。然后我们可以用如下 delete 方法更新其内容:
<?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() 方法,确保用户拥有该文章后再删除:
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
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();
}
}<button wire:click="delete">Delete Post</button>如你所见,我们没有通过 wire:click 将 $postId 作为参数传给 delete 方法,而是将其存储为 Livewire 组件上的公共属性。
这种方法的问题是,任何恶意用户都可以向页面注入自定义元素,例如:
<input type="text" wire:model="postId">这允许他们在按下「Delete Post」之前自由修改 $postId。由于 delete 操作没有对 $postId 的值进行授权,用户现在可以删除数据库中的任意文章,无论是否拥有。
为防范此风险,有两种可行方案:
使用模型属性
设置公共属性时,Livewire 对模型的处理方式不同于字符串和整数等普通值。因此,若我们将整个 post 模型存储为组件属性,Livewire 会确保 ID 永远不会被篡改。
下面是存储 $post 属性而非简单 $postId 属性的示例:
<?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();
}
}<button wire:click="delete">Delete Post</button>该组件现在是安全的,因为恶意用户无法将 $post 属性更改为不同的 Eloquent 模型。
锁定属性
另一种防止属性被设为不期望值的方法是使用 #[Locked] 属性。通过应用 #[Locked] 属性即可锁定属性。若用户试图篡改该值,将抛出错误。
请注意,带有 Locked 属性的属性仍可在后端更改,因此仍需注意不要在自己的 Livewire 函数中将不可信的用户输入传给该属性。
<?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
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();
}
}<button wire:click="delete">Delete Post</button>现在,即使恶意用户仍可自由修改 $postId 的值,当调用 delete 操作时,若用户不拥有该文章,$this->authorize() 会抛出 AuthorizationException。
延伸阅读:
中间件
当 Livewire 组件加载在包含路由级 授权中间件 的页面上时,例如:
Route::livewire('/post/{post}', App\Livewire\UpdatePost::class)
->middleware('can:update,post'); // [tl! highlight]Livewire 会确保这些中间件被重新应用到后续的 Livewire 网络请求。这在 Livewire 核心中称为「持久中间件」(Persistent Middleware)。
持久中间件可保护你免受初始页面加载后授权规则或用户权限发生变化的场景影响。
下面是此类场景的更深入示例:
Route::livewire('/post/{post}', App\Livewire\UpdatePost::class)
->middleware('can:update,post'); // [tl! highlight]<?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 会在网络请求间持久化以下中间件:
\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
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 组件的任何后续网络请求。
WARNING
不支持中间件参数
Livewire 目前不支持持久中间件定义中的中间件参数。
// Bad...
Livewire::addPersistentMiddleware(AuthorizeResource::class.':admin');
// Good...
Livewire::addPersistentMiddleware(AuthorizeResource::class);应用全局 Livewire 中间件
或者,若你希望将特定中间件应用到每一个 Livewire 更新网络请求,可以通过注册自己的 Livewire 更新路由并附带任意中间件来实现:
Livewire::setUpdateRoute(function ($handle, $path) {
return Route::post($path, $handle)
->middleware(App\Http\Middleware\LocalizeViewPaths::class);
});发往服务器的任何 Livewire AJAX/fetch 请求都将使用上述端点,并在处理组件更新前应用 LocalizeViewPaths 中间件。
了解更多关于 在安装页面自定义更新路由。
快照校验和
在每次 Livewire 请求之间,会对 Livewire 组件拍摄快照并发送到浏览器。该快照用于在下一次服务器往返期间重建组件。
因为 fetch 请求可在浏览器中被拦截和篡改,Livewire 会为每个快照生成一个「校验和」(checksum)一并发送。
该校验和随后在下一次网络请求中用于验证快照没有任何变更。
若 Livewire 发现校验和不匹配,将抛出 CorruptComponentPayloadException,请求将失败。
这可防范任何形式的恶意篡改,否则可能导致用户能够执行或修改无关代码。