授权


简介

除了提供内置的身份认证服务之外,Laravel 还提供了一种简单的方法来对给定资源的用户操作进行授权。例如,即使用户已经通过身份验证,他们可能仍然没有权限更新或删除应用程序管理的某些 Eloquent 模型或数据库记录。Laravel 的授权功能提供了一种简单、有组织的方式来管理这些授权检查类型。

Laravel 提供了两种主要的授权方式:策略。可以将门和策略理解为路由和控制器。门提供了一种简单的闭包式的授权方法,而策略则像控制器一样将逻辑围绕特定的模型或资源进行分组。在本文档中,我们首先将探讨门,然后再研究策略。

在构建应用程序时,您不需要在门和策略之间进行选择性使用。大多数应用程序很可能会包含一些门和策略的混合,这是完全可以的!门最适用于与任何模型或资源无关的操作,例如查看管理员仪表板。相比之下,策略应该用于希望授权特定模型或资源的操作。

编写门

门是学习 Laravel 授权功能基础的好方法;但在构建强大的 Laravel 应用程序时,应考虑使用策略来组织您的授权规则。

门只是决定用户是否有权限执行给定操作的闭包。通常情况下,门在 App\Providers\AuthServiceProvider 类的 boot 方法中使用 Gate 门面进行定义。门总是接收一个用户实例作为第一个参数,并可以选择接收其他参数,例如相关的 Eloquent 模型。

在这个例子中,我们将定义一个门来确定用户是否可以更新给定的 App\Models\Post 模型。通过比较用户的 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;
    });
}

与控制器一样,门也可以使用类回调数组来定义:

use App\Policies\PostPolicy;
use Illuminate\Support\Facades\Gate;

/**
 * 注册任何身份验证/授权服务。
 */
public function boot(): void
{
	Gate::define('update-post', [PostPolicy::class, 'update']);
}

授权操作

要使用门授权操作,应该使用 Gate 门面提供的 allowsdenies 方法。请注意,您不需要将当前已验证的用户传递给这些方法。Laravel 将自动处理将用户传递给门闭包的工作。通常情况下,在应用程序的控制器中执行需要授权的操作之前,调用门授权方法是很常见的:

<?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)) {
    // 用户无法更新帖子...
}

您可以使用 anynone 方法一次授权多个操作:

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);

// 操作已授权...

提供额外的上下文

用于授权能力 (allowsdeniescheckanynoneauthorizecancannot) 和授权 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('You must be an administrator.');
});

即使您从门中返回了授权响应,Gate::allows 方法仍将返回一个简单的布尔值;但是,您可以使用 Gate::inspect 方法来获得门返回的完整授权响应:

$response = Gate::inspect('edit-settings');

if ($response->allowed()) {
    // 操作已授权...
} else {
    echo $response->message();
}

使用 Gate::authorize 方法时,如果操作未经授权,授权响应提供的错误消息将传播到 HTTP 响应:

Gate::authorize('edit-settings');

// 操作已授权...

自定义 HTTP 响应状态

当通过 Gate 拒绝操作时,将返回一个 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;
    }
});

类似于 before 方法,如果 after 闭包返回一个非空结果,则该结果将被视为授权检查的结果。

内联授权

有时,您可能希望确定当前经过身份验证的用户是否有权限执行某个操作,而无需编写与该操作对应的专门门。Laravel 允许您通过 Gate::allowIfGate::denyIf 方法执行这些类型的“内联”授权检查:

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 的实例会自动转换为 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 应用程序中包含的 App\Providers\AuthServiceProvider 包含一个 policies 属性,将您的 Eloquent 模型映射到其对应的策略。注册策略将指示 Laravel 在授权特定的 Eloquent 模型操作时使用哪个策略:

<?php

namespace App\Providers;

use App\Models\Post;
use App\Policies\PostPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * 存放应用程序策略的映射。
     *
     * @var array
     */
    protected $policies = [
        Post::class => PostPolicy::class,
    ];

    /**
     * 注册任何应用程序身份验证/授权服务。
     */
    public function boot(): void
    {
        // ...
    }

}

策略自动发现

除了手动注册模型策略之外,Laravel 还可以自动发现策略,只要模型和策略遵循标准的 Laravel 命名约定。具体来说,策略必须位于一个包含模型的目录的上级目录或同级目录中。例如,模型可以放置在 app/Models 目录中,而策略可以放置在 app/Policies 目录中。在这种情况下,Laravel 将检查 app/Models/Policies 然后是 app/Policies 中的策略。此外,策略名称必须与模型名称匹配,并以 Policy 作为后缀。因此,User 模型将对应于 UserPolicy 策略类。

如果您想定义自己的策略发现逻辑,可以使用 Gate::guessPolicyNamesUsing 方法注册自定义的策略发现回调。通常情况下,这个方法应该在应用程序的 AuthServiceProviderboot 方法中调用:

use Illuminate\Support\Facades\Gate;

Gate::guessPolicyNamesUsing(function (string $modelClass) {
    // 返回给定模型的策略类的名称...
});

AuthServiceProvider 中显式映射的任何策略都会优先于可能自动发现的策略。

编写策略

策略方法

一旦策略类被注册,就可以为每个授权操作添加相应的方法。例如,让我们在我们的 PostPolicy 中定义一个 update 方法,该方法确定给定的 App\Models\User 是否可以更新给定的 App\Models\Post 实例。

update 方法将接收一个 User 实例和一个 Post 实例作为参数,应返回一个布尔值,指示用户是否有权限更新给定的 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;
    }
}

您可以根据需要为策略定义其他方法,以授权它授权的各种操作。例如,您可以定义 viewdelete 方法来授权各种与帖子相关的操作。但请记住,您可以自由地给策略方法任何名称。

如果在使用 Artisan 命令行通过 --model 选项生成策略时,策略已包含了与查看、创建、更新和删除资源相关的示例策略方法。

所有策略都是通过 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 方法时,如果操作未经授权,授权响应提供的错误消息将传播到 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();
}	

没有模型的方法

某些策略方法只接收当前经过身份验证的用户的实例。这种情况最常见的是用于授权创建操作。例如,如果您在创建博客,您可能希望确定用户是否有权限创建任何帖子。在这种情况下,您的策略方法只应该期望接收一个用户实例:

/**
 * 确定给定的用户是否可以创建帖子。
 */
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 模型包括两个有用的授权操作方法:cancannotcancannot 方法接收您希望授权的操作名称和相关模型。例如,让我们确定用户是否被授权更新给定的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 方法将尝试调用与给定操作名称匹配的基于闭包的 Gate。

不需要模型的操作

请记住,一些操作可能对应于不需要模型实例的策略方法,比如创建操作。在这些情况下,您可以将类名传递给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');
    }

}

通过控制器助手

除了为 App\Models\User 模型提供的有用方法外,Laravel 还为扩展了 App\Http\Controllers\Controller 基类的所有控制器提供了一个有用的 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;

class PostController extends Controller
{
    /**
     * 更新给定的博客帖子。
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function update(Request $request, Post $post): RedirectResponse
    {
        $this->authorize('update', $post);

        // 当前用户可以更新博客帖子...
     
        return redirect('/posts');
    }

}

不需要模型的操作

正如前面讨论的那样,一些策略方法(例如 create)不需要模型实例。在这些情况下,您应该将类名传递给authorize 方法。用于在授权操作时确定使用哪个策略:

use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

/**
 * 创建新博客帖子。
 *
 * @throws \Illuminate\Auth\Access\AuthorizationException
 */
public function create(Request $request): RedirectResponse
{
    $this->authorize('create', Post::class);

    // 当前用户可以创建博客帖子...

    return redirect('/posts');
}

授权资源控制器

如果您正在使用资源控制器,可以在控制器的构造函数中使用 authorizeResource 方法。此方法将相应的 can 中间件定义附加到资源控制器的方法上。

authorizeResource 方法接受模型的类名作为第一个参数,以及将包含模型 ID 的路由/请求参数的名称作为第二个参数。您应该确保使用 --model 标志创建资源控制器,以便具有所需的方法签名和类型提示:

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\Post;

class PostController extends Controller
{
    /**
     * 创建控制器实例。
     */
    public function __construct()
    {
        $this->authorizeResource(Post::class, 'post');
    }
}

以下控制器方法将映射到相应的策略方法。当请求路由到给定的控制器方法时,将在控制器方法执行之前自动调用相应的策略方法:

控制器方法 策略方法
index viewAny
show view
create create
store create
edit update
update update
destroy delete

您可以使用 make:policy 命令和 --model 选项快速为给定模型生成策略类:php artisan make:policy PostPolicy --model=Post

通过中间件

Laravel 包含一个中间件,可以在请求到达路由或控制器之前对操作进行授权。默认情况下,Illuminate\Auth\Middleware\Authorize 中间件在 App\Http\Kernel 类中被赋予 can 键。我们来看一个示例,使用 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');

不需要模型的操作

同样,如果操作不需要模型实例,您可以将类名传递给中间件。类名将用于确定在授权操作时使用哪个策略:

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
{
    $this->authorize('update', [$post, $request->category]);

    // 当前用户可以更新博客帖子...

    return redirect('/posts');
}

点赞 取消点赞 收藏 取消收藏

<< 上一篇: 认证

>> 下一篇: 邮箱验证