Laravel Policy与Gate的深度应用:构建复杂资源权限系统与前置/后置授权逻辑

Laravel Policy与Gate的深度应用:构建复杂资源权限系统与前置/后置授权逻辑

大家好,今天我们深入探讨Laravel Policy与Gate在构建复杂资源权限系统中的应用,并着重讲解如何实现前置和后置授权逻辑。

一、权限控制的基础:Policy与Gate的区别与选择

在Laravel中,Policy和Gate都是用于权限控制的工具,但它们的应用场景有所不同。

  • Gate: Gate 通常用于检查用户是否有权执行特定操作,通常与某个模型无关。例如,检查用户是否是管理员,或者是否可以发布文章(不针对特定文章)。Gate可以定义成闭包或者类方法。

  • Policy: Policy 则通常用于检查用户是否有权对特定模型实例执行特定操作。例如,检查用户是否有权更新或删除某个特定的文章。Policy总是与一个模型关联。

简单来说,Gate更适合全局性的权限判断,而Policy更适合模型级别的权限判断。选择哪个,取决于你的需求。如果你的权限控制是基于特定模型的,那么Policy是更好的选择。

特性 Gate Policy
作用域 全局,通常与模型无关 模型实例级别,针对特定模型
应用场景 管理员权限、发布文章权限等 编辑/删除特定文章、查看特定用户资料等
定义方式 闭包或类方法 类方法
模型关联 通常没有 必须关联一个模型

二、Policy的深度应用:构建模型级别的权限控制

假设我们有一个 Article 模型,我们需要控制用户对文章的创建、查看、更新和删除权限。

  1. 生成Policy:

    使用 Artisan 命令生成 ArticlePolicy:

    php artisan make:policy ArticlePolicy --model=Article

    这会在 app/Policies 目录下生成 ArticlePolicy.php 文件。

  2. 注册Policy:

    AuthServiceProviderboot 方法中注册 Article 模型和 ArticlePolicy:

    <?php
    
    namespace AppProviders;
    
    use AppModelsArticle;
    use AppPoliciesArticlePolicy;
    use IlluminateFoundationSupportProvidersAuthServiceProvider as ServiceProvider;
    use IlluminateSupportFacadesGate;
    
    class AuthServiceProvider extends ServiceProvider
    {
        /**
         * The policy mappings for the application.
         *
         * @var array<class-string, class-string>
         */
        protected $policies = [
            Article::class => ArticlePolicy::class,
        ];
    
        /**
         * Register any authentication / authorization services.
         *
         * @return void
         */
        public function boot()
        {
            $this->registerPolicies();
    
            // Implicitly grant "Super Admin" role all permissions.
            // This works in the app by using gate-related methods like:
            // $user->can('do-something');
            Gate::before(function ($user, $ability) {
                return $user->hasRole('Super Admin') ? true : null; //null will continue to other checks
            });
        }
    }
  3. 定义Policy方法:

    ArticlePolicy 中定义对应于不同操作的权限判断方法:

    <?php
    
    namespace AppPolicies;
    
    use AppModelsUser;
    use AppModelsArticle;
    use IlluminateAuthAccessHandlesAuthorization;
    
    class ArticlePolicy
    {
        use HandlesAuthorization;
    
        /**
         * Determine whether the user can view any models.
         *
         * @param  AppModelsUser  $user
         * @return IlluminateAuthAccessResponse|bool
         */
        public function viewAny(User $user)
        {
            // 任何人都可以查看文章列表
            return true;
        }
    
        /**
         * Determine whether the user can view the model.
         *
         * @param  AppModelsUser  $user
         * @param  AppModelsArticle  $article
         * @return IlluminateAuthAccessResponse|bool
         */
        public function view(User $user, Article $article)
        {
            // 只有作者或管理员可以查看文章详情
            return $user->id === $article->user_id || $user->hasRole('Admin');
        }
    
        /**
         * Determine whether the user can create models.
         *
         * @param  AppModelsUser  $user
         * @return IlluminateAuthAccessResponse|bool
         */
        public function create(User $user)
        {
            // 只有登录用户可以创建文章
            return $user != null;
        }
    
        /**
         * Determine whether the user can update the model.
         *
         * @param  AppModelsUser  $user
         * @param  AppModelsArticle  $article
         * @return IlluminateAuthAccessResponse|bool
         */
        public function update(User $user, Article $article)
        {
            // 只有作者可以更新自己的文章
            return $user->id === $article->user_id;
        }
    
        /**
         * Determine whether the user can delete the model.
         *
         * @param  AppModelsUser  $user
         * @param  AppModelsArticle  $article
         * @return IlluminateAuthAccessResponse|bool
         */
        public function delete(User $user, Article $article)
        {
            // 只有作者或管理员可以删除文章
            return $user->id === $article->user_id || $user->hasRole('Admin');
        }
    
        /**
         * Determine whether the user can restore the model.
         *
         * @param  AppModelsUser  $user
         * @param  AppModelsArticle  $article
         * @return IlluminateAuthAccessResponse|bool
         */
        public function restore(User $user, Article $article)
        {
            // 目前不考虑恢复功能
            return false;
        }
    
        /**
         * Determine whether the user can permanently delete the model.
         *
         * @param  AppModelsUser  $user
         * @param  AppModelsArticle  $article
         * @return IlluminateAuthAccessResponse|bool
         */
        public function forceDelete(User $user, Article $article)
        {
            // 目前不考虑强制删除功能
            return false;
        }
    }

    这里,我们定义了 viewAny, view, create, update, delete 等方法,分别对应于文章的查看列表、查看详情、创建、更新和删除操作。每个方法都接收当前用户 $user 和相应的 Article 模型实例 $article 作为参数,并返回一个布尔值表示用户是否有权执行该操作。

  4. 在Controller中使用Policy:

    ArticleController 中,可以使用 authorize 方法来检查用户是否有权执行某个操作:

    <?php
    
    namespace AppHttpControllers;
    
    use AppModelsArticle;
    use IlluminateHttpRequest;
    use IlluminateSupportFacadesGate;
    
    class ArticleController extends Controller
    {
        public function index()
        {
            // 检查用户是否有权查看文章列表
            $this->authorize('viewAny', Article::class); //注意这里是Article::class,因为没有具体的Article实例
    
            $articles = Article::all();
            return view('articles.index', compact('articles'));
        }
    
        public function show(Article $article)
        {
            // 检查用户是否有权查看特定文章
            $this->authorize('view', $article);
    
            return view('articles.show', compact('article'));
        }
    
        public function create()
        {
            // 检查用户是否有权创建文章
            $this->authorize('create', Article::class);
    
            return view('articles.create');
        }
    
        public function store(Request $request)
        {
            // 检查用户是否有权创建文章
            $this->authorize('create', Article::class);
    
            // 创建文章的逻辑
            $article = new Article($request->all());
            $article->user_id = auth()->id();
            $article->save();
    
            return redirect()->route('articles.show', $article);
        }
    
        public function edit(Article $article)
        {
            // 检查用户是否有权更新特定文章
            $this->authorize('update', $article);
    
            return view('articles.edit', compact('article'));
        }
    
        public function update(Request $request, Article $article)
        {
            // 检查用户是否有权更新特定文章
            $this->authorize('update', $article);
    
            // 更新文章的逻辑
            $article->update($request->all());
    
            return redirect()->route('articles.show', $article);
        }
    
        public function destroy(Article $article)
        {
            // 检查用户是否有权删除特定文章
            $this->authorize('delete', $article);
    
            $article->delete();
    
            return redirect()->route('articles.index');
        }
    }

    $this->authorize('view', $article) 会自动调用 ArticlePolicy 中的 view 方法,并将当前用户和 $article 实例作为参数传递。如果用户没有权限,会抛出一个 AuthorizationException 异常。

三、Gate的深度应用:实现全局性的权限控制

假设我们需要控制用户是否可以访问管理后台。

  1. 定义Gate:

    AuthServiceProviderboot 方法中定义 Gate:

    <?php
    
    namespace AppProviders;
    
    use IlluminateFoundationSupportProvidersAuthServiceProvider as ServiceProvider;
    use IlluminateSupportFacadesGate;
    
    class AuthServiceProvider extends ServiceProvider
    {
        /**
         * The policy mappings for the application.
         *
         * @var array<class-string, class-string>
         */
        protected $policies = [
            // 'AppModelsModel' => 'AppPoliciesModelPolicy',
        ];
    
        /**
         * Register any authentication / authorization services.
         *
         * @return void
         */
        public function boot()
        {
            $this->registerPolicies();
    
            Gate::define('access-admin', function ($user) {
                return $user->hasRole('Admin');
            });
        }
    }

    这里,我们定义了一个名为 access-admin 的 Gate,它接收当前用户 $user 作为参数,并判断用户是否拥有 Admin 角色。

  2. 在Controller中使用Gate:

    在需要进行权限判断的地方,可以使用 Gate::allowsGate::denies 方法:

    <?php
    
    namespace AppHttpControllers;
    
    use IlluminateSupportFacadesGate;
    
    class AdminController extends Controller
    {
        public function index()
        {
            if (Gate::allows('access-admin')) {
                return view('admin.index');
            } else {
                abort(403, 'Unauthorized');
            }
        }
    }

    Gate::allows('access-admin') 会自动调用定义的 access-admin Gate,并将当前用户作为参数传递。如果用户有权限,返回 true,否则返回 false

四、前置授权逻辑:在所有权限检查之前执行的逻辑

Laravel 提供了 Gate::before 方法,允许我们在所有权限检查之前执行一些逻辑。这对于实现一些通用的权限规则非常有用,例如,允许超级管理员访问所有资源。

AuthServiceProviderboot 方法中使用 Gate::before:

    <?php

    namespace AppProviders;

    use IlluminateFoundationSupportProvidersAuthServiceProvider as ServiceProvider;
    use IlluminateSupportFacadesGate;

    class AuthServiceProvider extends ServiceProvider
    {
        /**
         * The policy mappings for the application.
         *
         * @var array<class-string, class-string>
         */
        protected $policies = [
            // 'AppModelsModel' => 'AppPoliciesModelPolicy',
        ];

        /**
         * Register any authentication / authorization services.
         *
         * @return void
         */
        public function boot()
        {
            $this->registerPolicies();

            Gate::define('access-admin', function ($user) {
                return $user->hasRole('Admin');
            });

            Gate::before(function ($user, $ability) {
                return $user->hasRole('Super Admin') ? true : null; //null will continue to other checks
            });
        }
    }

Gate::before 接收一个闭包,该闭包接收当前用户 $user 和要检查的权限 $ability 作为参数。如果闭包返回 true,则表示用户拥有该权限,不再进行后续的权限检查。如果返回 false,则表示用户没有该权限,会直接抛出 AuthorizationException 异常。如果返回 null,则表示不确定,继续进行后续的权限检查(例如 Policy 中的检查)。

在这个例子中,如果用户拥有 Super Admin 角色,则 Gate::before 会返回 true,表示用户拥有所有权限,不再进行后续的 Policy 或 Gate 检查。如果用户没有 Super Admin 角色,则返回 null,继续进行后续的权限检查。

五、后置授权逻辑:在所有权限检查之后执行的逻辑

Laravel 也提供了 Gate::after 方法,允许我们在所有权限检查之后执行一些逻辑。这对于实现一些更复杂的权限规则非常有用,例如,在用户拥有某个权限后,记录用户的操作日志。

    <?php

    namespace AppProviders;

    use IlluminateFoundationSupportProvidersAuthServiceProvider as ServiceProvider;
    use IlluminateSupportFacadesGate;

    class AuthServiceProvider extends ServiceProvider
    {
        /**
         * The policy mappings for the application.
         *
         * @var array<class-string, class-string>
         */
        protected $policies = [
            // 'AppModelsModel' => 'AppPoliciesModelPolicy',
        ];

        /**
         * Register any authentication / authorization services.
         *
         * @return void
         */
        public function boot()
        {
            $this->registerPolicies();

            Gate::define('access-admin', function ($user) {
                return $user->hasRole('Admin');
            });

            Gate::before(function ($user, $ability) {
                return $user->hasRole('Super Admin') ? true : null; //null will continue to other checks
            });

            Gate::after(function ($user, $ability, $result, $arguments) {
                if ($result) {
                    // 记录用户操作日志
                    //Log::info('User ' . $user->id . ' performed action ' . $ability . ' on ' . get_class($arguments[0]));
                }
            });
        }
    }

Gate::after 接收一个闭包,该闭包接收当前用户 $user、要检查的权限 $ability、权限检查的结果 $result (true/false) 和传递给权限检查的参数 $arguments 作为参数。在这个例子中,如果用户拥有某个权限 ($resulttrue),则记录用户的操作日志。

六、Blade模板中的权限控制

在 Blade 模板中,可以使用 @can@cannot 指令来进行权限控制:

@can('update', $article)
    <a href="{{ route('articles.edit', $article) }}">编辑</a>
@endcan

@cannot('delete', $article)
    <p>您没有权限删除此文章。</p>
@endcannot

@if (Gate::allows('access-admin'))
    <a href="/admin">进入管理后台</a>
@endif

@can('update', $article) 会自动调用 ArticlePolicy 中的 update 方法,并将当前用户和 $article 实例作为参数传递。如果用户拥有权限,则显示链接。@cannot 指令则相反,如果用户没有权限,则显示内容。

七、使用Policy和Gate进行API权限控制

Policy和Gate同样适用于API的权限控制。在API Controller中,使用authorize方法和Gate::allows方法进行权限验证,如果验证失败,则返回相应的HTTP状态码(例如403 Forbidden)。

<?php

namespace AppHttpControllersApi;

use AppHttpControllersController;
use AppModelsArticle;
use IlluminateHttpRequest;
use IlluminateSupportFacadesGate;

class ArticleApiController extends Controller
{
    public function show(Article $article)
    {
        if (Gate::allows('view', $article)) {
            return response()->json($article);
        } else {
            return response()->json(['message' => 'Unauthorized'], 403);
        }
    }

    public function update(Request $request, Article $article)
    {
        $this->authorize('update', $article);

        $article->update($request->all());

        return response()->json($article);
    }
}

八、实际案例:一个复杂的权限系统

假设我们需要构建一个在线教育平台的权限系统,包括以下角色:

  • 管理员 (Admin): 拥有所有权限。
  • 教师 (Teacher): 可以创建、编辑和发布自己的课程。
  • 学生 (Student): 可以购买和学习课程。

我们需要控制以下资源:

  • 课程 (Course):
    • 查看课程列表
    • 查看课程详情
    • 创建课程
    • 编辑课程
    • 删除课程
    • 发布课程
  • 章节 (Chapter):
    • 创建章节
    • 编辑章节
    • 删除章节
  • 用户 (User):
    • 查看用户列表
    • 查看用户详情
    • 创建用户
    • 编辑用户
    • 删除用户

我们可以使用 Policy 和 Gate 来实现这个权限系统:

  1. 定义角色 (Role):

    创建一个 Role 模型和迁移,用于存储角色信息。

  2. 关联用户和角色:

    User 模型中添加一个 roles 关系,用于获取用户拥有的角色。

  3. 定义Gate:

    定义 Gate 用于检查用户是否拥有特定角色,例如 isAdminisTeacherisStudent

  4. 定义CoursePolicy:

    定义 CoursePolicy 用于控制用户对课程的访问权限:

    • viewAny: 所有人都可以查看课程列表。
    • view: 所有人都可以查看课程详情。
    • create: 只有教师和管理员可以创建课程。
    • update: 只有课程的创建者和管理员可以编辑课程。
    • delete: 只有课程的创建者和管理员可以删除课程。
    • publish: 只有课程的创建者和管理员可以发布课程。
  5. 定义ChapterPolicy:

    定义 ChapterPolicy 用于控制用户对章节的访问权限:

    • create: 只有课程的创建者和管理员可以创建章节。
    • update: 只有章节的创建者和管理员可以编辑章节。
    • delete: 只有章节的创建者和管理员可以删除章节。
  6. 定义UserPolicy:

    定义 UserPolicy 用于控制用户对用户的访问权限:

    • viewAny: 只有管理员可以查看用户列表。
    • view: 只有管理员可以查看用户详情。
    • create: 只有管理员可以创建用户。
    • update: 只有管理员可以编辑用户。
    • delete: 只有管理员可以删除用户。
  7. 使用前置授权逻辑:

    使用 Gate::before 允许管理员拥有所有权限。

  8. 使用后置授权逻辑:

    使用 Gate::after 记录用户的操作日志。

通过这种方式,我们可以构建一个灵活且可扩展的权限系统,可以根据不同的角色和资源进行精细化的权限控制。

九、一些最佳实践

  • 保持Policy方法的简洁: Policy 方法应该只包含权限判断的逻辑,避免包含业务逻辑。
  • 使用Gate::before进行统一的权限处理: 例如,超级管理员的权限处理。
  • 在Controller中进行权限检查: 确保所有需要进行权限控制的操作都经过了权限检查。
  • 在Blade模板中使用@can@cannot指令: 方便地控制UI元素的显示和隐藏。
  • 编写测试用例: 确保权限系统能够正常工作。
  • 不要在Policy中直接使用auth()->user(): 应该使用传入的 $user 参数,这样更容易进行单元测试。
  • 考虑使用第三方权限管理包: 如果你的权限系统非常复杂,可以考虑使用 Spatie 的 Laravel-permission 包,它提供了更丰富的功能,例如角色和权限的管理。

十、总结:灵活运用Policy和Gate,构建健壮的权限体系

Policy和Gate是Laravel提供的强大的权限控制工具。 Policy专注于模型级别的权限管理,而Gate则适用于全局权限的控制。 通过巧妙地结合两者,以及利用前置和后置授权逻辑,我们可以构建出复杂且健壮的权限体系,保障应用的安全性和可靠性。在实际开发中,根据具体的业务需求选择合适的权限控制方案,并遵循最佳实践,才能充分发挥 Policy 和 Gate 的优势。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注