前端权限控制思路

界面控制

登录请求中获取权限数据,根据权限展示对应菜单。

用户未登录时,手动敲入管理界面地址,需跳转到登录界面

用户已登录时,手动敲入非权限地址,需跳转 404 界面

按钮控制

仅显示有权操作的按钮,或禁用无权操作的按钮

请求与响应控制

对于非常规操作,如通过浏览器调试工具将某些禁用的按钮变成启用状态,此时发请求也应该被前端拦截

Vue 前端实现

界面控制

界面控制的实现方法是动态路由。用户登录后获取用户有权操作的菜单列表,前端在进行路由构建的时候将用户有权操作的路由添加到 vue-router 中,这样即便用户手动输入无权界面的地址也会因为没有在 vue-router 中添加该路由而返回 not found。

1、用户登录获取菜单列表

2、将用户菜单列表存储到 pinia 中

3、通过 pinia 中的数据构建菜单栏

注意事项:网页刷新后 pinia 的数据也会被刷新,如果构建菜单栏时使用的 pinia 的数据,会出现菜单无法构建的情况。

解决方式:通过将用户菜单权限信息存储在 sessionstroage 中或通过 pinia 持久化来解决

登录成功后:

1
2
3
4
5
6
7
8
9
const store = useDirectoryConfigStore();
// 登录成功后的跳转
const signInSuccess = async () => {
setSession("token", userInfo.username);
const res = await getDirectory(); //获取用户目录权限
store.directory = res.data;
initDynamicRoutes(); //初始化动态路由
router.push("/");
};

pinia:

1
2
3
4
5
6
7
export const useDirectoryConfigStore = defineStore('directoryConfig', {
state: () => ({
directory: [] as any[]
}),
// 持久化
persist: { paths: ['directory'] }
}

这里有个推荐的写法,当获取到后端出来的路由数据,我们需要给 vue-router 进行添加动态路由的操作,若服务器返回结果为:

1
2
3
4
5
6
7
8
9
[
{
"id": 12,
"icon": "icon-user",
"title": "用户管理",
"path": "users",
"auth": ["add", "edit"]
}
]

我们可以让 path 路径自动对应我们的 view 下的页面(仅作示范):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// route.js
import { createRouter, createWebHashHistory } from 'vue-router';
const router = createRouter({
history: createWebHashHistory(),
routes: staticRoutes,
scrollBehavior: () => ({ left: 0, top: 0 }),
});

export const initDynamicRoutes = (){
const store = useDirectoryConfigStore();
const pageModule = import.meta.glob("@/views/**/**/*.vue");
store.directory.foreach((dir)=>{
router.addRoute({
path:'/'+dir.path,
component:pageModule[`/src/view/${path}/index.vue`],
name:dir.path
})
})
}

注意事项:动态路由是通过点击登录按钮后添加的,如果刷新了页面这个路由就不会添加。
解决方式:在 App.vue 的 created 生命周期中调用 initDynamicRoutes 即可。

按钮控制

按钮控制可以通过自定义指令来实现:

1
<el-button type="primary" , v-permission="{action:'add'}"> 添加用户 </el-button>

在加载动态路由的时候,给路由的 meta 元数据添加一个权限列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// route.js
import { createRouter, createWebHashHistory } from 'vue-router';
const router = createRouter({
history: createWebHashHistory(),
routes: staticRoutes,
scrollBehavior: () => ({ left: 0, top: 0 }),
});

export const initDynamicRoutes = (){
const store = useDirectoryConfigStore();
const pageModule = import.meta.glob("@/views/**/**/*.vue");
store.directory.foreach((dir)=>{
router.addRoute({
path:'/'+dir.path,
component:pageModule[`/src/view/${path}/index.vue`],
name:dir.path,
meta:{
auth:dir.auth // 添加当前路由规则在用户所具有的权限
}
})
})
}

自定义指令实现方式:

1
2
3
4
5
6
7
8
9
10
11
import Vue from 'vue'
import router from '@/router';
Vue.directive('permission'.{
inserted(el,binding){
const action =binding.value.action
// 判断当前的路由所对应的组件中,用户是否具备action的权限
if(router.currentRoute.meta.indexOf(aciton) == -1){
el.parentNode.removeChild(el) //如果没有权限则移除这个元素
}
}
})

请求与响应控制

拦截用户请求的基本思路是通过 axios 的请求拦截器来实现,在我给出的示例中,仅适用于 restful 风格的请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import router frm '@/router'
const actionMapping = {
'get':'view',
'post':'add',
'put':'edit',
'delete':'delete'
}
axios.interceprots.request.use(function(req){
const currentUrl = req.url
if(currentUrl !== 'login'){
// 不是登录的请求,应在请求头中加入token
req.headers.Authorzation = sessionStorage.getItem('token')
const auth = router.currentRoute.meta.auth
// 判断当前请求的请求方式(仅适用于restful风格)
const action = actionMapping[req.method]
if(auth && auth.indexOf(action) === -1 ){
alert('没有权限')
return Promise.reject(new Error('没有权限'))
}
}
})

当服务器返回状态码为 401 代表 token 过期或被篡改,此时应强制跳转到登录界面:

1
2
3
4
5
6
7
8
axios.interceptors.response.use(function (res) {
if (res.data.meta.status === 401) {
router.push("/login");
sessionStorage.clear();
window.location.reload();
}
return res;
});

位掩码实现权限运算

核心原理

权限用二进制位表示:每个权限对应一个独立的二进制位(如 0001、0010、0100 等)
权限组合:通过按位或(|)运算合并权限
权限校验:通过按位与(&)运算判断是否拥有某个权限

实现步骤

1、定义权限
每个权限用唯一的二进制位表示,通常用1<<n生成:

1
2
3
4
5
6
const Permission = {
READ: 1 << 0,
WRITE: 1 << 1,
DELETE: 1 << 2,
ADMIN: 1 << 3,
};

2、分配用户权限
用户权限是多个权限的组合,通过按位或运算合并:

1
2
// 用户拥有 READ 和 WRITE 权限
const userPermissions = Permission.READ | Permission.WRITE; // 0001 | 0010 = 0011 -> 3

3、校验权限
用按位与(&)判断用户是否拥有某个权限:

1
2
3
4
5
6
7
function hasPermission(userPerm, requiredPerm) {
return (userPerm & requiredPerm) === requiredPerm;
}

// 检查是否有 WRITE 权限
console.log(hasPermission(userPermissions, Permission.WRITE)); // true
console.log(hasPermission(userPermissions, Permission.DELETE)); // false

如果 userPerm & requiredPerm 的结果等于 requiredPerm,说明用户拥有该权限。

优点

​​ 高效 ​​:二进制运算速度极快。
​​ 节省空间 ​​:单个数字可表示多达 32 种权限(JavaScript 中位运算使用 32 位整数)。
​​ 组合灵活 ​​:支持动态添加/移除权限。

缺点

​​ 可读性差 ​​:对不熟悉位运算的开发者不友好。
​​ 权限数量限制 ​​:最多支持 32 种独立权限(在 JS 中)。