一、RBAC
RBAC: role base access control 基于角色的用户访问权限控制权限,就是权限分配给角色,角色又分配给用户。
即一个用户对应一个角色,一个角色对应多个权限,一个用户对应用户组,一个用户组对应多个权限。
二、认证授权逻辑
登录逻辑:
权限控制逻辑:
三、具体实现
创建表的迁移文件
用户:
- 创建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.search
、admin.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{
// 此处省略增删查改的方法 ...
}