安全性
确保你的 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 策略
php artisan make:policy PostPolicy --model=Post
在运行上述命令后,将在 app/Policies/PostPolicy.php
中创建一个新的策略。然后,我们可以使用 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); $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>
如你所见,我们没有将 $postId
作为参数从 wire:click
传递给 delete
方法,而是将其存储为 Livewire 组件上的公共属性。
这种方法的问题在于,任何恶意用户都可以向页面注入自定义元素,例如
<input type="text" wire:model="postId">
这将允许他们在按下“删除帖子”之前自由修改 $postId
。由于 delete
操作不会授权 $postId
的值,因此用户现在可以删除数据库中的任何帖子,无论他们是否拥有该帖子。
为了防止这种风险,有两种可能的解决方案
使用模型属性
在设置公共属性时,Livewire 将模型视为与字符串和整数等普通值不同的对象。因此,如果我们改为将整个帖子模型存储为组件上的属性,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 属性的属性仍可以在后端更改,因此仍需注意不要将不受信任的用户输入传递给 Livewire 函数中的属性。
<?php use App\Models\Post;use Livewire\Component;use Livewire\Attributes\Locked; class ShowPost extends Component{ #[Locked] 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); $post->delete(); }}
<button wire:click="delete">Delete Post</button>
现在,即使恶意用户仍然可以自由修改 $postId
的值,当调用 delete
动作时,如果用户不拥有该帖子,$this->authorize()
将抛出 AuthorizationException
。
进一步阅读
中间件
当 Livewire 组件加载在包含路由级别授权中间件的页面上时,就像这样
Route::get('/post/{post}', App\Livewire\UpdatePost::class) ->middleware('can:update,post');
Livewire 将确保这些中间件重新应用于后续的 Livewire 网络请求。在 Livewire 的核心代码中,这被称为“持久中间件”。
持久中间件可以保护你免受在初始页面加载后授权规则或用户权限发生更改的情况。
以下是此类场景的更深入示例
Route::get('/post/{post}', App\Livewire\UpdatePost::class) ->middleware('can:update,post');
<?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([ App\Http\Middleware\EnsureUserHasRole::class, ]); }}
如果 Livewire 组件加载在使用应用程序中的 EnsureUserHasRole
中间件的页面上,它现在将被保留并重新应用于对该 Livewire 组件的任何未来的网络请求。
Livewire 目前不支持持久中间件定义的中间件参数。
// Bad...Livewire::addPersistentMiddleware(AuthorizeResource::class.':admin'); // Good...Livewire::addPersistentMiddleware(AuthorizeResource::class);
应用全局 Livewire 中间件
或者,如果你希望将特定中间件应用于每个 Livewire 更新网络请求,你可以通过使用你希望的任何中间件注册你自己的 Livewire 更新路由来实现
Livewire::setUpdateRoute(function ($handle) { return Route::post('/livewire/update', $handle) ->middleware(App\Http\Middleware\LocalizeViewPaths::class);});
发送到服务器的任何 Livewire AJAX/fetch 请求都将使用上述端点并在处理组件更新之前应用 LocalizeViewPaths
中间件。
快照校验和
在每个 Livewire 请求之间,会对 Livewire 组件进行快照并将其发送到浏览器。此快照用于在下一次服务器往返期间重新构建组件。
在 Hydration 文档中了解有关 Livewire 快照的更多信息。
由于浏览器可以拦截和篡改获取请求,Livewire 会为每个快照生成一个“校验和”。
然后在下一个网络请求中使用此校验和来验证快照是否以任何方式更改。
如果 Livewire 发现校验和不匹配,它将抛出 CorruptComponentPayloadException
并且请求将失败。
这可以防止任何形式的恶意篡改,否则会导致授予用户执行或修改不相关代码的能力。