授权
简介
除了提供内置的【身份验证】服务,Laravel 还提供了一种简单的方式来授权用户对特定资源的操作。例如,尽管用户已经通过身份验证,但他们可能没有权限更新或删除由应用程序管理的某些 Eloquent 模型或数据库记录。Laravel 的授权功能提供了一种简便、有序的方式来管理这些类型的授权检查。
Laravel 提供了两种主要的授权方式:【门】(gates)和【策略】(policies)。可以将门和策略类比为路由和控制器。门提供了一种基于闭包的简单授权方法,而策略则像控制器一样,将与特定模型或资源相关的逻辑进行组织。在本文档中,我们首先探讨门(gates),然后再研究策略(policies)。
在构建应用程序时,您不需要在仅使用门或仅使用策略之间做出选择。大多数应用程序很可能会同时使用门和策略,这完全没问题!门主要适用于与任何模型或资源无关的操作,比如查看管理员仪表盘。而策略则应当用于您希望对特定模型或资源授权某个操作的情况。
门(Gates)
编写门(Writing Gates)
门(Gates)是学习 Laravel 授权功能基础的一个很好的方式;然而,在构建健壮的 Laravel 应用程序时,您应该考虑使用策略(【policies】)来组织授权规则。 |
门只是判断用户是否被授权执行某个特定操作的闭包(closures)。通常,门是在 App\Providers\AppServiceProvider
类的 boot
方法中使用 Gate
facade 定义的。门始终接收一个用户实例作为第一个参数,并且可以选择性地接收其它参数,比如相关的 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']);
}
授权操作(Authorizing Actions)
要使用门(gates)授权某个操作,您应该使用 Gate
facade 提供的 allows
或 denies
方法。请注意,您不需要手动将当前认证的用户传递给这些方法,Laravel 会自动处理将用户传递到门的闭包中。通常,您会在应用程序的控制器中调用这些门授权方法,在执行需要授权的操作之前进行授权检查:
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
{
/**
* 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
facade 的 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
facade 的 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...
}
门的响应(Gate Responses)
到目前为止,我们只研究了返回简单布尔值的门(gates)。然而,有时您可能希望返回更详细的响应,包括错误信息。为此,您可以从门中返回一个 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 状态码。您可以使用 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();
});
拦截门检查(Intercepting Gate Checks)
有时,您可能希望为特定用户授予所有权限。您可以使用 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;
}
});
after
闭包返回的值不会覆盖授权检查的结果,除非门或策略返回 null
。
内联授权(Inline Authorization)
有时,您可能希望在不编写对应于某个操作的专门授权门的情况下,判断当前已认证的用户是否有权限执行某个操作。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 响应。
创建策略(Creating Policies)
生成策略(Generating Policies)
策略(Policies)是围绕特定模型或资源组织授权逻辑的类。例如,如果您的应用程序是一个博客,您可能会有一个 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
注册策略(Registering Policies)
策略发现(Policy Discovery)
默认情况下,只要模型和策略遵循标准的 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) {
// 为给定模型返回策略类的名称...
});
编写策略(Writing Policies)
策略方法(Policy Methods)
一旦策略类被注册,您可以为它所授权的每个操作添加方法。例如,假设我们在 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
方法来授权与帖子相关的操作,但请记住,您可以自由地为策略方法命名。
如果您在通过 Artisan 控制台生成策略时使用了 --model
选项,生成的策略类将已经包含 viewAny
、view
、create
、update
、delete
、restore
和 forceDelete
等方法。
所有策略都是通过 Laravel 的服务容器解析的,允许您在策略的构造函数中使用类型提示来自动注入所需的依赖项。 |
策略响应(Policy Responses)
到目前为止,我们只研究了返回简单布尔值的策略方法。然而,有时您可能希望返回更详细的响应,包括错误信息。为此,您可以从策略方法返回一个 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();
}
没有模型的方法(Methods Without Models)
有些策略方法只接收当前已认证用户的实例。这种情况最常见于授权创建操作。例如,如果你正在创建一个博客,可能希望确定用户是否有权限创建任何帖子。在这种情况下,您的策略方法应该只期望接收一个用户实例:
/**
* 确定给定的用户是否可以创建帖子。
*/
public function create(User $user): bool
{
return $user->role == 'writer';
}
访客用户(Guest Users)
默认情况下,如果传入的 HTTP 请求不是由已认证用户发起的,所有的 gates 和 policies 都会自动返回 false
。然而,你可以通过声明一个“可选”类型提示或者为用户参数定义提供 null
默认值,来允许这些授权检查通过 gates 和 policies:
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 Filters)
对于某些用户,你可能希望授权其执行给定策略中的所有操作。为了实现这一点,可以在策略中定义一个 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
,则授权检查将继续传递到策略方法。
如果策略类没有包含与被检查的能力名称匹配的方法,则 |
使用策略授权操作(Authorizing Actions Using Policies)
通过用户模型(Via the User Model)
在你的 Laravel 应用程序中,App\Models\User
模型提供了两个有用的方法来授权操作:can
和 cannot
。这两个方法接收你希望授权的操作名称以及相关的模型。例如,我们可以判断一个用户是否有权限更新给定的 App\Models\Post
模型。通常,这将在控制器方法中完成:
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
方法将尝试调用与给定操作名称匹配的基于闭包的 Gate。
不需要模型的操作
请记住,有些操作可能对应于像 create
这样的策略方法,而这些方法不需要模型实例。在这些情况下,你可以将类名传递给 can
方法。类名将用于确定在授权操作时使用哪个策略:
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');
}
}
通过门(Via the Gate Facade)
除了 App\Models\User
模型提供的有用方法,你还可以通过 Gate
门面中的 authorize
方法来授权操作。
与 can
方法一样,authorize
方法接受你希望授权的操作名称以及相关的模型。如果操作未被授权,authorize
方法将抛出一个 Illuminate\Auth\Access\AuthorizationException
异常,Laravel 的异常处理器会自动将其转换为一个具有 403 状态码的 HTTP 响应:
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');
}
通过中间件(Via Middleware)
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 模板(Via Blade Templates)
在编写 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
提供额外上下文(Supplying Additional Context)
在使用策略授权操作时,您可以将一个数组作为第二个参数传递给各种授权函数和辅助方法。数组中的第一个元素将用于确定应该调用哪个策略,而数组中的其余元素将作为参数传递给策略方法,并在做出授权决策时提供额外的上下文。例如,考虑以下 PostPolicy
方法定义,它包含一个额外的 $category
参数:
/**
* Determine if the given post can be updated by the user.
*/
public function update(User $user, Post $post, int $category): bool
{
return $user->id === $post->user_id &&
$user->canUpdateCategory($category);
}
当尝试确定认证用户是否可以更新给定的帖子时,我们可以这样调用该策略方法:
/**
* Update the given blog post.
*
* @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
{
// ...
/**
* 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),
],
],
],
];
}
}