简介
除了提供内置的身份验证服务,Laravel还提供了一种简单的方法来对给定资源的用户操作进行授权。例如,即使用户已通过身份验证,他们也可能未被授权更新或删除应用程序管理的某些Eloquent模型或数据库记录。Laravel的授权功能提供了一种简单、有条理的方式来管理这些类型的授权检查。
Laravel提供了两种主要的授权操作方式:门(gates)和策略(policies)。可以将门和策略看作是路由和控制器。门提供了一种基于闭包的简单授权方式,而策略则像控制器一样,围绕特定模型或资源对逻辑进行分组。在本文档中,我们将先探讨门,再研究策略。
在构建应用程序时,无需在仅使用门或仅使用策略之间做出选择。大多数应用程序很可能包含门和策略的混合使用,这完全没问题!门最适合用于与任何模型或资源无关的操作,例如查看管理员仪表板。相比之下,当您希望为特定模型或资源授权某项操作时,应使用策略。
门(Gates)
编写门
门是学习Laravel授权功能基础的好方法;然而,在构建健壮的Laravel应用程序时,您应考虑使用策略来组织授权规则。
门本质上是用于判断用户是否有权执行给定操作的闭包。通常,门是在App\Providers\AppServiceProvider类的boot方法中使用Gate外观定义的。门始终将用户实例作为第一个参数接收,并且可以选择接收其他参数,例如相关的Eloquent模型。
在此示例中,我们将定义一个门,用于判断用户是否可以更新给定的App\Models\Post模型。该门将通过比较用户的id与创建该帖子的用户的user_id来实现这一点:
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Gate::define('update-post', function (User $user, Post $post) {
return $user->id === $post->user_id;
});
}与控制器类似,门也可以使用类回调数组来定义:
use App\Policies\PostPolicy;
use Illuminate\Support\Facades\Gate;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Gate::define('update-post', [PostPolicy::class, 'update']);
}授权操作
要使用门来授权操作,您应使用Gate外观提供的allows或denies方法。请注意,您无需将当前经过身份验证的用户传递给这些方法。Laravel会自动负责将用户传入门闭包。通常的做法是在执行需要授权的操作之前,在应用程序的控制器中调用门授权方法:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class PostController extends Controller
{
/**
* Update the given post.
*/
public function update(Request $request, Post $post): RedirectResponse
{
if (! Gate::allows('update-post', $post)) {
abort(403);
}
// Update the post...
return redirect('/posts');
}
}如果您想确定除当前已认证用户之外的其他用户是否有权执行操作,可以使用Gate外观上的forUser方法:
if (Gate::forUser($user)->allows('update-post', $post)) {
// The user can update the post...
}
if (Gate::forUser($user)->denies('update-post', $post)) {
// The user can't update the post...
}您可以使用any或none方法同时授权多个操作:
if (Gate::any(['update-post', 'delete-post'], $post)) {
// The user can update or delete the post...
}
if (Gate::none(['update-post', 'delete-post'], $post)) {
// The user can't update or delete the post...
}授权或抛出异常
如果您想尝试授权操作,并且在用户未被允许执行给定操作时自动抛出Illuminate\Auth\Access\AuthorizationException,可以使用Gate外观的authorize方法。AuthorizationException实例会被Laravel自动转换为403 HTTP响应:
Gate::authorize('update-post', $post);
// The action is authorized...提供额外上下文
用于授权能力的门方法(allows、denies、check、any、none、authorize、can、cannot)以及授权Blade指令(@can、@cannot、@canany)可以接收一个数组作为第二个参数。这些数组元素会作为参数传递给门闭包,可用于在做出授权决策时提供额外上下文:
use App\Models\Category;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::define('create-post', function (User $user, Category $category, bool $pinned) {
if (! $user->canPublishToGroup($category->group)) {
return false;
} elseif ($pinned && ! $user->canPinPosts()) {
return false;
}
return true;
});
if (Gate::check('create-post', [$category, $pinned])) {
// The user can create the post...
}门响应
到目前为止,我们只研究了返回简单布尔值的门。然而,有时您可能希望返回一个更详细的响应,包括错误消息。为此,您可以从门中返回一个Illuminate\Auth\Access\Response:
use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Gate;
Gate::define('edit-settings', function (User $user) {
return $user->isAdmin
? Response::allow()
: Response::deny('You must be an administrator.');
});即使您从门中返回了授权响应,Gate::allows方法仍会返回一个简单的布尔值;不过,您可以使用Gate::inspect方法来获取门返回的完整授权响应:
$response = Gate::inspect('edit-settings');
if ($response->allowed()) {
// The action is authorized...
} else {
echo $response->message();
}当使用Gate::authorize方法时,如果操作未被授权,它会抛出AuthorizationException,此时授权响应提供的错误消息将被传播到HTTP响应中:
Gate::authorize('edit-settings');
// The action is authorized...自定义HTTP响应状态
当操作通过门被拒绝时,会返回一个403 HTTP响应;然而,有时返回其他的HTTP状态码会很有用。您可以使用Illuminate\Auth\Access\Response类上的denyWithStatus静态构造函数来自定义授权检查失败时返回的HTTP状态码:
use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Gate;
Gate::define('edit-settings', function (User $user) {
return $user->isAdmin
? Response::allow()
: Response::denyWithStatus(404);
});由于通过404响应隐藏资源是Web应用程序中一种非常常见的模式,因此为了方便起见,提供了denyAsNotFound方法:
use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Gate;
Gate::define('edit-settings', function (User $user) {
return $user->isAdmin
? Response::allow()
: Response::denyAsNotFound();
});拦截门检查
有时,您可能希望授予特定用户所有权限。您可以使用before方法定义一个在所有其他授权检查之前运行的闭包:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::before(function (User $user, string $ability) {
if ($user->isAdministrator()) {
return true;
}
});如果before闭包返回非空结果,那么该结果将被视为授权检查的结果。
您可以使用after方法定义一个在所有其他授权检查之后执行的闭包:
use App\Models\User;
Gate::after(function (User $user, string $ability, bool|null $result, mixed $arguments) {
if ($user->isAdministrator()) {
return true;
}
});after 闭包返回的值不会覆盖授权检查的结果,除非门或策略返回了 null。
内联授权
偶尔,你可能希望确定当前已认证用户是否有权执行某个操作,而无需编写与该操作对应的专用门。Laravel 允许你通过 Gate::allowIf 和 Gate::denyIf 方法来执行这类“内联”授权检查。内联授权不会执行任何已定义的“before”或“after”授权钩子:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::allowIf(fn (User $user) => $user->isAdministrator());
Gate::denyIf(fn (User $user) => $user->banned());如果操作未获授权或者当前没有已认证用户,Laravel 会自动抛出 Illuminate\Auth\Access\AuthorizationException 异常。AuthorizationException 实例会被 Laravel 的异常处理器自动转换为 403 HTTP 响应。
创建策略
生成策略
策略是围绕特定模型或资源组织授权逻辑的类。例如,如果你的应用是一个博客,你可能有一个 App\Models\Post 模型,以及对应的 App\Policies\PostPolicy 来授权创建或更新文章等用户操作。
你可以使用 make:policy Artisan 命令生成策略。生成的策略将被放置在 app/Policies 目录中。如果你的应用中不存在该目录,Laravel 会为你创建:
php artisan make:policy PostPolicymake:policy 命令会生成一个空的策略类。如果你希望生成一个包含与查看、创建、更新和删除资源相关的示例策略方法的类,可以在执行命令时提供 --model 选项:
php artisan make:policy PostPolicy --model=Post注册策略
策略发现
默认情况下,只要模型和策略遵循 Laravel 的标准命名规范,Laravel 就会自动发现策略。具体来说,策略必须位于包含模型的目录或其上级目录的 Policies 目录中。因此,例如,模型可以放在 app/Models 目录中,而策略可以放在 app/Policies 目录中。在这种情况下,Laravel 会先在 app/Models/Policies 中查找策略,然后在 app/Policies 中查找。此外,策略名称必须与模型名称匹配,并带有 Policy 后缀。因此,User 模型会对应 UserPolicy 策略类。
如果你希望定义自己的策略发现逻辑,可以使用 Gate::guessPolicyNamesUsing 方法注册自定义策略发现回调。通常,此方法应该在应用的 AppServiceProvider 的 boot 方法中调用:
use Illuminate\Support\Facades\Gate;
Gate::guessPolicyNamesUsing(function (string $modelClass) {
// 返回给定模型的策略类名称……
});手动注册策略
使用 Gate 门面,你可以在应用的 AppServiceProvider 的 boot 方法中手动注册策略及其对应的模型:
use App\Models\Order;
use App\Policies\OrderPolicy;
use Illuminate\Support\Facades\Gate;
/**
* 引导任何应用服务。
*/
public function boot(): void
{
Gate::policy(Order::class, OrderPolicy::class);
}或者,你可以在模型类上放置 UsePolicy 特性,以告知 Laravel 该模型对应的策略:
<?php
namespace App\Models;
use App\Policies\OrderPolicy;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Model;
#[UsePolicy(OrderPolicy::class)]
class Order extends Model
{
//
}编写策略
策略方法
一旦策略类注册完成,你就可以为其授权的每个操作添加方法。例如,让我们在 PostPolicy 中定义一个 update 方法,该方法用于判断给定的 App\Models\User 是否有权更新给定的 App\Models\Post 实例。
update 方法将接收一个 User 和一个 Post 实例作为参数,并且应该返回 true 或 false,以表明该用户是否有权更新给定的 Post。因此,在本例中,我们将验证用户的 id 是否与文章的 user_id 匹配:
<?php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
/**
* 判断给定的文章是否能被该用户更新。
*/
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
}你可以根据策略授权的各种操作的需要,继续在策略中定义其他方法。例如,你可以定义 view 或 delete 方法来授权各种与 Post 相关的操作,但请记住,你可以自由地为策略方法命名。
如果你在使用 Artisan 控制台生成策略时使用了 --model 选项,那么它已经包含了用于 viewAny、view、create、update、delete、restore 和 forceDelete 操作的方法。
所有策略都通过 Laravel 服务容器 进行解析,这样你就可以在策略的构造函数中对所需的任何依赖进行类型提示,以让它们被自动注入。
策略响应
到目前为止,我们只研究了返回简单布尔值的策略方法。然而,有时你可能希望返回一个更详细的响应,包括错误消息。为此,你可以在策略方法中返回一个 Illuminate\Auth\Access\Response 实例:
use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
/**
* 判断给定的文章是否能被该用户更新。
*/
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::deny('You do not own this post.');
}当从策略返回授权响应时,Gate::allows 方法仍然会返回一个简单的布尔值;不过,你可以使用 Gate::inspect 方法获取门返回的完整授权响应:
use Illuminate\Support\Facades\Gate;
$response = Gate::inspect('update', $post);
if ($response->allowed()) {
// 操作已授权……
} else {
echo $response->message();
}当使用 Gate::authorize 方法(若操作未获授权则会抛出 AuthorizationException)时,授权响应提供的错误消息将被传播到 HTTP 响应中:
Gate::authorize('update', $post);
// 操作已授权……自定义 HTTP 响应状态
当某个操作通过策略方法被拒绝时,会返回 403 HTTP 响应;不过,有时返回一个备选的 HTTP 状态码会很有用。你可以使用 Illuminate\Auth\Access\Response 类上的 denyWithStatus 静态构造函数,自定义授权检查失败时返回的 HTTP 状态码:
use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
/**
* 判断给定的文章是否能被该用户更新。
*/
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::denyWithStatus(404);
}由于通过 404 响应隐藏资源是 Web 应用中非常常见的模式,因此为方便起见,提供了 denyAsNotFound 方法:
use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
/**
* 判断给定的文章是否能被该用户更新。
*/
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::denyAsNotFound();
}无模型的方法
某些策略方法只接收当前已认证用户的实例。在授权 create 操作时,这种情况最为常见。例如,如果你在构建一个博客,你可能希望确定用户是否有权创建任何文章。在这些情况下,你的策略方法应该只期望接收一个用户实例:
/**
* 判断给定用户是否能创建文章。
*/
public function create(User $user): bool
{
return $user->role == 'writer';
}访客用户
默认情况下,如果传入的 HTTP 请求不是由已认证的用户发起的,所有 Gate 和策略都会自动返回 false。不过,你可以通过声明“可选”的类型提示,或者为 user 参数定义提供 null 默认值,来允许这些授权检查传递到你的 Gate 和策略:
<?php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
/**
* 判断给定的文章能否被该用户更新。
*/
public function update(?User $user, Post $post): bool
{
return $user?->id === $post->user_id;
}
}策略过滤器
对于某些用户,你可能希望授权给定策略中的所有操作。为此,请在策略中定义一个 before 方法。before 方法会在任何其他策略方法之前执行,让你有机会在实际调用预期的策略方法之前完成授权。这一特性常用于授权应用管理员执行任意操作:
use App\Models\User;
/**
* 执行预授权检查。
*/
public function before(User $user, string $ability): bool|null
{
if ($user->isAdministrator()) {
return true;
}
return null;
}如果你希望拒绝特定类型用户的所有授权检查,可以在 before 方法中返回 false。若返回 null,授权检查会继续交给策略方法处理。
如果策略类中不存在与正在检查的能力名称相匹配的方法,则不会调用该策略类的 before 方法。
使用策略进行操作授权
通过 User 模型
Laravel 应用自带的 App\Models\User 模型包含两个用于授权操作的实用方法:can 和 cannot。can 与 cannot 方法会接收你想要授权的操作名称及相关模型。例如,我们来看看某位用户是否有权更新给定的 App\Models\Post 模型。通常,这一操作会在控制器方法中执行:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PostController extends Controller
{
/**
* 更新给定文章。
*/
public function update(Request $request, Post $post): RedirectResponse
{
if ($request->user()->cannot('update', $post)) {
abort(403);
}
// 更新文章……
return redirect('/posts');
}
}若已为相应模型注册策略,can 方法会自动调用对应策略并返回布尔结果。若该模型没有注册策略,can 方法会尝试调用与给定操作名匹配的基于闭包的 Gate。
不需要模型的操作
请记住,某些操作可能对应诸如 create 这类不需要模型实例的策略方法。在这种情况下,你可以将类名传给 can 方法。该类名会用于确定授权操作时使用哪一个策略:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PostController extends Controller
{
/**
* 创建文章。
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->cannot('create', Post::class)) {
abort(403);
}
// 创建文章……
return redirect('/posts');
}
}通过 Gate 门面
除了 App\Models\User 模型提供的实用方法,你随时可以通过 Gate 门面的 authorize 方法进行操作授权。
与 can 方法类似,该方法接收你想要授权的操作名称和相关模型。如果操作未获授权,authorize 方法会抛出 Illuminate\Auth\Access\AuthorizationException 异常,Laravel 的异常处理器会自动将其转换为状态码为 403 的 HTTP 响应:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class PostController extends Controller
{
/**
* 更新给定的博客文章。
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(Request $request, Post $post): RedirectResponse
{
Gate::authorize('update', $post);
// 当前用户可更新这篇博客文章……
return redirect('/posts');
}
}不需要模型的操作
如前所述,诸如 create 这类策略方法不需要模型实例。在这种情况下,应将类名传给 authorize 方法。该类名会用于确定授权操作时使用哪一个策略:
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
/**
* 创建一篇新的博客文章。
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create(Request $request): RedirectResponse
{
Gate::authorize('create', Post::class);
// 当前用户可创建博客文章……
return redirect('/posts');
}通过中间件
Laravel 内置了一个中间件,可在传入请求抵达你的路由或控制器之前进行操作授权。默认情况下,可以使用 can 中间件别名 将 Illuminate\Auth\Middleware\Authorize 中间件附加到路由上,该别名由 Laravel 自动注册。下面我们来看一个使用 can 中间件授权用户能否更新文章的示例:
use App\Models\Post;
Route::put('/post/{post}', function (Post $post) {
// 当前用户可以更新文章……
})->middleware('can:update,post');在本示例中,我们向 can 中间件传递了两个参数。第一个是我们希望授权的操作名,第二个是要传给策略方法的路由参数。在本例中,由于使用了隐式模型绑定,App\Models\Post 模型会被传给策略方法。若用户未被授权执行该操作,中间件会返回一个状态码为 403 的 HTTP 响应。
为方便起见,你也可以使用 can 方法将 can 中间件附加到路由:
use App\Models\Post;
Route::put('/post/{post}', function (Post $post) {
// 当前用户可以更新文章……
})->can('update', 'post');如果你在使用控制器中间件特性,可以通过 Authorize 特性应用 can 中间件:
use Illuminate\Routing\Attributes\Controllers\Authorize;
#[Authorize('update', 'post')]
public function update(Post $post)
{
// 当前用户可以更新文章……
}不需要模型的操作
同样,某些策略方法(如 create)不需要模型实例。在这种情况下,你可以将类名传给中间件。该类名会用于确定授权操作时使用哪一个策略:
Route::post('/post', function () {
// 当前用户可以创建文章……
})->middleware('can:create,App\Models\Post');在字符串形式的中间件定义里书写完整的类名可能会比较繁琐。因此,你也可以选择使用 can 方法将 can 中间件附加到路由:
use App\Models\Post;
Route::post('/post', function () {
// 当前用户可以创建文章……
})->can('create', Post::class);通过 Blade 模板
编写 Blade 模板时,你可能希望仅当用户被授权执行某项操作时,才显示页面的部分内容。例如,你可能希望仅当用户确实可以更新文章时,才显示博客文章的更新表单。这种场景下,你可以使用 @can 和 @cannot 指令:
@can('update', $post)
<!-- 当前用户可以更新文章…… -->
@elsecan('create', App\Models\Post::class)
<!-- 当前用户可以创建新文章…… -->
@else
<!-- …… -->
@endcan
@cannot('update', $post)
<!-- 当前用户无法更新文章…… -->
@elsecannot('create', App\Models\Post::class)
<!-- 当前用户无法创建新文章…… -->
@endcannot这些指令是编写 @if 和 @unless 语句的便捷快捷方式。上面的 @can 和 @cannot 语句等效于以下语句:
@if (Auth::user()->can('update', $post))
<!-- 当前用户可以更新文章... -->
@endif
@unless (Auth::user()->can('update', $post))
<!-- 当前用户无法更新文章... -->
@endunless您还可以判断用户是否有权执行给定操作数组中的任意操作。要实现此目的,请使用 @canany 指令:
@canany(['update', 'view', 'delete'], $post)
<!-- 当前用户可以更新、查看或删除文章... -->
@elsecanany(['create'], \App\Models\Post::class)
<!-- 当前用户可以创建文章... -->
@endcanany无需模型的操作
与大多数其他授权方法一样,如果操作不需要模型实例,您可以将类名传递给 @can 和 @cannot 指令:
@can('create', App\Models\Post::class)
<!-- 当前用户可以创建文章... -->
@endcan
@cannot('create', App\Models\Post::class)
<!-- 当前用户无法创建文章... -->
@endcannot提供额外上下文
在使用策略进行操作授权时,您可以将数组作为第二个参数传递给各种授权函数和辅助方法。数组的第一个元素将用于确定应调用哪个策略,而数组的其余元素将作为参数传递给策略方法,可在进行授权决策时提供额外上下文。例如,考虑以下包含额外 $category 参数的 PostPolicy 方法定义:
/**
* 确定给定文章能否由用户更新。
*/
public function update(User $user, Post $post, int $category): bool
{
return $user->id === $post->user_id &&
$user->canUpdateCategory($category);
}当尝试判断已认证用户能否更新给定文章时,我们可以这样调用此策略方法:
/**
* 更新给定的博客文章。
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(Request $request, Post $post): RedirectResponse
{
Gate::authorize('update', [$post, $request->category]);
// 当前用户可以更新博客文章...
return redirect('/posts');
}授权与 Inertia
尽管授权始终需要在服务器端处理,但向前端应用提供授权数据通常很方便,以便正确渲染应用程序的界面。Laravel 并未定义向基于 Inertia 的前端暴露授权信息的统一规范。
不过,如果您使用的是 Laravel 基于 Inertia 的入门套件之一,您的应用程序已经包含一个 HandleInertiaRequests 中间件。在该中间件的 share 方法中,您可以返回共享数据,这些数据将提供给应用程序中的所有 Inertia 页面。这些共享数据可以作为一个便捷的位置,用于定义用户的授权信息:
<?php
namespace App\Http\Middleware;
use App\Models\Post;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
// ...
/**
* Define the props that are shared by default.
*
* @return array<string, mixed>
*/
public function share(Request $request)
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user(),
'permissions' => [
'post' => [
'create' => $request->user()->can('create', Post::class),
],
],
],
];
}
}