前言
在现代前端开发中,权限控制是一个至关重要的环节。无论是后台管理系统还是企业级应用,都需要根据用户角色或权限级别来限制界面元素的可见性和可访问性。
在前后端分离的架构中,前端不再拥有完整的权限判断能力,必须依赖后端提供的权限信息。
注意,以下实现之后,每次使用权限相关功能还需要后端校验保证安全。
实现
页面权限
页面的访问,是根据配置路由表的路径来访问页面。那么,我们控制了路由表,就能控制页面的访问。
实现思路:登录 -> 获取权限信息 -> 动态添加可访问路由。
传统的静态路由配置无法满足现代应用的权限需求,需在前端使用动态路由实时修改,无需刷新整个应用。
基本页面
bash
src
└─views
├─home.vue
├─menu.vue
└─user.vue
html
<!-- home.vue -->
<template>
<div>主页</div>
</template>
<!-- menu.vue -->
<template>
<div>菜单管理</div>
</template>
<!-- user.vue -->
<template>
<div>用户管理</div>
</template>
配置路由
bash
src
└─router
├─modules
│ ├─remaining.js
│ └─dynamic.js
└─index.js
-
remaining
存放公共路由,没有权限页可以访问,比如登录页、404页面这些ts// remaining.js import Layout from "@/layout/index.vue"; const remainingRouter = [ { path: "/remaining", component: Layout, redirect: "/remaining/home", meta: { title: "公共页面", roles: ["admin", "user"] }, children: [ { path: "/remaining/home", component: () => import("@/views/home.vue"), name: "Home", meta: { title: "首页", icon: "home" }, }, ], }, { path: "/:pathMatch(.*)*", name: "404", component: () => import("@/views/404.vue"), meta: { hidden: true }, }, ]; export default remainingRouter;
-
dynamic
动态路由模块,需要权限访问。ts// router/modules/dynamic.js import Layout from '@/layout/index.vue' // 动态路由(需要权限) export const dynamicRouter = [ { path: '/system', component: Layout, redirect: '/system/user', meta: { title: '系统管理', icon: 'system', roles: ['admin'] }, children: [ { path: 'user', component: () => import('@/views/user.vue'), name: 'User', meta: { title: '用户管理', icon: 'user' } }, { path: 'menu', component: () => import('@/views/menu.vue'), name: 'Menu', meta: { title: '菜单管理', icon: 'menu' } } ] } ] export default dynamicRouter
-
index.ts
路由配置tsimport { createRouter, createWebHistory } from "vue-router"; import { useAuthStore, usePermissionStore } from "@/stores"; import remainingRouter from "./modules/remaining"; export const constantRoutes = [...remainingRouter]; // 基础配置 const router = createRouter({ history: createWebHistory(), routes: constantRoutes, scrollBehavior: () => ({ left: 0, top: 0 }), }); // 白名单路由(无需登录) const WHITE_LIST = ["Home", "404"]; // 全局前置守卫 router.beforeEach(async (to, from) => { const authStore = useAuthStore(); // 已登录 if (authStore.token) { return true; } // 未登录 else { // 白名单路由直接放行 if (WHITE_LIST.includes(to.name)) { return true; } return { path: "/remaining/home" }; } }); export default router;
Pinia 储存登录状态
bash
src
└─stores
├─modules
│ ├─auth.store.js
│ └─permission.store.js
└─index.js
-
auth.store.js
控制登录退出jsimport { defineStore } from "pinia"; import router from "@/router"; import { usePermissionStore } from "@/stores"; export const useAuthStore = defineStore("auth", { state: () => ({ token: localStorage.getItem("token") || null, roles: [], }), actions: { // 用户登录 async login() { this.token = "12345"; this.roles = ["admin"]; localStorage.setItem("token", this.token); // 添加动态路由 const permissionStore = usePermissionStore(); const accessRoutes = permissionStore.generateRoutes(this.roles); accessRoutes.forEach((route) => { router.addRoute(route); }); }, // 用户登出 logout() { this.token = null; this.roles = []; localStorage.removeItem("token"); router.replace({ name: "Home" }); }, }, });
-
permission.store.js
控制动态添加路由jsimport { defineStore } from "pinia"; import dynamicRoutes from "@/router/modules/dynamic"; import { constantRoutes } from "@/router"; export const usePermissionStore = defineStore("permission", { state: () => ({ routes: [], addRoutes: [], }), actions: { // 生成可访问路由 generateRoutes(roles) { let accessedRoutes; if (roles.includes("admin")) { accessedRoutes = dynamicRoutes; // 管理员获取所有路由 } else { accessedRoutes = []; } this.addRoutes = accessedRoutes; this.routes = constantRoutes.concat(accessedRoutes); return accessedRoutes; // 返回生成的路由 }, }, });
-
index.js
导出 pinia 实例jsimport { createPinia } from 'pinia' const pinia = createPinia() export * from './modules/auth.store.js' export * from './modules/permission.store.js' export default pinia
这样即可实现动态添加路由
按钮权限
按钮权限,通过访问权限是否存在而决定是否显示按钮。
可以通过全局挂载自定义指令访问 pinia 中的权限,如果存在对应权限则显示按钮
配置 Directive
src
src
└─directive
└─permission.js
-
permission.js
自定义指令jsimport { watchEffect } from "vue"; import { useAuthStore } from "@/stores"; const permissionDirective = { mounted(el, binding) { const checkPermission = () => { const authStore = useAuthStore(); const hasPermission = binding.value && binding.value.some((permission) => authStore.roles.includes(permission) ); el.style.display = hasPermission ? "" : "none"; }; // 初始检查 + 响应式监听权限变化 checkPermission(); watchEffect(checkPermission); }, }; export default permissionDirective;
之后挂载到 app 即可实现全局按钮权限控制。
全局配置
简单实现一个布局
bash
src
└─layout
└─index.vue
-
index.vue
html<template> <div> <button @click="$router.push({ name: 'Home' })" >Home</button> <button @click="$router.push({ name: 'User' })" v-permission="['admin']">User</button> <button @click="$router.push({ name: 'Menu' })" v-permission="['admin']">Menu</button> <button @click="authStore.login()">登录</button> <button @click="authStore.logout()">登出</button> <RouterView /> </div> </template> <script setup> import { RouterView } from 'vue-router' import { useAuthStore } from "@/stores"; const authStore = useAuthStore(); </script>
将之前的内容挂载到 main
-
main.ts
加载tsimport { createApp } from 'vue' import router from './router' import pinia from './stores' import App from './App.vue' import permissionDirective from './directive/permission' const app = createApp(App) app.directive('permission', permissionDirective); // 注册为 v-permission app.use(pinia) app.use(router) app.mount('#app')
运行
- 运行项目:
npm run dev
- 进入访问页:
localhost:5173/remaining
- 尝试运行
项目结构: