1.权限控制的方案选择。
做后台项目区别于做其它的项目,权限验证与安全性是非常重要的,可以说是一个后台项目一开始就必须考虑和搭建的基础核心功能。在后台管理系统中,实现权限控制可以采用多种方案:
这里我选择了基于角色的访问控制(Role-Based Access Control,RBAC) 这是因为RBAC提供了一种灵活且易于管理的方式来控制用户对系统功能和资源的访问,也是目前最主流的前端权限方案选择。
在RBAC中,系统中的功能和资源被组织成角色,而用户则被分配到不同的角色。每个角色都有一组权限,定义了该角色可以执行的操作和访问的资源。通过给用户分配适当的角色,可以实现对用户的权限控制。
RBAC的好处之一是它简化了权限管理的复杂性。管理员只需管理角色和分配角色给用户,而不需要为每个用户单独定义权限。当需要对用户的权限进行修改时,只需调整其角色的权限即可。
此外,RBAC还支持灵活的权限组合,允许创建具有不同权限组合的角色,以适应不同用户的需求。它也便于扩展,可以随着系统的发展和需求的变化而调整和添加角色。
是对用户身份认证的细化。可简单理解为访问控制,在用户身份认证通过后,系统对用户访问菜单或按钮进行控制。也就是说,该用户有身份进入系统了,但他不一定能访问系统里的所有菜单或按钮,而他只能访问管理员给他分配的权限菜单或按钮。 主要包括:
权限标识或权限字符串校验规则:
关键数据模型如下:
关系图如下:
【用户】 <---多对多---> 【角色】 <---多对多---> 【菜单/权限】
前端权限一般分为路由级权限和按钮级权限,这里我们先实现页面路由级的权限功能,按钮级的会在后面讲到。大致的思路如下图:
上图是用户从登录-->路由守卫-->权限验证-->构建路由表-->跳转目标页面的一个简单的正向流程,可以看到,权限验证和构建路由表这两步是发生在路由守卫这一步里,而实际上在开发设计阶段,我们还要做的准备有:
为了方便理解,我们就按照这个正向流程来逐步实现,期间需要用到或者提前考虑设计到的内容,包括以上需要准备的两点,我会补充在步骤当中。
登录成功后,获取到token,将token存到本地的sessionStorage里。
async login({ commit }, userInfo) {
const { data } = await login(userInfo)
const accessToken = 'Bearer ' + data.access_token
if (accessToken) {
sessionStorage.getItem(tokenTableName)
} else {
message.error(`登录接口异常,未正确返回${tokenName}...`)
}
}
接着,进行路由跳转到首页
import { useRouter } from 'vue-router';
const router = useRouter();
router.push('/');
如果考虑到页面token失效后,重新登陆后返回原先的路由地址,则需要在路由守卫中添加redirect字段用来存储当前的路由地址
next({ path: '/login', query: { redirect: to.fullPath }, replace: true })
然后在登录页中,监听路由对象中的这个值,如果有值,那么在刚刚路由跳转时,就跳转到该路由地址,而非/首页,另外,还需要考虑到403,404页面。所以,添加了这些逻辑后,部分代码如下:
//login.vue
<script setup>
import { ref, watch, useRouter } from'vue';
import { useRouter } from'vue-router';
const redirect = ref('/');
const router = useRouter();
watch(
() => router.currentRoute.value.query.redirect,
(redirectValue) => {
redirect.value = redirectValue || '/';
},
{ immediate: true }
);
function handleRoute() {
return redirect.value === '/404' || redirect.value === '/403'
? '/'
: redirect.value
},
async handleSubmit() {
loading.value = true
try {
await login(form.value)
loading.value = false
router.push(handleRoute());
} catch {
loading.value = false
}
},
</script>
当进行路由跳转时,会进入到路由守卫中,在路由守卫中:
调取用户信息接口获取用户角色信息和权限信息,代码如下:
//store/user
// 获取用户信息
GetInfo({ commit, dispatch }) {
returnnewPromise((resolve, reject) => {
getUserInfo()
.then(async (response) => {
const result = response.data
if (result.roles && result.roles.length > 0) {
// 验证返回的roles是否是一个非空数组
commit('SET_ROLES', result.roles)
//permissions就是对应的用户权限信息
commit('SET_PERMISSION', result.permissions)
} else {
// 如果当前用户没有角色,则赋值一个默认角色
commit('SET_ROLES', ['ROLE_DEFAULT'])
}
resolve(result)
// GetInfo一旦失败就说明这个token不是过期就是丢失了,直接走catch并让调用方跳转路由
if (!response.success) {
reject(response.errorMsg)
}
})
.catch((error) => {
reject(error)
})
})
},
根据之前上文提到的权限管理模型,从之间的关系是多对多,因此,role(角色)和permssions(权限)的类型应为数组,其中的权限permssion这个字段的格式规范也在上文提及。在与后端约定好后,后端返回的信息如图:
在成功获取到用户信息,拿到用户角色和对应权限后,此时,就需要根据权限生成对应的路由表了,到这里,我们需要思考一个问题:路由表是由后端提供还是由前端提供?A:前端根据权限生成路由表 B:后端生成路由表给前端
没错!答案是C:路由表可以由后端提供或由前端提供。
两者的优劣分别是:
1、后端提供路由表:
优点:
安全性高:后端负责验证和控制权限,可以确保只有授权的用户能够访问特定的路由和功能。
隐藏敏感信息:后端可以根据用户的角色和权限隐藏不应该被访问的敏感路由和数据。
适用于复杂的权限规则:后端可以使用更复杂的逻辑和规则来处理权限控制,例如基于用户角色、用户组、权限等的复杂控制逻辑。
缺点:
前后端耦合度高:由于路由表由后端提供,前端开发人员可能需要与后端开发人员密切协作,增加了协调和沟通的成本。
前端依赖后端:前端应用程序可能需要等待后端提供路由表后才能进行开发和测试,增加了开发的时间和依赖性。
2、前端提供路由表:
优点:
前后端解耦:前端可以独立开发和维护路由表,减少了与后端的依赖性和协调成本。更好的用户体验:
前端可以根据用户的角色和权限动态展示路由和功能,提供更灵活和个性化的用户体验。
缺点:
安全性较低:前端提供的路由表容易受到篡改和绕过,安全性相对较低。隐藏敏感信息的难度增加:
前端无法直接隐藏敏感路由和数据,需要依赖后端接口的授权验证来保护敏感信息。
最终,考虑到后台管理系统的安全性优先级最高,我选择了由后端存储,并生成返回路由表信息。 确定好了之后,你就会遇到一个坑:后端提供的路由表无法直接在前端路由中添加使用 这是因为,后端存储的路由表的结构只一个JSON对象。怎么解决,我们放到后面讲。
我们现在把整个路由分为两个部分,分别是公共路由和动态路由(私有路由)。
公共路由顾名思义就是无论当前用户是什么角色,都会出现的路由部分。一般这样的路由分别有:首页驾驶舱、登录注册页、403页、404页。 这部分路由是在前端项目中提前写死的。以我的项目为例,我在src/config下创建一个publicRouter.js文件,然后里面放入基础路由信息:
//基础公共路由
exportconst constantRouterMap = [
{
path: '/',
name: '',
component: () =>import('@/layout'),
redirect: '/homePage',
children: [
{
path: '/homePage',
name: 'homePage',
component: () =>import('@/views/homePage/index.vue'),
},
],
},
{
path: '/login',
component: () =>import('@/views/login'),
},
{
path: '/403',
name: '403',
component: () =>import('@/views/403'),
},
{
path: '/:pathMatch(.*)*',
component: () =>import('@/views/404'),
}]
这里面都是路由懒加载的写法,这个没啥好说的。需要注意的是在vue3中,vue-router的版本是4.x以上,在跳转404页面时,如果你的写法是
{
path: "/404",
name: "notFound",
component: () => import('@/views/404')
}
这样写会提示报错,这是因为vue-router4.x的版本官方推荐引入了这种新写法,配置一个通配符路由,匹配所有未被其他具体路由匹配的路径。其中 :pathMatch 是参数名,而 (.*)* 是参数的匹配模式,它使用正则表达式来匹配任意路径。
上文提到的后端返回给前端当前用户的路由表信息,其实就是私有路由,这部分的路由是动态的。那么我们只需要把公共路由+动态路由=当前角色用户完整路由表。然鹅,后端返回给前端的动态路由是无法直接添加到当前页面路由中的,这是因为在浏览器的前后端请求当中,信息的返回体都是以JSON字符串的数据交换格式进行传输,而JSON字符串是不支持函数的。为什么要提到函数形式呢?因为当我们在路由表使用懒加载的写法时,component的值是一个异步函数。也只有异步函数才能实现对路由的异步懒加载。import()会返回一个promise,这个 promise 最终会加载对应的组件模块。在格式上表现为一个func函数,因此,我们无法将compnent的值传给后端存储但是办法总比困难多,我们可以将对应的路径字符串传给后端存储,然后再通过后端返回的路径字符串转换成这种箭头函数的写法。 除了这个需要转换以外,我们还要考虑一些问题:
{
"path": "/", //path,路由的路径,即访问该路由时的 URL 路径。
"name": "homePage",//name,路由的名称,用于在代码中标识路由。通常用于编程式导航。
"hidden": false,//hidden,是否隐藏路由,在Vue Router 4.0 中被废弃。
"component": "Layout"//路由对应的组件,可以是通过懒加载方式导入的异步组件,或直接引入的同步组件。
"meta": {//meta,路由元信息,可以用于存储一些额外的信息,比如页面标题、权限等
"title": "首页", //路由标题
"icon": "icon-Home", //路由图标
"target": "", //是否跳转新页签
"permission": "homePage",//权限
"keepAlive": false//是否缓存
},
"redirect": "/homePage",//重定向
"fullPath": "/",//完整的url路径,会带上?后面的参数
"children": [//子路由
{
"path": "/homePage",
"name": "homePage",
"meta": {
"title": "首页",
"icon": "",
"target": "",
"permission": "homePage",
"keepAlive": false
},
"fullPath": "/homePage"
}
]
}
考虑完这些之后,我们就开始拿着后端返回的路由表信息动手转换吧! 先看看后端返回给前端的路由表格式内容
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!