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











暂无评论内容