Vue3 中后台管理系统权限管理实现
一、权限管理核心思路
- 路由权限:根据用户角色动态生成可访问的路由表
- 按钮权限:通过自定义指令或组件控制按钮显示/隐藏
- 接口权限:在请求拦截器中校验权限
二、简单登录功能实现
1、登录组件实现(含密码登录)
LoginForm.vue
<template><a-form:model="formState" // 绑定表单数据对象:rules="rules" // 绑定表单验证规则ref="formRef"@finish="handleLogin" // 表单提交成功时触发的方法class="login-form"><!-- 用户名输入框 --><a-form-item name="username"><a-inputv-model:value="formState.username"placeholder="请输入用户名"size="large"><template #prefix><!-- 输入框前缀图标 --><UserOutlined /> <!-- 用户图标 --></template></a-input></a-form-item><!-- 密码输入框(带显示/隐藏切换) --><a-form-item name="password"><a-input-passwordv-model:value="formState.password"placeholder="请输入密码"size="large"><template #prefix><LockOutlined /> <!-- 锁图标 --></template></a-input-password></a-form-item><!-- 登录按钮 --><a-form-item><a-buttontype="primary"html-type="submit" // 原生 submit 类型size="large":loading="loading" // 绑定加载状态block // 块级按钮(占满宽度)>登录</a-button></a-form-item></a-form>
</template><script setup>
import { ref, reactive } from 'vue';
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { useUserStore } from '@/stores/userStore';// 组件属性定义
const props = defineProps({showCaptcha: { type: Boolean, default: false // 是否显示验证码 }
});// 组件事件定义
const emit = defineEmits(['login-success', 'change-login-type']);// 表单引用和状态
const formRef = ref(); // 表单引用(用于调用表单方法)
const loading = ref(false); // 登录按钮加载状态// 表单数据对象
const formState = reactive({username: '', // 用户名password: '' // 密码
});// 表单验证规则
const rules = {username: [{ required: true, message: '请输入用户名' }],password: [{ required: true, message: '请输入密码' }],
};/*** 处理登录逻辑*/
const handleLogin = async () => {try {loading.value = true; // 显示加载状态// 调用Pinia的登录方法await useUserStore().login({username: formState.username,password: formState.password,});// 登录成功处理message.success('登录成功');emit('login-success'); // 通知父组件登录成功} catch (error) {// 登录失败处理message.error(error.message || '登录失败');} finally {loading.value = false; // 隐藏加载状态}
};
</script><style scoped>
.login-form {max-width: 400px;margin: 0 auto;
}
</style>
2、登录方法调用(含获取Token及用户信息)(userStore.js)
import { defineStore } from 'pinia';// 导入Pinia的store定义方法
import { login, getUserInfo } from '@/api/login';// 导入登录和用户信息API
import { ref, computed } from 'vue';export const useUserStore = defineStore('user', () => {const token = ref(Session.get('token') || null);// 响应式token,优先从本地存储读取const refresh_token = ref(Session.get('refresh_token') || null);// 响应式refresh_tokenconst userInfo = ref({});// 用户信息对象const permissions = ref([]);// 权限列表const roles = ref([]);// 角色列表const isLogin = computed(() => !!token.value);// 根据token是否存在计算登录状态// 设置Tokenconst setToken = (data) => {token.value = data.access_token;// 更新tokenrefresh_token.value = data.refresh_token;// 更新refresh_tokenlocalStorage.setItem('token', token.value);localStorage.setItem('refresh_token', refresh_token.value);};// 清除Tokenconst clearToken = () => {token.value = null;// 清空tokenrefresh_token.value = null;// 清空refresh_tokenlocalStorage.removeItem('token');localStorage.removeItem('refresh_token');};// 登录方法const login= async (params) => {const { data } = await login(params);// 调用登录接口setToken(data);// 存储返回的tokenreturn data;};// 获取用户信息const fetchUserInfo = async () => {const { data } = await getUserInfo();// 调用获取用户信息接口userInfo.value = data.sysUser;// 更新用户信息roles.value = data.roles;// 更新角色permissions.value = data.permissions;// 更新权限localStorage.setItem('userInfo', JSON.stringify(userInfo.value));return data;};// 退出登录const logout = () => {clearToken(); // 清除tokenwindow.localStorage.clear()// 清空所有本地存储window.location.reload();// 刷新页面(重置应用状态)};// 暴露状态和方法return {token,refresh_token,userInfo,permissions,roles,isLogin,setToken,clearToken,login,getUserInfo: fetchUserInfo,logout};
});
小疑问解答:
1、refresh_token有什么作用?
答:在用户认证系统中,refresh_token是一个 可选但推荐 的机制,它的核心作用是 在无需用户重新登录的情况下获取新的访问令牌。因为access_token(即后端接口返回的Token)
通常有效期较短(如 2 小时),过期后用户需重新登录,体验差。通过 refresh_token 静默获取新 access_token
,保持用户持续登录状态。
三、登录组件使用(含路由跳转)
<template><div class="login-page"><LoginForm @login-success="handleLoginSuccess"<!-- 监听登录成功事件 -->/></div>
</template><script setup>
import LoginForm from '@/components/LoginForm.vue';// 登录成功后的跳转处理事件
const handleLoginSuccess= async () => {router.push('xxxx')//进行路由跳转useMessage().success('欢迎回来!')
}
</script>
四、路由权限控制
router.js
// 从vue-router导入创建路由和history模式的函数
import { createRouter, createWebHistory } from 'vue-router'
// 导入自动生成布局的工具函数和路由
import { setupLayouts } from 'virtual:generated-layouts'
import generatedRoutes from '~pages'
// 导入Pinia状态管理库
import { defineStore } from 'pinia'
import { message } from 'ant-design-vue'
import { ref } from 'vue'// 定义基础路由配置(所有用户都可访问)
const baseRoutes = [{path: '/login', name: 'Login', component: () => import('@/views/Login.vue'),meta: { public: true } // 元信息,标记为公开路由},{path: '/', name: 'Home',component: () => import('@/views/Home.vue'),meta: { title: '首页' } // 页面标题},{path: '/:pathMatch(.*)*',name: 'NotFound',component: () => import('@/views/NotFound.vue'), meta: { public: true } // 标记为公开路由}
]// 定义需要权限的动态路由配置
const dynamicRoutes = [{path: '/system', name: 'System', component: () => import('@/views/System/index.vue'),meta: { title: '系统管理', roles: ['admin'] }, // 需要admin角色children: [ {path: 'users', name: 'SystemUsers',component: () => import('@/views/System/Users.vue'),meta: { title: '用户管理', roles: ['admin'] } }]},{path: '/business', name: 'Business', component: () => import('@/views/Business/index.vue'), meta: { title: '业务管理', roles: ['admin', 'user'] }, // admin和user都可访问children: [ {path: 'orders', name: 'BusinessOrders',component: () => import('@/views/Business/Orders.vue'),meta: { title: '订单管理', roles: ['admin', 'user'] } }]}
]// 创建路由实例
const router = createRouter({history: createWebHistory(), // 使用HTML5 history模式routes: [ // 合并路由配置...baseRoutes, // 展开基础路由...setupLayouts(generatedRoutes) // 展开自动生成的路由并应用布局]
})// 权限存储
const usePermissionStore = defineStore('permission', () => {// 标记路由是否已加载const isRoutesLoaded = ref(false)// 根据用户角色生成可访问路由const generateRoutes = (roles) => {return dynamicRoutes.filter(route => {// 如果路由不需要角色或用户有权限则保留return !route.meta?.roles || roles.some(role => route.meta.roles.includes(role))})}// 设置路由加载状态const setRoutesLoaded = (loaded) => {isRoutesLoaded.value = loaded}// 暴露状态和方法return {isRoutesLoaded,generateRoutes,setRoutesLoaded}
})// 路由守卫设置
export function setupRouterGuards() {// 注册全局前置守卫(每次路由跳转前执行)router.beforeEach(async (to, from, next) => {// 1. 公共路由直接放行if (to.meta.public) return next()// 获取用户和权限store实例const userStore = useUserStore()const permissionStore = usePermissionStore()// 2. 登录状态验证if (!userStore.isLogin) {// 未登录时重定向到登录页,并携带当前路径return next({name: 'Login',query: { redirect: to.fullPath }})}// 3. 动态路由加载if (!permissionStore.isRoutesLoaded) {try {// 获取用户信息(包含角色)await userStore.fetchUserInfo()// 根据用户角色生成可访问路由(传入角色列表)const accessRoutes = permissionStore.generateRoutes(userStore.roles)// 动态添加路由accessRoutes.forEach(route => {router.addRoute(route)})// 标记路由已加载permissionStore.setRoutesLoaded(true)// 重定向到原本要去的页面(替换当前历史记录)return next({ ...to, replace: true })} catch (error) {// 错误处理:显示错误信息并登出message.error(error.message || '权限加载失败')userStore.logout()return next({ name: 'Login' })}}// 4. 权限检查if (to.meta.roles && !userStore.roles.some(role => to.meta.roles.includes(role))) {// 无权限访问时跳转404页return next({ name: 'NotFound' })}// 所有检查通过,继续导航next()})
}// 导出路由实例
export { router }
五、使用方法
main.js
// 在 main.js 中
import { createApp } from 'vue'
import App from './App.vue'
import { router, setupRouterGuards } from './router'const app = createApp(App)
app.use(router)
setupRouterGuards()
app.mount('#app')