laravel 中 laravel-permission 对角色权限管理很好用。这里,就用这个玩意吧。突然想到一个额外的问题。项目中使用的是 Laravel Sanctum 来做认证。这样,仅仅是它自己可以使用了。如果想将 node.js 等和这个配合起来认证,就不好搞了。如果是用 jwt 来做,就不会出现这个问题。jwt 的实现都是一样的,各个语言都可以通用。岔开了,这里还是 laravel-permission 吧。
操作一波
文档:
https://spatie.be/docs/laravel-permission/v3/introduction
https://github.com/spatie/laravel-permission#installation
composer require spatie/laravel-permission
# 生成配置文件和迁移文件
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
# 如果之前做过缓存,需要这样处理下
php artisan config:clear
# 生成数据库表
php artisan migrate
可是,默认的迁移还不够接地气。因为我们的路由会展示在菜单中的,而且有的还有 icon,是否显示在菜单中,是否要显示面包屑。这个时候,多几个字段就很有必要了。如果结合 vue elemeui admin
的话,还需要控制一些其他参数,比如 path,redirect,alwaysShow,noCache 等。
https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/essentials/router-and-nav.html
想到这里,那么后端的 laravel-permission 和 前端的 vue element admin 的路由就耦合在一起了。这样有好处也有坏处。个人还是耦合在一起吧,官方不是这样弄的。
如果完全动态生成路由, 前端的角色控制路由就没必要了。直接根据接口生就好了。
还有其他问题。显示和非显示问题,显示的话至少是 get 方式的路由,但并不是所有 get 的路由都显示。权限不仅控制 get,也要控制一切可以访问的路由行为。好像这样又很麻烦,细节越多越麻烦。最终只是想控制路由和角色。貌似,如果对于前端,只关心菜单树,而不用关心 post,delete 等请求。如果权限表中不增加每一个的定义,就需要逻辑上去定义限制了。要不仅仅是前端路由限制,如果人家知道请路由和参数等,就可以模拟提交了。
还有一点,我们都是通过角色去控制权限的,不想直接给用户赋予权限。这样控制起来有意思又方便,还不容易混。
# permissions 表中增加 path, title, icon, hidden, breadcrumb,redirect,activeMenu, component 这几个字段。
为了适应中国国情, roles 中也需要加入 title 字段,来显示中文名称。为了方便生成无底线层级菜单,也需要加上 parent_id 字段,为了方便排序,也可以加上 sort 字段。
然后修改成这样子:
Schema::create($tableNames['permissions'], function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('guard_name');
$table->string('path')->comment('对应 vue path');
$table->string('icon')->nullable()->comment('对应 vue icon');
$table->string('title')->comment('对应 vue title');
$table->string('component')->comment('对应 vue component');
$table->boolean('breadcrumb')->default(1)->comment('对应 vue 是否显示面包屑中');
$table->boolean('hidden')->default(0)->comment('对应 vue 是否不显示在侧边栏中');
$table->boolean('noCache')->default(0)->comment('对应 vue 是否使用 keep-alive');
$table->string('redirect')->nullable()->comment('对应 vue 重定向');
$table->string('activeMenu')->nullable()->comment('对应 vue 需高亮相对应的侧边栏');
$table->unsignedBigInteger('parent_id')->default(0);
$table->unsignedTinyInteger('sort')->default(0)->comment('排序');
$table->timestamps();
});
Schema::create($tableNames['roles'], function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('guard_name');
$table->string('title')->comment('角色名称');
$table->timestamps();
});
其他的不变,然后执行:
php artisan migrate:rollback --step=1
php artisan migrate
建立 seeder 方便搞事情
既然表好了,我们建立一个 seeder,方便我们初始化一些权限模块。这里最多嵌套三级,不要太多了。
# 建立 seeder
php artisan make:seeder PermissionSeeder
# 填充
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Spatie\Permission\PermissionRegistrar;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use App\Models\User;
/**
* 权限 seeder
* https://spatie.be/docs/laravel-permission/v3/basic-usage/new-app
*/
class PermissionSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// Reset cached roles and permissions
// php artisan cache:forget spatie.permission.cache 清除缓存
app()[PermissionRegistrar::class]->forgetCachedPermissions();
$tableNames = config('permission.table_names');
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
}
// 先截断表
DB::statement('SET FOREIGN_KEY_CHECKS = 0'); // 禁用外键约束
DB::table($tableNames['role_has_permissions'])->truncate();
DB::table($tableNames['model_has_roles'])->truncate();
DB::table($tableNames['model_has_permissions'])->truncate();
DB::table($tableNames['roles'])->truncate();
DB::table($tableNames['permissions'])->truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1'); // 启用外键约束
$permissions = [
[
'name' => 'home',
'path' => '/',
'icon' => 'dashboard',
'title' => '主页',
'component' => 'Layout',
'breadcrumb' => 0,
'hidden' => 0,
'noCache' => 0,
'redirect' => '/dashboard',
'activeMenu' => '',
'child' => [
[
'name' => 'dashboard',
'path' => 'dashboard',
'icon' => 'dashboard',
'title' => '仪表盘',
'component' => 'dashboard/index',
'breadcrumb' => 0,
'hidden' => 0,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '',
'child' => [
]
]
]
],
[
'name' => 'system',
'path' => '/system',
'icon' => 'system',
'title' => '系统管理',
'component' => 'Layout',
'breadcrumb' => 0,
'hidden' => 0,
'noCache' => 0,
'redirect' => '/system/permission',
'activeMenu' => '',
'child' => [
[
'name' => 'permission',
'path' => 'permission',
'icon' => '',
'title' => '权限管理',
'component' => 'system/index',
'breadcrumb' => 0,
'hidden' => 0,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '',
'child' => [
[
'name' => 'permission.index',
'path' => 'index',
'icon' => '',
'title' => '权限',
'component' => 'system/permission/index',
'breadcrumb' => 0,
'hidden' => 1,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '/system/permission',
],
[
'name' => 'permission.create',
'path' => 'create',
'icon' => '',
'title' => '添加权限',
'component' => 'system/permission/create',
'breadcrumb' => 0,
'hidden' => 1,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '/system/permission',
],
[
'name' => 'permission.edit',
'path' => 'edit',
'icon' => '',
'title' => '编辑权限',
'component' => 'system/permission/edit',
'breadcrumb' => 0,
'hidden' => 1,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '/system/permission',
],
[
'name' => 'permission.destroy',
'path' => 'destroy',
'icon' => '',
'title' => '删除权限',
'component' => 'system/permission/destroy',
'breadcrumb' => 0,
'hidden' => 1,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '/system/permission',
]
]
],
[
'name' => 'role',
'path' => 'role',
'icon' => '',
'title' => '角色管理',
'component' => 'system/index',
'breadcrumb' => 0,
'hidden' => 0,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '',
'child' => [
[
'name' => 'role.index',
'path' => 'index',
'icon' => '',
'title' => '角色',
'component' => 'system/role/index',
'breadcrumb' => 0,
'hidden' => 1,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '/system/role',
],
[
'name' => 'role.create',
'path' => 'create',
'icon' => '',
'title' => '添加角色',
'component' => 'system/role/create',
'breadcrumb' => 0,
'hidden' => 1,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '/system/role',
],
[
'name' => 'role.edit',
'path' => 'edit',
'icon' => '',
'title' => '编辑角色',
'component' => 'system/role/edit',
'breadcrumb' => 0,
'hidden' => 1,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '/system/role',
],
[
'name' => 'role.destroy',
'path' => 'destroy',
'icon' => '',
'title' => '删除角色',
'component' => 'system/role/destroy',
'breadcrumb' => 0,
'hidden' => 1,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '/system/role',
],
[
'name' => 'role.permission',
'path' => 'permission',
'icon' => '',
'title' => '分配权限',
'component' => 'system/role/permission',
'breadcrumb' => 0,
'hidden' => 1,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '/system/role',
]
]
],
[
'name' => 'manager',
'path' => 'manager',
'icon' => '',
'title' => '管理员',
'component' => 'system/index',
'breadcrumb' => 0,
'hidden' => 0,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '',
'child' => [
[
'name' => 'manager.index',
'path' => 'index',
'icon' => '',
'title' => '管理',
'component' => 'system/manager/index',
'breadcrumb' => 0,
'hidden' => 1,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '/system/manager',
],
[
'name' => 'manager.create',
'path' => 'create',
'icon' => '',
'title' => '添加管理',
'component' => 'system/manager/create',
'breadcrumb' => 0,
'hidden' => 1,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '/system/manager',
],
[
'name' => 'manager.edit',
'path' => 'edit',
'icon' => '',
'title' => '编辑管理',
'component' => 'system/manager/edit',
'breadcrumb' => 0,
'hidden' => 1,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '/system/manager',
],
[
'name' => 'manager.destroy',
'path' => 'destroy',
'icon' => '',
'title' => '删除管理',
'component' => 'system/manager/destroy',
'breadcrumb' => 0,
'hidden' => 1,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '/system/manager',
],
[
'name' => 'manager.role',
'path' => 'role',
'icon' => '',
'title' => '分配角色',
'component' => 'system/manager/role',
'breadcrumb' => 0,
'hidden' => 1,
'noCache' => 0,
'redirect' => '',
'activeMenu' => '/system/manager',
]
]
],
]
]
];
$role = Role::create([
'name' => 'root',
'title' => '站长'
]);
foreach ($permissions as $pem1) {
// 生成一级权限
$p1 = Permission::create([
'name' => $pem1['name'],
'path' => $pem1['path'],
'icon' => $pem1['icon']??'',
'title' => $pem1['title'],
'component' => $pem1['component'],
'breadcrumb' => $pem1['breadcrumb']??1,
'hidden' => $pem1['hidden']??0,
'noCache' => $pem1['noCache']??0,
'redirect' => $pem1['redirect']??'',
'activeMenu' => $pem1['activeMenu']??'',
]);
// 为角色添加权限
$role->givePermissionTo($p1);
// 为用户添加权限(用户不直接分配权限,需要通过角色来实现权限控制)
// $user->givePermissionTo($p1);
if (isset($pem1['child'])) {
foreach ($pem1['child'] as $pem2) {
//生成二级权限
$p2 = Permission::create([
'name' => $pem2['name'],
'path' => $pem2['path'],
'icon' => $pem2['icon']??'',
'title' => $pem2['title'],
'component' => $pem2['component'],
'breadcrumb' => $pem2['breadcrumb']??1,
'hidden' => $pem2['hidden']??0,
'noCache' => $pem2['noCache']??0,
'redirect' => $pem2['redirect']??'',
'activeMenu' => $pem2['activeMenu']??'',
'parent_id' => $p1->id,
]);
// 为角色添加权限
$role->givePermissionTo($p2);
// 为用户添加权限(用户不直接分配权限,需要通过角色来实现权限控制)
// $user->givePermissionTo($p2);
if (isset($pem2['child'])) {
foreach ($pem2['child'] as $pem3) {
//生成三级权限
$p3 = Permission::create([
'name' => $pem3['name'],
'path' => $pem3['path'],
'icon' => $pem3['icon']??'',
'title' => $pem3['title'],
'component' => $pem3['component'],
'breadcrumb' => $pem3['breadcrumb']??1,
'hidden' => $pem3['hidden']??0,
'noCache' => $pem3['noCache']??0,
'redirect' => $pem3['redirect']??'',
'activeMenu' => $pem3['activeMenu']??'',
'parent_id' => $p2->id,
]);
// 为角色添加权限
$role->givePermissionTo($p3);
// 为用户添加权限(用户不直接分配权限,需要通过角色来实现权限控制)
// $user->givePermissionTo($p3);
}
}
}
}
}
// 如果用户不存在,才创建用户
$user = User::where('account', 13671638524)->first();
if (! $user) {
$phone = 136xxxxxxxx;
$faker = \Faker\Factory::create('zh_CN');
$user = User::create([
'account' => $phone,
'password' => Hash::make('111111'),
'nickname' => '七月羽歌',
'phone' => $phone,
'gender' => 1,
'avatar' => $faker->imageUrl,
'email' => 'lichking_lin86@qq.com',
'signature' => '虚幻之物对应着冥冥之路。',
'email_verified_at' => now(),
'remember_token' => Str::random(10)
]);
}
//为用户添加角色
$user->assignRole($role);
}
}
一级目录 path 一定要带上 /
, component 千万不要和将前端的 views 目录也包含进去了。因为前端路由懒加载是需要 views 这个的。
完善
Call to undefined method App\Models\User::assignRole()
处理
App\Models\User
模型处理:
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasRoles;
……
}
再重新迁移填充
php artisan migrate:refresh --seed
增加一个生成路由和权限的方法。
// 获取用户的菜单
public function getMenu($permissions, $parent_id = 0)
{
$array = [];
foreach($permissions as $key => $permission)
{
if ($permission['parent_id'] === $parent_id) {
$permission['children'] = $this->getMenu($permissions, $permission['id']);
$array[] = $permission;
}
}
return $array;
}
public function test()
{
$user = User::first();
$user->menus = $this->getMenu($user->getAllPermissions());
return response()->json($user, 200);
}
# 有了 user 就可以获取对应用户的 permissions
路由和权限也生了,现在就在前端来处理这个数据了。
- 通过接口获取 userInfo 中可以携带
- 在 permission.js 中 通过 addRoutes 来操作。
接口完善如下:
/**
* 获取当前用户信息
*/
public function userInfo(Request $request)
{
$user = $request->user();
$user->menus = $this->getMenu($user->getAllPermissions());
return response()->json($user, 200);
}
先修改 permission.js:
# 修改成下边这样,其实 role 也可以完全不要,因为路由数据都是完全有后端生的。
// note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
const { roles, menus } = await store.dispatch('user/getInfo')
// generate accessible routes map based on roles
const accessRoutes = await store.dispatch('permission/generateRoutes', {roles, menus})
src/store/modules/permission.js 修改成这样
import { constantRoutes } from '@/router'
import Layout from '@/layout'
export function generateMenu(routes, menus) {
menus.forEach(item => {
const menu = {
path: item.path,
component: item.component === 'Layout' ? Layout : resolve => require([`@/views/${item.component}`], resolve),
hidden: item.hidden,
children: [],
name: item.name,
redirect: item.redirect ? item.redirect : '',
meta: { title: item.title, icon: item.icon, activeMenu: item.activeMenu, noCache: item.noCache }
}
if (item.children && item.children.length > 0) {
generateMenu(menu.children, item.children)
} else {
delete menu.children
}
routes.push(menu)
})
}
const state = {
routes: [],
addRoutes: []
}
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
}
const actions = {
generateRoutes({ commit }, data) {
const { roles, menus } = data
return new Promise(resolve => {
let accessedRoutes = []
generateMenu(accessedRoutes, menus)
accessedRoutes.push({ path: '*', redirect: '/404', hidden: true });
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
然后,框架自身的路由可以简化成这样子。
src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
/**
* constantRoutes
* a base page that does not have permission requirements
* all roles can be accessed
*/
export const constantRoutes = [
{
path: '/redirect',
component: Layout,
hidden: true,
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/redirect/index')
}
]
},
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/error-page/404'),
hidden: true
},
{
path: '/401',
component: () => import('@/views/error-page/401'),
hidden: true
}
]
export const asyncRoutes = [{ path: '*', redirect: '/404', hidden: true }]
const createRouter = () => new Router({
mode: 'history', // require service support
base: '/admin/',
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
const router = createRouter()
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router
到此,侧边栏完全有服务端生成,和前端配置毫无关系。当然,你可以默认使用一些前端配置路由。
view 页面填充。用个能连起来多就好
src/views/system/index.vue
<template>
<div>
<router-view />
</div>
</template>
src/views/system/permission/index.vue
<template>
<div style="padding:30px;">
<span>
permission
</span>
<router-view />
</div>
</template>
其他, create, edit 等也是这样。
遗忘了 icon 的处理了。因为侧边导航 icon 就那么几个,所以后端动态生成的时候,可以手动编辑赋值或 seed 赋值就好了。前端再手动添加控制 svg 就好。
然后编译,登录进去看效果。发现已经 ok 了。
下一步
就是操作完善系统管理的各个功能。权限管理啥的。