Vue3后台管理系统(四):认证与权限系统实现
一、认证系统设计
1.1 认证流程概述
基于JWT(JSON Web Token)实现,完整的认证流程如下:
- 用户输入账号密码,前端加密后发送到服务器
- 服务器验证成功后返回access_token和refresh_token
- 前端存储token,并在后续请求中携带
- 访问需要权限的资源时,服务端验证token有效性和权限
- token过期时,使用refresh_token获取新的access_token
1.2 Token管理
通过封装TokenService来管理token的存储和验证:
javascript
// src/utils/token.js
/**
* @description Token服务类
* @Author Wangkaibing
* @Date 2025-05-06
*
* Usage:
* import { TokenService } from '@/utils/token'
*
* // 获取token
* TokenService.getToken()
*
* // 设置token
* TokenService.setTokens(accessToken, refreshToken, expiresIn)
*
* // 清除token
* TokenService.clearTokens()
*/
import { setCookie, getCookie, removeCookie } from '@/utils/cookie'
export class TokenService {
static TOKEN_KEY = 'xxxx'
static REFRESH_TOKEN_KEY = 'xxxx'
static getToken() {
return getCookie(this.TOKEN_KEY)
}
static getRefreshToken() {
return getCookie(this.REFRESH_TOKEN_KEY)
}
static setTokens(accessToken, refreshToken = '', expiresIn = 7200) {
if (accessToken) {
setCookie(this.TOKEN_KEY, accessToken, expiresIn)
}
if (refreshToken) {
setCookie(this.REFRESH_TOKEN_KEY, refreshToken, expiresIn)
}
}
static clearTokens() {
removeCookie(this.TOKEN_KEY)
removeCookie(this.REFRESH_TOKEN_KEY)
}
static isAuthenticated() {
return !(!this.getToken() && !this.getRefreshToken())
}
}
二、登录功能实现
2.1 登录组合式API
使用组合式API(Composition API)封装登录相关功能:
javascript
/**
* @description 用户登录管理hook
* @Author Wangkaibing
* @Date 2025-04-13
*
* Usage:
* import { useLogin } from '@/composables/useLogin'
*
* const { login, logout } = useLogin()
* await login(loginForm)
*/
import { ref, reactive, computed, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { useGet, usePost } from '@/api/request'
import { useAuthStore } from '@/stores/auth'
import { useUser } from '@/composables/useUser'
import { usePermissionStore } from '@/composables/usePermission'
import { useEncryption } from '@/composables/useEncryption.js'
import { useRouter } from 'vue-router'
import { TokenService } from '@/utils/token.js'
import { invalidateCache } from 'alova'
/**
* 登录管理hook
* 登录、退出、获取验证码等功能
* @returns {Object} 登录控制对象
*/
export function useLogin() {
const router = useRouter()
const authStore = useAuthStore()
const { getUserInfo } = useUser()
const permissionStore = usePermissionStore()
const { encryptLoginData } = useEncryption()
const loading = ref(false)
const loginState = reactive({
client: '',
codeImage: '',
})
/**
* 获取验证码
* @param {boolean} isInit - 是否是初始化调用
*/
const getCaptcha = async (isInit = false) => {
try {
const params = {
width: 100,
height: 42,
}
if (!isInit && loginState.client) {
params.client = loginState.client
}
const res = await useGet('/api/xxxx', params, {
loading: false,
})
if (res.image) {
loginState.client = res.client
loginState.codeImage = res.image
}
} catch (error) {
console.log('🚀 ~ [error]', error)
}
}
/**
* 用户登录
* @param {Object} loginForm - 登录表单数据
*/
const login = async (loginForm) => {
if (loading.value) return
try {
loading.value = true
const loginData = encryptLoginData({
username: loginForm.username,
password: loginForm.password,
verificationCode: loginForm.captcha,
client: loginState.client,
})
const loginRes = await usePost('/api/xxxxx', loginData)
if (!loginRes?.data) {
throw new Error(loginRes?.message || '登录失败')
}
// 清除接口缓存
invalidateCache()
// 保存token
const { token_type, access_token, refresh_token, expires_in } = loginRes
TokenService.setTokens(token_type + ' ' + access_token, refresh_token, expires_in * 1000)
// 获取用户信息
await getUserInfo()
// 生成路由
const accessRoutes = await permissionStore.generateRoutes(authStore.state.roles)
// 添加动态路由
accessRoutes.forEach((route) => {
router.addRoute(route)
})
ElMessage({
message: '登录成功:欢迎回来',
type: 'success',
plain: true,
})
await nextTick()
await router.replace('/')
} catch (error) {
console.log('🚀 ~ [error]', error)
// 发生错误时重置状态
authStore.reset()
permissionStore.resetRoutes()
getCaptcha(false)
throw error
} finally {
loading.value = false
}
}
/**
* 退出登录
*/
const logout = async () => {
try {
// 清除接口缓存
invalidateCache()
await router.push('/login')
authStore.reset()
permissionStore.resetRoutes()
} catch (error) {
console.log('🚀 ~ [error]', error)
authStore.reset()
permissionStore.resetRoutes()
// 使用location作为备用方案
window.location.href = '/login'
}
}
return {
loading,
codeImage: computed(() => loginState.codeImage),
getCaptcha,
login,
logout,
}
}
三、权限系统实现
3.1 基于角色的权限控制
使用组合式API封装了权限控制逻辑:
javascript
/**
* @description 路由权限管理hook
* @Author Wangkaibing
* @Date 2025-04-13
*
* Usage:
* import { usePermissionStore } from '@/composables/usePermission'
*
* const permissionStore = usePermissionStore()
* await permissionStore.generateRoutes(roles)
*/
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import { constantRoutes, defaultRoutes, platform, catchAllRoute } from '@/router/routes'
export const usePermissionStore = defineStore('permission', () => {
const routes = ref([...constantRoutes])
const addRoutes = ref([])
// 添加路由元数据缓存
const routeMetaCache = ref(new Map())
// 添加角色路由映射缓存
const roleRoutesCache = ref(new Map())
// 路由是否已经生成
const isRoutesGenerated = computed(() => addRoutes.value.length > 0)
/**
* 检查路由权限
* @param {Array} roles - 用户角色列表
* @param {Object} route - 路由对象
* @returns {boolean} - 是否有权限
*/
const hasPermission = (roles, route) => {
// 生成缓存键
const routePath = route.path
const roleKey = roles.sort().join(',')
const cacheKey = `${routePath}:${roleKey}`
// 检查缓存中是否存在
if (routeMetaCache.value.has(cacheKey)) {
return routeMetaCache.value.get(cacheKey)
}
// 计算权限并缓存结果
let hasAccess = true
if (route.meta?.roles) {
hasAccess = roles.some((role) => route.meta.roles.includes(role))
}
// 保存到缓存
routeMetaCache.value.set(cacheKey, hasAccess)
return hasAccess
}
/**
* 过滤异步路由
* @param {Array} routes - 路由配置数组
* @param {Array} roles - 用户角色列表
* @returns {Array} - 过滤后的路由
*/
const filterAsyncRoutes = (routes, roles) => {
return routes
.filter((route) => hasPermission(roles, route))
.map((route) => {
// 创建路由的浅拷贝
const tmp = { ...route }
// 递归子路由
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
return tmp
})
}
/**
* 生成可访问路由
* @param {Array} roles - 用户角色列表
* @returns {Promise<Array>} - 可访问路由列表
*/
const generateRoutes = (roles) => {
// 生成角色缓存键
const roleKey = roles.sort().join(',')
// 如果角色路由缓存中已存在,直接返回缓存结果
if (roleRoutesCache.value.has(roleKey)) {
const cachedRoutes = roleRoutesCache.value.get(roleKey)
addRoutes.value = cachedRoutes
routes.value = [...constantRoutes, ...cachedRoutes]
return Promise.resolve(cachedRoutes)
}
// 角色路由映射表,配置每种角色对应的路由生成方法
const roleRouteMap = {
'ROLE_ADMIN': () => [...defaultRoutes, ...platform, catchAllRoute],
'ROLE_MANAGER': () => [...defaultRoutes, ...filterAsyncRoutes(platform, roles), catchAllRoute],
'ROLE_SUPERVISOR': () => [...defaultRoutes, ...filterAsyncRoutes(platform, roles), catchAllRoute],
}
// 默认路由
const defaultRouteFn = () => [...defaultRoutes, catchAllRoute]
// 查找用户拥有的最高权限角色
let routeGenerator = defaultRouteFn
// 遍历角色,找到对应路由
for (const role of roles) {
if (roleRouteMap[role]) {
routeGenerator = roleRouteMap[role]
break // 找到第一个匹配的角色就停止
}
}
// 生成路由
const accessedRoutes = routeGenerator()
// 更新状态
addRoutes.value = accessedRoutes
routes.value = [...constantRoutes, ...accessedRoutes]
// 保存到角色路由缓存
roleRoutesCache.value.set(roleKey, accessedRoutes)
return Promise.resolve(accessedRoutes)
}
/**
* 重置路由配置
*/
const resetRoutes = () => {
// 清空动态添加的路由
addRoutes.value = []
// 重置为初始路由
routes.value = [...constantRoutes]
// 清空路由元数据缓存
routeMetaCache.value.clear()
// 清空角色路由映射缓存
roleRoutesCache.value.clear()
}
return {
routes,
addRoutes,
isRoutesGenerated,
generateRoutes,
resetRoutes,
}
})
3.2 访问控制指令
为了在页面中控制元素的显示,实现自定义指令:
javascript
/**
* @description 权限访问控制hook
* @Author Wangkaibing
* @Date 2025-04-13
*
* Usage:
* import { useAccess } from '@/composables/useAccess'
*
* const { hasRole, hasRoles } = useAccess()
* hasRole('ROLE_ADMIN')
*/
import { computed } from 'vue'
import { defineStore } from 'pinia'
import { useAuthStore } from '@/stores/auth'
export const useAccessStore = defineStore('access', () => {
const authStore = useAuthStore()
// 计算属性:判断是否为管理员
const isAdmin = computed(() => {
return authStore.state.roles.includes('ROLE_ADMIN')
})
return {
isAdmin,
}
})
/**
* 权限访问控制hook
* 提供基于角色的权限验证功能
*/
export function useAccess() {
const accessStore = useAccessStore()
const authStore = useAuthStore()
/**
* 判断是否拥有指定角色
* @param {string} role - 角色标识
* @returns {boolean} - 是否拥有该角色
*/
const hasRole = (role) => {
if (!role || !authStore.state.roles.length) {
return false
}
return authStore.state.roles.includes(role)
}
/**
* 判断是否拥有指定角色集合中的任意一个
* @param {string[]} roles - 角色标识数组
* @param {boolean} every - 是否需要满足所有角色,默认false
* @returns {boolean} - 是否满足角色要求
*/
const hasRoles = (roles, every = false) => {
if (!roles || !roles.length || !authStore.state.roles.length) {
return false
}
const verify = (role) => hasRole(role)
return every ? roles.every(verify) : roles.some(verify)
}
// 角色指令
const vRole = {
mounted(el, binding) {
const { value } = binding
const hasPermission = Array.isArray(value) ? hasRoles(value) : hasRole(value)
if (!hasPermission) {
el.parentNode?.removeChild(el)
}
},
}
return {
...accessStore,
hasRole,
hasRoles,
vRole,
}
}
自定义指令的注册:
javascript
/**
* @description 角色权限指令
* @Author Wangkaibing
* @Date 2025-04-13
* @Usage
* // 单个角色
* <button v-role="'ROLE_ADMIN'">管理员操作</button>
*
* // 多个角色(满足其一)
* <button v-role="['ROLE_ADMIN', 'ROLE_MANAGER']">管理操作</button>
*/
import { useAccess } from '@/composables/useAccess'
/**
* 注册全局角色权限指令
* @param {Object} app - Vue应用实例
*/
export function setupPermissionDirective(app) {
const { vRole } = useAccess()
// 注册 v-role 指令
app.directive('role', vRole)
}
3.3 路由守卫中的权限控制
在路由守卫中,实现基于token和角色的访问控制:
javascript
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { constantRoutes } from './routes'
import { ElNotification } from 'element-plus'
import { useAuthStore } from '@/stores/auth'
import { useUser } from '@/composables/useUser'
import { usePermissionStore } from '@/composables/usePermission'
import NProgress from 'nprogress'
import '@/styles/nprogress.scss'
import { TokenService } from '@/utils/token.js'
// 配置 NProgress
NProgress.configure({
easing: 'ease-out',
speed: 400,
showSpinner: false,
trickleSpeed: 200,
minimum: 0.3,
})
// 修复重复点击路由报错问题
const originalPush = createRouter.prototype.push
createRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch((err) => err)
}
const router = createRouter({
history: createWebHistory(),
scrollBehavior: () => ({ top: 0 }),
routes: [...constantRoutes],
})
// 异步组件加载失败
router.onError((error) => {
const pattern = /Loading chunk (\d)+ failed/g
const isChunkLoadFailed = error.message.match(pattern)
if (isChunkLoadFailed) {
window.location.reload()
}
})
// 路由白名单
const whiteList = ['/login']
// 路由守卫
router.beforeEach(async (to, from, next) => {
// 开启进度条
NProgress.start()
const authStore = useAuthStore()
const { getUserInfo } = useUser()
const permissionStore = usePermissionStore()
// 白名单直接通过
if (whiteList.includes(to.path)) {
return next()
}
// URL中的token
if (to.query.token) {
const token = to.query.token
TokenService.setTokens('Bearer ' + token)
// 移除token参数,避免暴露在URL中
const { token: _, ...query } = to.query
return next({ path: to.path, query })
}
// 检查是否已登录
if (!authStore.isAuthenticated) {
ElNotification({
title: '登录失败',
message: '登录过期',
type: 'error',
})
return next(`/login?redirect=${to.path}`)
}
try {
// 只在首次加载或刷新页面时执行路由生成
if (!permissionStore.isRoutesGenerated) {
// 如果没有角色信息,先获取用户信息
if (!authStore.hasRoles) {
await getUserInfo()
}
// 生成动态路由
const accessRoutes = await permissionStore.generateRoutes(authStore.state.roles)
// 添加动态路由
accessRoutes.forEach((route) => {
router.addRoute(route)
})
// 重定向到原目标,确保路由已更新
return next({ ...to, replace: true })
}
// 默认首页重定向到账户页
if (to.path === '/') {
return next({ path: '/my/account', replace: true })
}
next()
} catch (error) {
// 异常
console.log('🚀 ~ [error]', error)
authStore.reset()
permissionStore.resetRoutes()
ElNotification({
title: '错误',
message: error.message || '登录失败,请重新登录',
type: 'error',
})
next('/login')
}
})
// 路由后置守卫
router.afterEach(() => {
// 结束进度条
NProgress.done()
})
四、前端权限应用场景
4.1 基于角色的按钮权限
在实际业务中可以这样使用自定义指令:
vue
<template>
<!-- 根据角色控制按钮显示 -->
<el-button v-role="'ROLE_ADMIN'" type="primary">管理员专用按钮</el-button>
<!-- 支持多个角色(满足一个即可显示) -->
<el-button v-role="['ROLE_ADMIN', 'ROLE_MANAGER']">管理操作按钮</el-button>
</template>