Laravel实现权限控制

本文详细介绍了如何在 Laravel 框架中实现基于角色的访问控制(RBAC)系统,包括用户、角色、权限的模型和迁移文件创建,以及认证授权逻辑。文章还涵盖了控制器、路由设置、自定义验证中间件以及中间件过滤控制器方法的实现,旨在提供一套完整的 RBAC 解决方案。
摘要由CSDN通过智能技术生成

一、RBAC

RBAC: role base access control 基于角色的用户访问权限控制权限,就是权限分配给角色,角色又分配给用户。

一个用户对应一个角色,一个角色对应多个权限,一个用户对应用户组,一个用户组对应多个权限。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Be9NmnKt-1601438441622)(1.jpg)]

二、认证授权逻辑

登录逻辑:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OI9DwHAm-1601438441624)(2.jpg)]

权限控制逻辑:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oCiQK0wb-1601438441627)(3.jpg)]


三、具体实现

创建表的迁移文件

用户:
  • 创建model和迁移文件:
php artisan make:model Models/User -m
  • 修改迁移文件:
class CreateUsersTable extends Migration
{

    /**
     * 后台用户表
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            // 角色
            $table->unsignedInteger('role_id')->default(0)->comment('角色ID');
            $table->string('username', 50)->comment("账号");
            $table->string('truename', 20)->default('未知')->comment("账号");
            $table->string('password', 255)->comment('密码');
            $table->string('email', 50)->nullable()->comment('邮箱');
            $table->string('phone', 15)->default('')->comment('手机号码');
            $table->enum('sex', ['先生','女士'])->default('先生')->comment('性别');
            $table->char('last_ip', 15)->default('')->comment('登录IP');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

角色:
  • 创建model和迁移文件:
php artisan make:model Models/Role -m
  • 修改迁移文件:
class CreateRolesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('roles', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name', 20)->comment('角色名称');
            $table->unsignedInteger('status')->default(0)->comment('0未引用,1已引用');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('roles');
    }
}

权限:
  • 创建model和迁移文件:
php artisan make:model Models/Node -m
  • 修改迁移文件:
class CreateNodesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('nodes', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name',50)->comment('节点名称');
            $table->string('route_name', 100)->nullable()->comment('路由别名,权限认证标识');
            $table->unsignedInteger('pid')->default(0)->comment('上级ID');
            $table->enum('is_menu', ['0','1'])->comment('是否是菜单');
            $table->unsignedInteger('status')->default(0)->comment('0未引用,1已引用');
            $table->softDeletes();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('nodes');
    }
}
  • 添加用户、角色一对一关联关系
class User extends Authenticatable
{
    public function role()
    {
        return $this->belongsTo(Role::class,'role_id');
    }
}
  • 添加角色、节点多对多关联关系
class Role extends Model
{
    public function nodes()
    {
        // 参1 关联模型
        // 参2 中间表的表名,没有前缀
        // 参3 本模型对应的外键ID
        // 参4 关联模型对应的外键ID
        return $this->belongsToMany(
            Node::class,
            'role_node',
            'role_id',
            'node_id');
    }
}
  • 添加节点展示方法
class Node extends Model
{
    /**
     * 获取所有权限节点
     *
     * @return array
     */
    public function getAllList(){
        $data = self::get()->toArray();
        return $this->treeLevel($data);
    }

    /**
     * 数组的合并,并加上html标识前缀
     *
     * @param array $data
     * @param int $pid
     * @param string $html
     * @param int $level
     * @return array
     */
    public function treeLevel(array $data, int $pid = 0, string $html = '--', int $level = 0) {
        static $arr = [];
        foreach ($data as $val) {
            if ($pid == $val['pid']) {
                // 重复一个字符多少次
                $val['html'] = str_repeat($html, $level * 2);
                $val['level'] = $level + 1;
                $arr[] = $val;
                $this->treeLevel($data, $val['id'], $html, $val['level']);
            }
        }
        return $arr;
    }


    /**
     * 数据多层级
     *
     * @param array $data
     * @param int $pid
     * @return array
     */
    public function subTree(array $data, int $pid = 0) {
        // 返回的结果
        $arr = [];
        foreach ($data as $val) {
            // 给定的PID是当前记录的上级ID
            if ($pid == $val['pid']) {
                // 递归
                $val['sub'] = $this->subTree($data,$val['id']);
                $arr[] = $val;
            }
        }
        return $arr;
    }

}

创建控制器

php artisan make:controller Api/Admin/NodeController
php artisan make:controller Api/Admin/UserController
php artisan make:controller Api/Admin/RoleController

修改路由文件

新增角色、节点、用户列表

    Route::post('/admin/user/login', 'AuthController@login')->name('admin.index');
    Route::get('/admin/user/logout', 'AuthController@logout')->name('admin.logout');

    Route::group(['prefix' => '/admin','middleware' => 'adminAuth','as' => 'admin.'], function () {
        // 角色列表
        Route::group(['prefix' => 'role','as' => 'role.'], function () {
            Route::get('', 'RoleController@search')->name('search');
            Route::get('/{id}', 'RoleController@show')->where('id', '\d+')->name('show');
            Route::put('/{id}', 'RoleController@update')->where('id', '\d+')->name('update');
            Route::post('/', 'RoleController@store')->name('store');
            Route::delete('/{id}', 'RoleController@destroy')->where('id', '\d+')->name('destroy');
            // 获取某一角色的权限列表
            Route::get('/node/{id}', 'RoleController@nodeList')->name('node');
            // 更新某一角色的权限列表
            Route::post('/node/{role}', 'RoleController@saveNode')->name('node');
        });

        // 节点列表
        Route::group(['prefix' => 'node','as' => 'node.'], function () {
            Route::get('', 'NodeController@search')->name('search');
            Route::get('/{id}', 'NodeController@show')->where('id', '\d+')->name('show');
            Route::put('/{id}', 'NodeController@update')->where('id', '\d+')->name('update');
            Route::post('/', 'NodeController@store')->name('store');
            Route::delete('/{id}', 'NodeController@destroy')->where('id', '\d+')->name('destroy');
        });

        // 用户列表
        Route::group(['prefix' => 'user','as' => 'user.'], function () {
            Route::get('', 'UserController@search')->name('search');
            Route::get('/{id}', 'UserController@show')->where('id', '\d+')->name('show');
            Route::put('/{id}', 'UserController@update')->where('id', '\d+')->name('update');
            Route::post('/', 'UserController@store')->name('store');
            Route::delete('/{id}', 'UserController@destroy')->where('id', '\d+')->name('destroy');
        });

    });// end-auth

路由别名

命名规则: 格式为XXX.YYY.ZZZ,以角色接口为例,假设路由接口为 api/admin/role/search ,则设置该路由的别名为 admin.role.search,以此类推, admin.node.searchadmin.role.update

设置路由别名主要是为了权限鉴定的时候方便处理对路由的鉴权。


拓展FormRequest验证

使用自定义 FormRequest 类,该类集成自 Http\Request,但是针对每一种请求都要定义一个 FormRequest,比较麻烦,因此在控制器方法里只注入一个 Request,根据模板设计模式,针对不同的场景,拓展不同的验证规则。

  • 第一步:创建AbstractRequest类
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Str;

class AbstractRequest extends FormRequest
{
    public $scenes = [];
    public $currentScene;               //当前场景
    public $autoValidate = false;       //是否注入之后自动验证
    public $extendRules;
    public $messages;                   //错误消息提示

    public function authorize()
    {
        return true;
    }

    /**
     * 设置场景
     *
     * @param $scene
     * @return $this
     */
    public function scene($scene)
    {
        $this->currentScene = $scene;
        return $this;
    }

    /**
     * 使用扩展rule
     *
     * @param string $name
     * @return AbstractRequest
     */
    public function with($name = '')
    {
        if (is_array($name)) {
            $this->extendRules = array_merge($this->extendRules[], array_map(function ($v) {
                return Str::camel($v);
            }, $name));
        } else if (is_string($name)) {
            $this->extendRules[] = Str::camel($name);
        }

        return $this;
    }

    /**
     * 覆盖自动验证方法
     */
    public function validateResolved()
    {
        if ($this->autoValidate) {
            $this->handleValidate();
        }
    }

    /**
     * 验证方法
     *
     * @param string $scene
     * @throws \Illuminate\Auth\Access\AuthorizationException
     * @throws \Illuminate\Validation\ValidationException
     */
    public function validate($scene = '')
    {
        if ($scene) {
            $this->currentScene = $scene;
        }
        $this->handleValidate();
    }

    /**
     * 重写返回错误信息格式
     *
     * @param Validator $validator
     */
    public function failedValidation($validator)
    {

        $error= $validator->errors()->all();
        // $error = $validator;
        throw  new HttpResponseException(response()->json(['code'=>404,'message'=>$error[0]]));

    }

    /**
     * 根据场景获取规则
     *
     * @return array|mixed
     */
    public function getRules()
    {
        $rules = $this->container->call([$this, 'rules']);
        $newRules = [];
        if ($this->extendRules) {
            $extendRules = array_reverse($this->extendRules);
            foreach ($extendRules as $extendRule) {
                if (method_exists($this, "{$extendRule}Rules")) {   //合并场景规则
                    $rules = array_merge($rules, $this->container->call(
                        [$this, "{$extendRule}Rules"]
                    ));
                }
            }
        }
        if ($this->currentScene && isset($this->scenes[$this->currentScene])) {
            $sceneFields = is_array($this->scenes[$this->currentScene])
                ? $this->scenes[$this->currentScene] : explode(',', $this->scenes[$this->currentScene]);
            foreach ($sceneFields as $field) {
                if (array_key_exists($field, $rules)) {
                    $newRules[$field] = $rules[$field];
                }
            }
            return $newRules;
        }
        return $rules;
    }

    /**
     * 覆盖设置 自定义验证器
     *
     * @param $factory
     * @return mixed
     */
    public function validator($factory)
    {
        return $factory->make(
            $this->validationData(), $this->getRules(),
            $this->messages, $this->attributes()
        );
    }

    /**
     * 最终验证方法
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     * @throws \Illuminate\Validation\ValidationException
     */
    protected function handleValidate()
    {
        if (!$this->passesAuthorization()) {
            $this->failedAuthorization();
        }
        $instance = $this->getValidatorInstance();
        if ($instance->fails()) {
            $this->failedValidation($instance);
        }
    }

}
  • 第二步:
  • 创建UserRequest继承AbstractRequest
class UserRequest extends AbstractRequest
{
    function __construct()
    {
        $this->messages = $this->messages();
    }

    // 不同验证场景
    public $scenes = [
        'login' => 'username,password',
    ];

    /**
     * 获取已定义验证规则的错误消息
     *
     * @return array
     */
    public function messages()
    {
        return [
            'username.required' => '用户名称不得为空',
            'username.unique' => '用户名称不得重复',
            'password.required' => '密码不得为空',
            'role_id.required' => '角色不得为空',
            'role_id.numeric' => '角色id必须为数字',
        ];
    }

    /**
     * 全部的验证规则
     *
     * @return array
     */
    public function rules()
    {
        $id = $this->route('id'); //获取当前需要排除的id,这里的 id 是 路由 {} 中的参数
        $rules = [
            'password' => 'required|string',
            'role_id' => 'required|numeric'
        ];
        // 修改节点时的验证规则
        if($id){
            $rules['username'] = 'required|string|unique:users,username,'.$id;
            return $rules;
        }
        // 新增节点时的验证规则
        $rules['username'] = 'required|string|unique:users,username';
        return $rules;
    }

    public function loginRules(){
        return [
            'password' => 'required|string',
            'username' => 'required|string',
        ];
    }
}
  • 创建NodeRequest继承AbstractRequest
class NodeRequest extends AbstractRequest
{

    function __construct()
    {
        $this->messages = $this->messages();
    }

    /**
     * 获取已定义验证规则的错误消息
     *
     * @return array
     */
    public function messages()
    {
        return [
            'name.required' => '节点名称不得为空',
            'name.unique' => '节点名称不得重复',
            'route_name.required' => '路由别名不得为空',
            'route_name.unique' => '路由别名不得重复',
            'pid.required' => '上级节点不得为空',
            'pid.numeric' => '上级节点必须为数字',
            'is_menu.required' => '是否是菜单不得为空',
            'is_menu.numeric' => '是否是菜单类型必须为数字',
            'is_menu.in' => '是否是菜单值必须为0或1',
        ];
    }

    /**
     * 全部的验证规则
     *
     * @return array
     */
    public function rules()
    {
        $id = $this->route('id'); //获取当前需要排除的id,这里的 id 是 路由 {} 中的参数
        $rules = [
            'pid' => 'required|numeric',
            'is_menu' => 'required|numeric|in:0,1',
            'status' => 'required|numeric|in:0,1'
        ];

        // 修改节点时的验证规则
        if($id){
            $rules['name'] = 'required|string|unique:nodes,name,'.$id;
            $rules['route_name'] = 'required|string|unique:nodes,route_name,'.$id;
            return $rules;
        }
        // 新增节点时的验证规则
        $rules['name'] = 'required|string|unique:nodes,name';
        $rules['route_name'] = 'required|string|unique:nodes,route_name';
        return $rules;
    }

}
  • 创建RoleRequest继承AbstractRequest
class RoleRequest extends AbstractRequest
{
    function __construct()
    {
        $this->messages = $this->messages();
    }

    /**
     * 获取已定义验证规则的错误消息
     *
     * @return array
     */
    public function messages()
    {
        return [
            'name.required' => '角色名称不得为空',
            'name.unique' => '角色名称不得重复',
        ];
    }

    /**
     * 全部的验证规则
     *
     * @return array
     */
    public function rules()
    {
        $id = $this->route('id'); //获取当前需要排除的id,这里的 id 是 路由 {} 中的参数
        $rules = [];

        // 修改节点时的验证规则
        if($id){
            $rules['name'] = 'required|string|unique:roles,name,'.$id;
            return $rules;
        }
        // 新增节点时的验证规则
        $rules['name'] = 'required|string|unique:roles,name';
        return $rules;
    }
}

至此,验证规则全部写完。

参考博客:https://learnku.com/laravel/t/31215


中间件过滤

  • 创建中间件
php artisan make:middleware AdminAuthenticated 

Kernel.php文件里$routeMiddleware新增

        // 后台
        'adminAuth' => \App\Http\Middleware\AdminAuthenticated::class,
  • 白名单

考虑到业务本身的原因,这里新增一个权限白名单,在config目录下创建rbac.php文件,配置用户白名单以及路由白名单,以便后续业务的延申拓展。

<?php
    return [
        // 超级管理员
        "super" => 'admin',
        // 默认允许通过的路由
        "allow_route" => [
            'admin.index',
            'admin.logout'
        ]
    ];

需要使用时,config('rbac.super')、config('rbac.allow_route')读取白名单的信息即可

  • 中间件过滤:
	public function handle($request, Closure $next, $guard = null)
    {
        if (!auth()->check()){
            return response()->json(['code'=>401, 'msg' => '您未登录!']);
        }
        $allow_node = session('admin.auth.'.Auth::id());
        $auths = is_array($allow_node) ? array_filter($allow_node):[];
        // 合并数组
        $auths = array_merge($auths, config('rbac.allow_route'));
        // 当前访问的路由
        $currentRoute = $request->route()->getName();
        $request->auths = $auths;
        // 权限判断
        if (auth()->user()->username != config('rbac.super') &&
            !in_array($currentRoute, $auths)){
            return response()->json(['code' => 400, 'msg' => '您没有权限访问']);
        }
        return $next($request);
    }

控制器方法

  • 登录
 public function login(UserRequest $request)
    {
        $request->scene('login') ->with('login')->validate();
        $data = $request->input();
        $user = User::where('username', $data['username'])->first();
        if (!$user)
            return $this->json_output(404, '此用户不存在!');
        if (auth()->attempt(['username' => $data['username'], 'password' => $data['password']])) {
//            Auth::guard('admin2')->login($user);
            $user->last_login_ip = $request->ip();
            $user->save();
            // config配置rbac白名单
            if (config('rbac.super') != $data['username']){
                $userModel = auth()->user();
                $roleModel = $userModel->role;
                $nodeArr = $roleModel->nodes()->pluck('name','nodes.id')->toArray();
                // 权限保持到session中
                session(['admin.auth.'.Auth::id() => $nodeArr]);
                return $this->json_output(200, '登录成功', ['user' => $user,'nodeArr' => $nodeArr]);
            }
            session(['admin.auth.'.Auth::id() => true]);
            return $this->json_output(200, '登录成功', ['user' => $user,'nodeArr' => true]);
        }
        // 登录失败
        return $this->json_output(403, '账号或者密码错误');
    }
  • 退出登录
    public function logout()
    {
        auth()->logout();
        return $this->json_output(200, '登出成功');
    }
  • 角色控制器RoleController
class RoleController extends Controller{
    // 此处省略增删查改的方法 ...
    
    // 通过id获取权限信息 
    public function nodeList($id)
    {
        $role = Role::find($id);
        // 获取所有节点权限
        $nodeAll = (new Node)->getAllList();
        $nodes = $role->nodes()->pluck('nodes.id')->toArray();
        return $this->json_output(200, '权限信息',compact('nodeAll', 'nodes', 'role'));
    }
    
    // 更新角色权限
    public function saveNode(Request $request, Role $role)
    {
        // 关联模型的数据同步
        // sync 方法接收一个 ID 数组以替换中间表的记录。
        // 中间表记录中,所有未在 ID 数组中的记录都将会被移除。
        // 所以该操作结束后,只有给出数组的 ID 会被保留在中间表中:
        $role->nodes()->sync($request->get('node'));
        return $this->json_output(200, '更新权限成功');
    }   
}
  • 用户控制器UserController
class UserController extends Controller
{
    // 此处省略部分增删查改的方法 ...
    
    public function store(UserRequest $request)
    {
        $data = $request->input();
        $request->validate();
        $user = new User;
        foreach ($data as $key => $value) {
            if (is_null($value)) continue;
            if ($key == 'password') {
                $user->$key = bcrypt($value);
                continue;
            }
            // 使角色status状态变更 新增用户时同时添加角色绑定  用户1->1角色 
            if ($key == 'role_id'){
                Role::where('id', $value)->update(['status' => 1]);
            }
            $user->$key = $value;
        }
        $user->save();
        return $this->json_output(200, '创建成功', $user);
    }
}
  • 节点控制器NodeController
class NodeController extends Controller{
        // 此处省略增删查改的方法 ...
}
  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值