用户授权
用户授权
简介
除了提供内置的 身份认证 authentication 服务外, Laravel 还提供了一种简单的方法来授权用户对给定资源的操作。例如, 即使用户已经通过认证,他们也可能没有被授权更新或删除某些 Eloquent 模型或应用程序管理的数据库记录。 Laravel 的授权功能提供了一种简单、有序的方式来管理这些类型的授权检查。
Laravel 提供了两种主要的授权操作方式:拦截器 Gates 和策略 Policies。可以把拦截器和策略想象成路由和控制器。拦截器提供了一种基于闭包的简单授权方法,而策略则像控制器一样,围绕特定模型或资源组织逻辑。在本文档中,我们将首先探讨拦截器,然后研究策略。
在构建应用程序时,你不需要在仅使用拦截器和仅使用策略之间做出选择。大多数应用程序很可能会混合使用拦截器和策略,这是完全正常的!拦截器最适用于与任何模型或资源无关的操作,例如查看管理员仪表板。相反,当你希望为特定模型或资源授权某项操作时,应该使用策略。
拦截器 (Gates)
编写拦截器(Gates)
[!注意]
拦截器(Gates)是学习 Laravel 授权特性基础知识的好方法,但在构建健壮的 Laravel 应用时,应该考虑结合使用策略 policies 来组织授权规则。
拦截器(Gates)是用来确定用户是否有权执行给定操作的闭包函数。通常,拦截器(Gates)通过 Gate
门面在 App\Providers\AppServiceProvider
类中的 boot
函数里定义。拦截器(Gates)始终接收一个用户实例作为第一个参数,可以选择性的接收额外参数,例如相关的 Eloquent 模型。
在下面的例子中,我们将定义一个拦截器(Gates)来确定用户是否可以更新给定的 App\Models\Post
模型。拦截器(Gates)将通过比较用户的 id
与创建帖子的用户的 user_id
来实现该判断:
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)也可以使用类回调数组来定义::
use App\Policies\PostPolicy;
use Illuminate\Support\Facades\Gate;
/**
* 启动任何应用程序服务.
*/
public function boot(): void
{
Gate::define('update-post', [PostPolicy::class, 'update']);
}
授权操作
要使用拦截器(Gates)授权操作,你应该使用 Gate
门面提供的 allows
或 denies
方法。注意,你不需要将当前认证的用户传递给这些方法,Laravel 会自动处理将用户传递给拦截器(gate)闭包。通常,在应用程序的控制器中,需要授权操作之前调用门面授权方法:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
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
门面的 forUser
方法:
if (Gate::forUser($user)->allows('update-post', $post)) {
// 该用户可以更新帖子...
}
if (Gate::forUser($user)->denies('update-post', $post)) {
// 该用户无法更新帖子...
}
你可以使用 any
或 none
方法一次授权多个操作:
if (Gate::any(['update-post', 'delete-post'], $post)) {
// 该用户可以更新或删除帖子...
}
if (Gate::none(['update-post', 'delete-post'], $post)) {
// 该用户不能更新或删除帖子...
}
授权或抛出异常
如果你想要在尝试授权一个操作,但该用户不允许执行该操作时自动抛出 Illuminate\Auth\Access\AuthorizationException
异常,可以使用 Gate
门面的 authorize
方法。Laravel 会自动将 AuthorizationException
实例转换为 403 HTTP 响应:
Gate::authorize('update-post', $post);
// 该操作已授权...
提供额外上下文
用于授权能力的拦截器方法(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])) {
// 该用户可以创建帖子...
}
拦截器响应
到目前为止,我们只考察了返回简单布尔值的拦截器。然而,有时你可能希望返回一个更详细的响应,包括错误信息。为此,你可以从你的拦截器返回一个 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('您必须是管理员。');
});
即使你从拦截器返回一个授权响应,Gate::allows
方法仍然会返回一个简单的布尔值;然而,你可以使用 Gate::inspect
方法来获取拦截器返回的完整授权响应:
$response = Gate::inspect('edit-settings');
if ($response->allowed()) {
// 该操作已授权...
} else {
echo $response->message();
}
当使用 Gate::authorize
方法时,如果操作未被授权,则会抛出 AuthorizationException
异常,授权响应提供的错误信息将传播到 HTTP 响应:
Gate::authorize('edit-settings');
// 该操作已授权...
自定义 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
闭包返回一个非 null 结果,该结果将被视为授权检查的结果。
你可以使用 after
方法来定义一个在所有其他授权检查之后执行的闭包:
use App\Models\User;
Gate::after(function (User $user, string $ability, bool|null $result, mixed $arguments) {
if ($user->isAdministrator()) {
return true;
}
});
与 before
方法类似,如果 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 PostPolicy
make: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);
}
编写策略
策略方法
一旦策略类被注册,你可以为每个它授权的操作添加方法。例如,让我们在 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('您不拥有该篇文章。');
}
当从策略返回一个授权响应时,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 请求不是由认证用户发起的,所有拦截器和策略会自动返回 false
。然而,你可以通过声明一个「可选」类型提示或在用户参数定义中提供一个 null
默认值,来允许这些授权检查传递给你的拦截器和策略:
<?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
方法。
使用策略授权操作
通过用户模型
Laravel 应用程序中包含的 App\Models\User
模型提供了两个有用的方法来授权操作:can
和 cannot
。can
和 cannot
方法接收你想要授权的操作名称和相关模型。例如,让我们确定用户是否有权更新给定的 App\Models\Post
模型。通常,这将在控制器方法中完成:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
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
方法将尝试调用与给定操作名称匹配的基于闭包的拦截器。
不需要模型的操作
请记住,某些操作可能对应于策略方法不需要模型实例,如 create
。在这些情况下,你可以将类名传递给 can
方法,类名将用于确定授权操作时要使用的策略:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
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\Http\Controllers\Controller;
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 包含一个中间件,可以在传入请求到达您的路由或控制器之前授权操作。默认情况下,Illuminate\Auth\Middleware\Authorize
中间件可以通过 can
中间件别名附加到路由,该别名由 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');
不需要模型的操作
同样,某些策略方法(如 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
虽然授权必须总是在服务器上处理,但提供前端应用程序授权数据以正确渲染应用程序的 UI 通常很方便。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
{
// ...
/**
* 定义默认情况下共享的属性。
*
* @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),
],
],
],
];
}
}
原文地址:cndocs/11.x/au...
译文地址:cndocs/11.x/au...