Dcat Admin 使用 Laravel 的拦截器 Gate 和策略 Policy,能否对除 App\Models\User 模型以外的其它认证模型进行授权呢?

运行环境

  • Valet 集成环境

当前使用的 Laravel 版本?

  • Laravel Framework 9.33.0
  • Dcat Admin version 2.2.2-beta

当前使用的 php/php-fpm 版本?

  • PHP 版本:PHP 8.1.20 (cli) (built: Jun 22 2023 06:14:35) (NTS)
  • php-fpm 版本:/usr/local/etc/php/8.1/php-fpm.conf

当前系统

  • MacOs

问题描述?

  • 最近使用 Dcat Admin 后台框架搭建了一个项目:

    后台有通用的 RBAC 管理模块,包括管理员、角色、权限以及角色和权限分配等相关内容。使用还比较很方便,但是最近漏扫发现了一个问题: 普通管理员,通过修改路由链接参数值,能够进入超级管理员信息的 xxx/admin/auth/users/{参数id}/edit 编辑页面,然后进行密码修改获得超管账号
    Dcat Admin Auth

  • 我就联想到 Laravel 中的 GatePolicy 可以帮助我实现这个功能:

    它很安全,即使用户已经通过了「身份验证(authentication)」, 用户也可能无权对应用程序中重要的模型或数据库记录进行删除或更改。简单、条理化的系统性,是 Laravel 对授权管理的特性。
    Dcat Admin 使用 Laravel 的拦截器 Gate 和策略 Policy,能否对除 App\Models\User 模型以外的其它认证模型进行授权呢?

首先我使用 Policy

  • 创建 policy

    php artisan make:policy AdminPolicy --model=Dcat\Admin\Admin
  • AdminPolicy 类内容

    <?php
    namespace App\Policies;
    use App\Models\Admin\AdminUser;
    use Dcat\Admin\Models\Administrator;
    use Dcat\Admin\Models\Role;
    use Illuminate\Auth\Access\HandlesAuthorization;
    use Illuminate\Auth\Access\Response;
    class AdminPolicy
    {
      use HandlesAuthorization;
     /**
      * Determine whether the user can view any models.
      *
      * @param Administrator $user
      * @return Response|bool
      */
     public function viewAny(Administrator $user): Response|bool
     {
         return $user->isAdministrator();
     }
     /**
      * Determine whether the user can view the model.
      *
      * @param Administrator $user
      * @param AdminUser $adminUser
      * @return Response|bool
      */
     public function view(Administrator $user, AdminUser $adminUser): Response|bool
     {
         return intval($user->getAttribute('id')) === intval($adminUser->getAttribute('id'));
     }
     /**
      * Determine whether the user can create models.
      *
      * @param Administrator $user
      * @return Response|bool
      */
     public function create(Administrator $user): Response|bool
     {
         return $user->isRole(Role::ADMINISTRATOR) || $user->isRole('manager');
     }
     /**
      * Determine whether the user can update the model.
      *
      * @param Administrator $user
      * @param AdminUser $adminUser
      * @return Response|bool
      */
     public function update(Administrator $user, AdminUser $adminUser): Response|bool
     {
         return intval($user->getAttribute('id')) === intval($adminUser->getAttribute('id'));
     }
     /**
      * Determine whether the user can delete the model.
      *
      * @param Administrator $user
      * @param AdminUser $adminUser
      * @return Response|bool
      */
     public function delete(Administrator $user, AdminUser $adminUser): Response|bool
     {
         return intval($user->getAttribute('id')) === intval($adminUser->getAttribute('id'));
     }
     /**
      * Determine whether the user can restore the model.
      *
      * @param Administrator $user
      * @param AdminUser $adminUser
      * @return Response|bool
      */
     public function restore(Administrator $user, AdminUser $adminUser): Response|bool
     {
         return intval($user->getAttribute('id')) === intval($adminUser->getAttribute('id'));
     }
     /**
      * Determine whether the user can permanently delete the model.
      *
      * @param Administrator $user
      * @param AdminUser $adminUser
      * @return Response|bool
      */
     public function forceDelete(Administrator $user, AdminUser $adminUser): Response|bool
     {
         return intval($user->getAttribute('id')) === intval($adminUser->getAttribute('id'));
     }
    }
  • AuthServiceProvider 提供者中注册 polic

    protected $policies = [
      AdminUser::class => AdminPolicy::class,
    ];
  • 在控制器中使用

    public function edit($id, Content $content): Content
    {
         $adminUser = AdminUserModel::query()->find($id);
         $response = Gate::inspect('view', $adminUser);
         // 第一种:打印结果第一个为 false ,第二个为  null
         // 无论是自己的信息,还是不是自己的信息,都返回 false
         dd($response->allowed(), $response->message());
    
         // 第二种:直接返回 403 权限验证失败页面
         Gate::authorize('view', AdminUserModel::query()->find($id));
    }

然后我使用 Gate

  • AuthServiceProviderboot 方法注册方法

    Gate::define('admin-view', function (Administrator $user, AdminUser $adminUser) {
             return $user->getAttribute('id') === $adminUser->getAttribute('id');
         });
  • 控制器中验证

    public function edit($id, Content $content): Content
     {
         $adminUser = AdminUserModel::query()->find($id);
         $status = Gate::allows('admin-view', $adminUser);
         // 打印结果一直为 false
         dd($status);
         if (! $status) {
             abort(403);
         }
    
    }

您期望得到的结果?

PolicyGate 能够验证,除 App\Models\User 以外的用户认证模型:比如说后台 App\Models\Admin\AdminUser 模型

您实际得到的结果?

最终验证不了, PolicyGate 方法默认的第一个参数,就是 认证用户,这个认证用户没搞明白是哪个模型的认证用户,所以得不出结果。

Xiao Peng
laravel_peng
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
最佳答案

哈哈,抱歉,我还以为是分享贴。

你这个问题不在 Gate 方法,而在于获取当前 登录 用户时发生的问题,且听我娓娓道来:

Dcat-admin 有自己的授权表和授权模型,并没有和 User 模型混到一起,所以我们首先应该为 Dcat-admin 添加一个 guard

打开 auth.php config 文件

'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'admin' => [
            'driver' => 'session',
            'provider' => 'admin',
        ],
    ],
'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],

        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Admin\Models\Administrator::class // 这里
        ],
    ],

我这里复制了 Dcat-admin 的模型文件到自己的项目,并继承它。

<?php
namespace App\Admin\Models;

use \Dcat\Admin\Models\Administrator as AdminAuth;

class Administrator extends AdminAuth
{
    protected $guarded = 'admin'; // 为这个授权模型指定我们上面定义的 guard
}

然后我们需要修改一下 admin.php 配置文件,130行 auth

'providers' => [
            'admin' => [
                'driver' => 'eloquent',
                'model'  => \App\Admin\Models\Administrator::class,
            ],
        ],

将 Providers 的模型改成我们自己的,还有下面的 283 行 database,更改默认的用户模型

 // User tables and model.
        'users_table' => 'admin_users',
        'users_model' => \App\Admin\Models\Administrator::class,

你上面做的都没问题,注册 Policy,定义 Gate 方法都是正确的。


那么为什么返回 false ?

不论是 Gate::allows 还是 Gate::inspect,他们都需要一个共同的东西,就是 当前授权用户,我们打开

/vendor/laravel/framework/src/Illuminate/Auth/Access/Gate.php
查看 inspect 方法

public function inspect($ability, $arguments = [])
    {
        try {
            $result = $this->raw($ability, $arguments); //注意这行
            if ($result instanceof Response) {
                return $result;
            }
          ...
    }

接着追踪 raw() 方法

public function raw($ability, $arguments = [])
{
        $arguments = Arr::wrap($arguments);
        $user = $this->resolveUser(); //这里打断点
        dd($user); // null 是在这里产生的
}

就如我上面所说,如果 Gate 方法获取不到当前登录用户,那么它肯定是无法判断权限的对吧?

那么 Gate 为什么获取不到当前用户?我们打开 auth.php config 文件第 16 行

    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

这里设置了默认的用户组是 Web,也就是默认的 User 模型,Gate 当然是来找这个模型相关的授权用户,但是 Dcat-admin 有自己独立的一套授权系统,所以就产生了你上面不论怎么写都是返回 null。


到此,问题已经明确,下面我们来修复它:

刚刚我们看到了 resoverUser() 方法是获取当前用户的,那么我们可以在调用 Gate 方法的时候告诉它当前登录用户是谁

public function edit($id, Content $content)
    {
        $adminUser = \App\Admin\Models\Administrator::query()->find($id);
        $response = Gate::forUser(Auth::guard('admin')->user())->allows('admin-view', $adminUser);
        dd($response);
        return parent::edit($id, $content); // TODO: Change the autogenerated stub
    }

Gate::forUser(Auth::guard('admin')->user()) 我们告诉 Gate,不要去默认的 web 授权组去找,而是我们直接指定一个授权用户给它,让它去验证。

希望能解决你的问题哈哈

1年前 评论
laravel_peng (楼主) 1年前
讨论数量: 14
laravel_peng

这篇是问答呀,怎么没人理我,嘤嘤嘤 :see_no_evil:

1年前 评论
DogLoML

后台权限没设置对吧,我这边刚刚测试,你如果不给普通角色auth路由的权限,他进去会显示无权访问

  • 检查admin.php配置中是否启用了权限
  • 检查用户是否有auth权限
  • 检查普通用户的角色是否有auth权限
  • 检查用户的角色是否设置正确

Laravel

Laravel

Laravel

Laravel

Laravel

1年前 评论
laravel_peng (楼主) 1年前
DogLoML (作者) 1年前
laravel_peng (楼主) 1年前

需要满足的要求:

1.用户模型需要实现 Illuminate\Contracts\Auth\AuthenticatableIlluminate\Contracts\Auth\Access\Authorizable 接口。当然直接继承至 Illuminate\Foundation\Auth\User 也可以!

2.在 config/auth.php 配置文件中为该模型配置 guardprovider,如下所示:

<?php

return [
    'defaults' => [
        'guard' => 'users',
        'passwords' => 'users',
    ],

    'guards' => [
        'users' => [
            'driver' => 'jwt',
            'provider' => 'users',
            'hash' => false,
        ],
        'customer' => [
            'driver' => 'jwt',
            'provider' => 'customer',
            'hash' => false,
        ],
    ],
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
        'customer' => [
            'driver' => 'eloquent',
            'model' => App\Models\Customer::class,
        ],
    ],
];
  1. 在路由中间件中定义:
Route::group(['middleware' => 'auth:users,customer'], function () {
});

我这里是 user 和 customer,在你的系统里可能是 Admin 和 SuperAdmin。但是个人建议,如果两种用户数据结构差别不大的话,没必要搞两个模型做隔离,不如通过属性或者角色区分!

再者就是 Laravel 的授权策略很难实现动态管理,如果有类似需求可以看看 Authorization as a Service 的开源项目如 Cerbos 这种,通过 YAML 定义授权策略,并且支持派生角色!

1年前 评论
laravel_peng (楼主) 1年前

哈哈,抱歉,我还以为是分享贴。

你这个问题不在 Gate 方法,而在于获取当前 登录 用户时发生的问题,且听我娓娓道来:

Dcat-admin 有自己的授权表和授权模型,并没有和 User 模型混到一起,所以我们首先应该为 Dcat-admin 添加一个 guard

打开 auth.php config 文件

'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'admin' => [
            'driver' => 'session',
            'provider' => 'admin',
        ],
    ],
'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],

        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Admin\Models\Administrator::class // 这里
        ],
    ],

我这里复制了 Dcat-admin 的模型文件到自己的项目,并继承它。

<?php
namespace App\Admin\Models;

use \Dcat\Admin\Models\Administrator as AdminAuth;

class Administrator extends AdminAuth
{
    protected $guarded = 'admin'; // 为这个授权模型指定我们上面定义的 guard
}

然后我们需要修改一下 admin.php 配置文件,130行 auth

'providers' => [
            'admin' => [
                'driver' => 'eloquent',
                'model'  => \App\Admin\Models\Administrator::class,
            ],
        ],

将 Providers 的模型改成我们自己的,还有下面的 283 行 database,更改默认的用户模型

 // User tables and model.
        'users_table' => 'admin_users',
        'users_model' => \App\Admin\Models\Administrator::class,

你上面做的都没问题,注册 Policy,定义 Gate 方法都是正确的。


那么为什么返回 false ?

不论是 Gate::allows 还是 Gate::inspect,他们都需要一个共同的东西,就是 当前授权用户,我们打开

/vendor/laravel/framework/src/Illuminate/Auth/Access/Gate.php
查看 inspect 方法

public function inspect($ability, $arguments = [])
    {
        try {
            $result = $this->raw($ability, $arguments); //注意这行
            if ($result instanceof Response) {
                return $result;
            }
          ...
    }

接着追踪 raw() 方法

public function raw($ability, $arguments = [])
{
        $arguments = Arr::wrap($arguments);
        $user = $this->resolveUser(); //这里打断点
        dd($user); // null 是在这里产生的
}

就如我上面所说,如果 Gate 方法获取不到当前登录用户,那么它肯定是无法判断权限的对吧?

那么 Gate 为什么获取不到当前用户?我们打开 auth.php config 文件第 16 行

    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

这里设置了默认的用户组是 Web,也就是默认的 User 模型,Gate 当然是来找这个模型相关的授权用户,但是 Dcat-admin 有自己独立的一套授权系统,所以就产生了你上面不论怎么写都是返回 null。


到此,问题已经明确,下面我们来修复它:

刚刚我们看到了 resoverUser() 方法是获取当前用户的,那么我们可以在调用 Gate 方法的时候告诉它当前登录用户是谁

public function edit($id, Content $content)
    {
        $adminUser = \App\Admin\Models\Administrator::query()->find($id);
        $response = Gate::forUser(Auth::guard('admin')->user())->allows('admin-view', $adminUser);
        dd($response);
        return parent::edit($id, $content); // TODO: Change the autogenerated stub
    }

Gate::forUser(Auth::guard('admin')->user()) 我们告诉 Gate,不要去默认的 web 授权组去找,而是我们直接指定一个授权用户给它,让它去验证。

希望能解决你的问题哈哈

1年前 评论
laravel_peng (楼主) 1年前
DogLoML

看了最佳回答,然后去找了下dcat的源码怎么实现的,发现这样也可以,没必要自己重新定义一个guard,直接用dcat的就行。

\Admin::user()就是auth('admin')->user()或者说\Auth::guard('admin')

file

file

Laravel

1年前 评论
laravel_peng (楼主) 1年前
MArtian 1年前
DogLoML (作者) 1年前
MArtian 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!