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