Laravel 授权

Laravel 授权

简介

除了内置身份验证服务,Laravel 还提供了一种简单方式,判断用户是否有权对某个资源执行操作。用户即使已经通过身份验证,也未必有权更新或删除应用管理的某些 Eloquent 模型或数据库记录。Laravel 的授权功能让这些检查易于组织和管理。

Laravel 主要提供两种授权方式:Gate 和策略(Policy)。它们的关系类似路由与控制器:Gate 使用闭包实现简单的授权判断;策略则像控制器一样,围绕某个模型或资源集中组织逻辑。下文先介绍 Gate,再介绍策略。

应用不必在两者之间二选一;大多数应用混合使用两者完全合理。Gate 更适合与具体模型或资源无关的操作,例如查看管理员仪表盘;针对特定模型或资源的操作,则适合使用策略。

Gate

编写 Gate

Gate 是判断用户能否执行指定操作的闭包。通常在 App\Providers\AppServiceProvider 类的 boot 方法中,通过 Gate Facade 定义。Gate 的第一个参数始终是用户实例,还可以接收额外参数,例如相关的 Eloquent 模型。

下面定义一个判断用户能否更新指定 App\Models\Post 的 Gate,通过比较用户的 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;
    });
}

与控制器类似,也可以使用类回调数组定义 Gate:

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 Facade 提供的 allows 或 denies 方法进行授权。无需传入当前已认证用户,Laravel 会自动将用户传给 Gate 闭包。通常应在控制器执行需要授权的操作之前调用:

<?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。Laravel 会自动把该异常转换成 HTTP 403 响应:

Gate::authorize('update-post', $post);

// The action is authorized...

提供额外上下文

Gate 的能力授权方法(allows、denies、check、any、none、authorize、can、cannot)和授权相关 Blade 指令(@can、@cannot、@canany)都可将数组作为第二个参数。数组元素会依次传入 Gate 闭包,为授权判断提供更多上下文:

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 响应

前面的 Gate 只返回布尔值。有时需要包含错误消息等更详细的响应,这时可以返回 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 返回了授权响应,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 响应状态

Gate 拒绝操作时,默认返回 HTTP 403。有时需要返回其他状态码,可使用 Illuminate\Auth\Access\Response 类的静态构造方法 denyWithStatus 自定义:

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

用 HTTP 404 隐藏资源是常见模式,因此 Laravel 还提供了便捷方法 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();
});

拦截 Gate 检查

有时希望授予某个用户所有能力。使用 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 的返回值不会覆盖已经得到的授权结果,除非 Gate 或策略返回的是 null。

内联授权

偶尔需要判断当前用户能否执行某个操作,却不想为它单独定义 Gate。这时可以使用 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;异常处理器会将其转换为 HTTP 403 响应。

创建策略

生成策略

策略是围绕特定模型或资源组织授权逻辑的类。例如博客应用可以有 App\Models\Post 模型,以及相应的 App\Policies\PostPolicy,用于授权创建、更新文章等操作。

使用 Artisan 的 make:policy 命令生成策略。生成的类放在 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 注册回调,通常放在 App\Providers\AppServiceProvider 的 boot 方法中:

use Illuminate\Support\Facades\Gate;

Gate::guessPolicyNamesUsing(function (string $modelClass) {
    // Return the name of the policy class for the given model...
});

手动注册策略

可在 AppServiceProvider::boot 中通过 Gate Facade 手动注册策略与对应模型:

use App\Models\Order;
use App\Policies\OrderPolicy;
use Illuminate\Support\Facades\Gate;

/**
 * Bootstrap any application services.
 */
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。下面检查用户的 id 是否等于文章的 user_id:

<?php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    /**
     * Determine if the given post can be updated by the user.
     */
    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 方法。

策略响应

策略方法也可以返回更详细的响应,例如包含错误消息的 Illuminate\Auth\Access\Response 实例:

use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;

/**
 * Determine if the given post can be updated by the user.
 */
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()) {
    // The action is authorized...
} else {
    echo $response->message();
}

使用授权失败时抛出 AuthorizationException 的 Gate::authorize,授权响应中的错误消息会传递到 HTTP 响应:

Gate::authorize('update', $post);

// The action is authorized...

自定义 HTTP 响应状态

策略拒绝操作时默认返回 HTTP 403。可以使用 Illuminate\Auth\Access\Response::denyWithStatus 返回其他状态码:

use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;

/**
 * Determine if the given post can be updated by the user.
 */
public function update(User $user, Post $post): Response
{
    return $user->id === $post->user_id
        ? Response::allow()
        : Response::denyWithStatus(404);
}

由于使用 HTTP 404 隐藏资源很常见,Laravel 提供了便捷的 denyAsNotFound:

use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;

/**
 * Determine if the given post can be updated by the user.
 */
public function update(User $user, Post $post): Response
{
    return $user->id === $post->user_id
        ? Response::allow()
        : Response::denyAsNotFound();
}

不接收模型的方法

某些策略方法只接收当前已认证用户,最常见的是 create。例如博客应用判断用户能否创建任何文章时,并不存在一个已有文章实例,这类策略方法只需接收用户:

/**
 * Determine if the given user can create posts.
 */
public function create(User $user): bool
{
    return $user->role == 'writer';
}

访客用户

默认情况下,如果 HTTP 请求不是由已认证用户发起,所有 Gate 和策略都会自动返回 false。可以通过将用户参数声明为可空类型,或为它设置 null 默认值,让这类请求继续进入 Gate 或策略:

<?php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    /**
     * Determine if the given post can be updated by the user.
     */
    public function update(?User $user, Post $post): bool
    {
        return $user?->id === $post->user_id;
    }
}

策略过滤器

要允许特定用户执行某策略中的所有操作,可以定义 before 方法。它在其他策略方法之前运行,让你有机会提前授权;典型用途是允许应用管理员执行任何操作:

use App\Models\User;

/**
 * Perform pre-authorization checks.
 */
public function before(User $user, string $ability): bool|null
{
    if ($user->isAdministrator()) {
        return true;
    }

    return null;
}

若要拒绝某类用户的所有操作,从 before 返回 false;返回 null 则会继续调用相应策略方法。

通过策略授权操作

通过用户模型

Laravel 应用中的 App\Models\User 提供 can 和 cannot 两个便捷方法,接收操作名称及相关模型。例如在控制器中判断用户能否更新指定文章:

<?php

namespace App\Http\Controllers;

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

class PostController extends Controller
{
    /**
     * Update the given post.
     */
    public function update(Request $request, Post $post): RedirectResponse
    {
        if ($request->user()->cannot('update', $post)) {
            abort(403);
        }

        // Update the post...

        return redirect('/posts');
    }
}

如果该模型已经注册策略,can 会自动调用对应策略并返回布尔结果;否则,它会尝试调用与操作名称匹配的闭包 Gate。

不需要模型实例的操作

create 等操作不需要模型实例。此时可以向 can 方法传入类名,Laravel 据此选择应使用的策略:

<?php

namespace App\Http\Controllers;

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

class PostController extends Controller
{
    /**
     * Create a post.
     */
    public function store(Request $request): RedirectResponse
    {
        if ($request->user()->cannot('create', Post::class)) {
            abort(403);
        }

        // Create the post...

        return redirect('/posts');
    }
}

通过 Gate Facade

除了用户模型的便捷方法,还可以随时使用 Gate::authorize 进行授权。与 can 一样,它接收操作名称与相关模型。授权失败时抛出 Illuminate\Auth\Access\AuthorizationException,由 Laravel 异常处理器自动转换成 HTTP 403:

<?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 blog post.
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function update(Request $request, Post $post): RedirectResponse
    {
        Gate::authorize('update', $post);

        // The current user can update the blog post...

        return redirect('/posts');
    }
}

不需要模型实例的操作

对于 create 等不需要模型实例的策略方法,应向 authorize 传入类名,以确定授权策略:

use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;

/**
 * Create a new blog post.
 *
 * @throws \Illuminate\Auth\Access\AuthorizationException
 */
public function create(Request $request): RedirectResponse
{
    Gate::authorize('create', Post::class);

    // The current user can create blog posts...

    return redirect('/posts');
}

通过中间件

Laravel 的授权中间件可以在请求到达路由或控制器之前检查操作。Illuminate\Auth\Middleware\Authorize 默认拥有自动注册的 can 中间件别名。例如,检查用户能否更新文章:

use App\Models\Post;

Route::put('/post/{post}', function (Post $post) {
    // The current user may update the post...
})->middleware('can:update,post');

此处向 can 中间件传入两个参数:待授权操作名称,以及传给策略的路由参数。因为使用了隐式模型绑定,策略收到的是 App\Models\Post 实例。用户未获授权时,中间件会返回 HTTP 403。

也可以通过路由上的 can 方法添加该中间件:

use App\Models\Post;

Route::put('/post/{post}', function (Post $post) {
    // The current user may update the post...
})->can('update', 'post');

如果使用控制器中间件属性,可以通过 Authorize 属性应用 can 中间件:

use Illuminate\Routing\Attributes\Controllers\Authorize;

#[Authorize('update', 'post')]
public function update(Post $post)
{
    // The current user may update the post...
}

不需要模型实例的操作

对于 create 等操作,向中间件传入类名,Laravel 将据此选择策略:

Route::post('/post', function () {
    // The current user may create posts...
})->middleware('can:create,App\Models\Post');

在字符串形式的中间件定义中写完整类名较为繁琐,因此也可以通过路由的 can 方法添加:

use App\Models\Post;

Route::post('/post', function () {
    // The current user may create posts...
})->can('create', Post::class);

通过 Blade 模板

编写 Blade 模板时,可以只在用户获准执行操作时显示某部分页面。例如只有用户能更新文章时才显示编辑表单。为此可使用 @can 和 @cannot 指令:

@can('update', $post)
    <!-- The current user can update the post... -->
@elsecan('create', App\Models\Post::class)
    <!-- The current user can create new posts... -->
@else
    <!-- ... -->
@endcan

@cannot('update', $post)
    <!-- The current user cannot update the post... -->
@elsecannot('create', App\Models\Post::class)
    <!-- The current user cannot create new posts... -->
@endcannot

这些指令是 @if 和 @unless 判断的便捷写法。上面的授权条件等价于:

@if (Auth::user()->can('update', $post))
    <!-- The current user can update the post... -->
@endif

@unless (Auth::user()->can('update', $post))
    <!-- The current user cannot update the post... -->
@endunless

使用 @canany 可以判断用户是否获准执行给定数组中的任意操作:

@canany(['update', 'view', 'delete'], $post)
    <!-- The current user can update, view, or delete the post... -->
@elsecanany(['create'], \App\Models\Post::class)
    <!-- The current user can create a post... -->
@endcanany

不需要模型实例的操作

与其他授权方法一样,操作无需模型实例时,可向 @can、@cannot 传入类名:

@can('create', App\Models\Post::class)
    <!-- The current user can create posts... -->
@endcan

@cannot('create', App\Models\Post::class)
    <!-- The current user can't create posts... -->
@endcannot

提供额外上下文

通过策略授权时,可将数组作为授权函数或辅助函数的第二个参数。数组的第一个元素用于确定策略,其余元素会传给策略方法,为决策提供上下文。例如下面的 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]);

    // The current user can update the blog post...

    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),
                    ],
                ],
            ],
        ];
    }
}

原文:Laravel Authorization,对应 Laravel 13.x 文档。Copyright © Taylor Otwell。中文翻译保留原文全部示例,代码未在此执行。原文与附带代码采用 MIT 许可,许可全文如下:

The MIT License (MIT)

Copyright (c) Taylor Otwell

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容