前端路由守卫:pig-ui权限路由实现
【免费下载链接】pig 项目地址: https://gitcode.com/gh_mirrors/pig/pig
引言
在现代前端应用中,路由守卫(Route Guard)是实现用户认证和权限控制的关键技术。它能够在用户访问特定路由前进行权限检查,确保只有具备相应权限的用户才能访问受保护的资源。本文将深入探讨pig-ui权限路由的实现原理,从后端权限模型到前端路由守卫的具体实现,为开发者提供一套完整的权限路由解决方案。
权限路由概述
什么是权限路由
权限路由(Permission-based Routing)是一种根据用户权限动态生成和控制路由访问的机制。它能够根据用户的角色和权限,动态地展示或隐藏应用中的某些路由,从而实现精细化的权限控制。
权限路由的核心价值
- 安全性增强:防止未授权用户访问敏感资源
- 用户体验优化:根据用户权限展示个性化菜单
- 系统可维护性:集中管理权限与路由的映射关系
- 动态扩展性:支持运行时动态更新权限路由
权限路由实现的技术挑战
- 前后端权限模型的一致性
- 路由守卫与权限检查的性能优化
- 动态路由的加载与卸载
- 复杂权限场景的处理(如数据权限)
pig-ui权限路由架构设计
整体架构
权限模型设计
pig-ui采用RBAC(Role-Based Access Control,基于角色的访问控制)模型,其核心实体包括:
- 用户(User):系统的操作者
- 角色(Role):具有相同权限的用户集合
- 菜单(Menu):系统的功能模块和路由节点
- 权限(Permission):具体的操作权限
数据流转流程
后端权限模型实现
菜单实体设计
在pig-ui中,菜单实体(SysMenu)是权限路由的核心数据结构:
public class SysMenu {
private Long menuId; // 菜单ID
private String name; // 菜单名称
private String permission; // 菜单权限标识
private Long parentId; // 父菜单ID
private String icon; // 菜单图标
private String path; // 前端路由标识路径
private String visible; // 菜单显示隐藏控制
private Integer sortOrder; // 排序值
private String menuType; // 菜单类型(0菜单 1按钮)
private String keepAlive; // 路由缓冲
// 其他属性...
}
菜单权限服务实现
SysMenuService接口定义了菜单权限的核心操作:
public interface SysMenuService extends IService<SysMenu> {
/**
* 通过角色编号查询URL权限
* @param roleId 角色ID
* @return 菜单列表
*/
List<SysMenu> findMenuByRoleId(Long roleId);
/**
* 构建菜单树
* @param parentId 父节点ID
* @param menuName 菜单名称
* @return 菜单树结构
*/
List<Tree<Long>> treeMenu(Long parentId, String menuName, String type);
// 其他方法...
}
权限菜单树构建
菜单树的构建是权限路由实现的关键步骤,它将扁平的菜单列表转换为层级结构:
public List<Tree<Long>> treeMenu(Long parentId, String menuName, String type) {
// 1. 查询符合条件的菜单列表
QueryWrapper<SysMenu> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("parent_id", parentId);
if (StrUtil.isNotBlank(menuName)) {
queryWrapper.like("name", menuName);
}
if (StrUtil.isNotBlank(type)) {
queryWrapper.eq("menu_type", type);
}
queryWrapper.orderByAsc("sort_order");
List<SysMenu> menuList = baseMapper.selectList(queryWrapper);
// 2. 转换为树结构
List<Tree<Long>> treeList = menuList.stream().map(menu -> {
Tree<Long> tree = new Tree<>();
tree.setId(menu.getMenuId());
tree.setParentId(menu.getParentId());
tree.setName(menu.getName());
Map<String, Object> extra = new HashMap<>();
extra.put("path", menu.getPath());
extra.put("icon", menu.getIcon());
extra.put("permission", menu.getPermission());
extra.put("menuType", menu.getMenuType());
extra.put("visible", menu.getVisible());
extra.put("keepAlive", menu.getKeepAlive());
tree.setExtra(extra);
return tree;
}).collect(Collectors.toList());
// 3. 构建树形结构
return TreeUtil.build(treeList, parentId);
}
前端权限路由实现
路由配置结构
pig-ui的路由配置包含基本路由和动态路由两部分:
// 基本路由(无需权限即可访问)
const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/error-page/404'),
hidden: true
}
]
// 动态路由(需要权限控制)
const asyncRoutes = [
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: '仪表盘', icon: 'dashboard', affix: true }
}
]
}
]
路由元信息设计
路由元信息(meta)是实现权限控制的关键,包含以下核心字段:
{
path: '/system',
component: Layout,
redirect: '/system/user',
name: 'System',
meta: {
title: '系统管理',
icon: 'system',
permission: ['system:manage'] // 所需权限
},
children: [
{
path: 'user',
name: 'User',
component: () => import('@/views/system/user/index'),
meta: {
title: '用户管理',
icon: 'user',
permission: ['system:user:view'] // 细粒度权限控制
}
}
]
}
路由守卫实现
路由守卫是权限控制的核心实现,主要通过Vue Router的导航守卫实现:
// src/router/index.js
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'
// 白名单路由
const whiteList = ['/login', '/auth-redirect']
// 全局前置守卫
router.beforeEach(async(to, from, next) => {
NProgress.start()
// 1. 检查是否已登录
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// 已登录,访问登录页,重定向到首页
next({ path: '/' })
NProgress.done()
} else {
// 2. 检查是否已加载用户权限信息
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
// 已加载权限信息,直接进入
next()
} else {
try {
// 3. 获取用户权限信息
const { roles, permissions } = await store.dispatch('user/getInfo')
// 4. 生成权限路由
const accessRoutes = await store.dispatch('permission/generateRoutes', {
roles,
permissions
})
// 5. 动态添加路由
router.addRoutes(accessRoutes)
// 6. 继续路由导航
next({ ...to, replace: true })
} catch (error) {
// 出错时,重置token并重定向到登录页
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
// 未登录状态
if (whiteList.indexOf(to.path) !== -1) {
// 在白名单中,直接进入
next()
} else {
// 不在白名单中,重定向到登录页
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
// 全局后置守卫
router.afterEach(() => {
NProgress.done()
})
权限路由生成算法
权限路由生成是将后端返回的权限菜单转换为前端路由配置的过程:
// src/store/modules/permission.js
import { constantRoutes, asyncRoutes } from '@/router'
/**
* 检查路由是否有权限访问
* @param route 路由配置
* @param permissions 权限列表
*/
function hasPermission(route, permissions) {
if (route.meta && route.meta.permission) {
// 如果路由配置了权限,则检查是否有权限
return permissions.some(permission => route.meta.permission.includes(permission))
} else {
// 未配置权限的路由默认有权限访问
return true
}
}
/**
* 过滤权限路由
* @param routes 路由列表
* @param permissions 权限列表
*/
function filterAsyncRoutes(routes, permissions) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
// 检查当前路由是否有权限
if (hasPermission(tmp, permissions)) {
// 递归过滤子路由
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, permissions)
}
res.push(tmp)
}
})
return res
}
const state = {
routes: [],
addRoutes: []
}
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
}
const actions = {
generateRoutes({ commit }, { roles, permissions }) {
return new Promise(resolve => {
let accessedRoutes
// 管理员角色拥有所有权限
if (roles.includes('admin')) {
accessedRoutes = asyncRoutes || []
} else {
// 根据权限过滤路由
accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
}
// 提交路由配置
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
高级功能实现
动态菜单渲染
根据权限路由动态生成侧边栏菜单:
<!-- src/components/Sidebar/SidebarItem.vue -->
<template>
<div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item.children, item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
!item.alwaysShow">
<router-link
:to="resolvePath(onlyOneChild.path)"
:class="{'submenu-title-noDropdown': !isNest}"
>
<el-tooltip :disabled="isNest" :content="item.meta.title" placement="right">
<svg-icon :icon-class="item.meta.icon || (onlyOneChild.meta && onlyOneChild.meta.icon)"/>
<span slot="title">{{ item.meta.title }}</span>
</el-tooltip>
</router-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)">
<template slot="title">
<el-tooltip :disabled="isNest" :content="item.meta.title" placement="right">
<svg-icon :icon-class="item.meta.icon"/>
<span slot="title">{{ item.meta.title }}</span>
</el-tooltip>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</div>
</template>
<script>
export default {
name: 'SidebarItem',
components: { SidebarItem },
props: {
// 当前路由配置
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
},
data() {
return {
onlyOneChild: null
}
},
methods: {
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter(item => {
// 过滤隐藏的路由
if (item.hidden) {
return false
} else {
// 临时保存子路由
this.onlyOneChild = item
return true
}
})
// 如果只有一个子路由且不是隐藏的
if (showingChildren.length === 1) {
return true
}
// 显示父路由但不显示子路由
if (showingChildren.length === 0 && parent.meta.alwaysShow) {
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
return true
}
return false
},
resolvePath(routePath) {
return this.basePath ? path.resolve(this.basePath, routePath) : routePath
}
}
}
</script>
路由缓存策略
pig-ui实现了基于路由元信息的缓存策略:
// src/components/Layout/TagsView/index.vue
<template>
<div class="tags-view-container">
<scroll-pane class="tags-view-wrapper" ref="scrollPane">
<router-link
v-for="tag in visitedViews"
ref="tag"
:key="tag.path"
:class="isActive(tag) ? 'active' : ''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span"
class="tags-view-item"
@click.middle.native="closeSelectedTag(tag)"
@contextmenu.prevent.native="openMenu(tag, $event)"
>
{{ tag.title }}
<span v-if="!tag.affix" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
</router-link>
</scroll-pane>
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)">刷新</li>
<li v-if="!selectedTag.affix" @click="closeSelectedTag(selectedTag)">关闭</li>
<li @click="closeOthersTags">关闭其他</li>
<li @click="closeAllTags">关闭全部</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
visitedViews: [],
cachedViews: [],
visible: false,
selectedTag: {},
left: 0,
top: 0
}
},
watch: {
$route() {
this.addTags()
this.moveToCurrentTag()
},
// 监听缓存路由变化
cachedViews(newVal) {
const cacheList = newVal.map(item => item.name)
this.$store.dispatch('tagsView/delCacheViews', cacheList)
}
},
methods: {
addTags() {
const route = this.getRoute()
// 如果路由是隐藏的或重定向的,则不添加到标签页
if (route.hidden || route.redirect === 'noRedirect') {
return
}
// 添加到访问过的视图
if (!this.visitedViews.some(v => v.path === route.path)) {
this.visitedViews.push(Object.assign({}, route, {
title: route.meta.title || 'no-name'
}))
}
// 添加到缓存视图(如果设置了keepAlive)
if (route.meta.keepAlive && !this.cachedViews.some(v => v.name === route.name)) {
this.cachedViews.push({ name: route.name })
}
// 限制标签页数量
this.limitVisitedViews()
},
// 限制标签页数量
limitVisitedViews() {
const maxNum = this.$store.getters.tagsViewNum
if (this.visitedViews.length > maxNum) {
const delView = this.visitedViews.shift()
this.cachedViews = this.cachedViews.filter(item => item.name !== delView.name)
}
},
// 判断当前路由是否激活
isActive(route) {
return route.path === this.$route.path
},
// 移动到当前标签
moveToCurrentTag() {
this.$nextTick(() => {
const tags = this.$refs.tag
const activeTag = tags.find(tag => tag.to.path === this.$route.path)
if (activeTag) {
this.$refs.scrollPane.moveToTarget(activeTag)
}
})
},
// 关闭选中的标签
closeSelectedTag(view) {
// 如果是固定标签,则不能关闭
if (view.affix) return
// 查找标签索引
const index = this.visitedViews.findIndex(v => v.path === view.path)
this.visitedViews.splice(index, 1)
// 如果关闭的是当前激活的标签,则跳转到前一个标签
if (this.isActive(view)) {
const latestView = this.visitedViews[index === 0 ? 0 : index - 1]
if (latestView) {
this.$router.push(latestView)
} else {
this.$router.push('/')
}
}
// 从缓存中移除
this.cachedViews = this.cachedViews.filter(item => item.name !== view.name)
}
// 其他方法...
}
}
</script>
复杂权限场景处理
对于复杂的权限场景,如数据权限,pig-ui采用了以下解决方案:
// src/utils/permission.js
import store from '@/store'
/**
* 检查是否有权限
* @param {Array} value 权限列表
* @returns {Boolean} 是否有权限
*/
export function checkPermission(value) {
if (value && value instanceof Array && value.length > 0) {
const permissions = store.getters && store.getters.permissions
const hasPermission = permissions.some(permission => {
return value.includes(permission)
})
return hasPermission
} else {
console.error('need permissions! Like checkPermission(["system:user:add"])')
return false
}
}
/**
* 检查数据权限
* @param {Object} row 数据行
* @param {String} permission 数据权限标识
* @returns {Boolean} 是否有数据权限
*/
export function checkDataPermission(row, permission) {
const permissions = store.getters && store.getters.permissions
// 1. 检查是否有全部数据权限
if (permissions.includes('*:*:*')) {
return true
}
// 2. 检查是否有指定数据权限
if (permissions.includes(permission)) {
return true
}
// 3. 检查行级数据权限(如只能查看自己创建的数据)
const userId = store.getters.userId
if (row.createBy === userId && permissions.includes('own:*:*')) {
return true
}
return false
}
// 注册全局权限检查指令
export function registerPermissionDirective(app) {
// v-permission 指令
app.directive('permission', {
mounted(el, binding) {
const { value } = binding
if (!checkPermission(value)) {
el.parentNode && el.parentNode.removeChild(el)
}
}
})
// v-data-permission 指令
app.directive('data-permission', {
mounted(el, binding) {
const { value } = binding
const { row, permission } = value
if (!checkDataPermission(row, permission)) {
el.parentNode && el.parentNode.removeChild(el)
}
}
})
}
性能优化策略
权限路由的性能瓶颈
- 权限检查的频繁执行
- 动态路由的频繁更新
- 大型应用中路由数量过多
优化方案
- 权限检查结果缓存
// src/utils/permission.js
const permissionCache = new Map()
// 带缓存的权限检查
export function checkPermissionWithCache(value) {
const cacheKey = JSON.stringify(value)
// 如果缓存中有结果,直接返回
if (permissionCache.has(cacheKey)) {
return permissionCache.get(cacheKey)
}
// 执行权限检查
const result = checkPermission(value)
// 缓存检查结果
permissionCache.set(cacheKey, result)
return result
}
// 用户权限更新时清除缓存
export function clearPermissionCache() {
permissionCache.clear()
}
- 路由懒加载优化
// 路由组件懒加载
const _import = require('@/router/_import_' + process.env.NODE_ENV)
// 路由配置
{
path: 'user',
name: 'User',
component: _import('system/user/index'), // 根据环境选择不同的加载方式
meta: {
title: '用户管理',
icon: 'user',
permission: ['system:user:view']
}
}
// router/_import_development.js (开发环境)
module.exports = file => require('@/views/' + file).default
// router/_import_production.js (生产环境)
module.exports = file => () => import('@/views/' + file)
- 路由预加载策略
// src/store/modules/permission.js
const actions = {
// 预加载关键路由组件
preloadCriticalRoutes({ state }) {
return new Promise(resolve => {
// 找出关键路由(如首页、仪表盘等)
const criticalRoutes = state.routes.filter(route =>
route.meta && route.meta.critical
)
// 预加载组件
criticalRoutes.forEach(route => {
if (typeof route.component === 'function' && route.component.toString().includes('import(')) {
route.component().then(() => {
console.log(`Preloaded critical route: ${route.path}`)
})
}
})
resolve()
})
}
}
最佳实践与常见问题
最佳实践
-
权限设计原则
- 最小权限原则:只授予用户必要的权限
- 权限粒度适中:避免过细或过粗的权限划分
- 权限命名规范:采用"资源:操作:实例"的命名方式
-
路由守卫使用建议
- 合理使用全局守卫、路由独享守卫和组件内守卫
- 避免在守卫中执行耗时操作
- 守卫中异常处理要完善
-
动态路由管理
- 权限变更时及时更新路由
- 退出登录时清理动态路由
- 路由更新时处理好当前页面状态
常见问题及解决方案
-
刷新页面后权限路由丢失
解决方案:在页面加载时重新获取权限并生成路由
// src/main.js
// 页面加载时检查权限路由
if (getToken()) {
store.dispatch('user/getInfo').then(() => {
store.dispatch('permission/generateRoutes').then(accessRoutes => {
router.addRoutes(accessRoutes)
})
})
}
-
动态路由添加后导航不生效
解决方案:使用
next({ ...to, replace: true })重新导航 -
复杂权限场景下的路由匹配问题
解决方案:实现自定义路由匹配逻辑
// src/router/matcher.js
import { createRouterMatcher } from 'vue-router'
export function createCustomRouterMatcher(routes, options) {
const matcher = createRouterMatcher(routes, options)
// 自定义匹配函数
const customAddRoute = matcher.addRoute
matcher.addRoute = (route, parent) => {
// 添加自定义路由匹配逻辑
if (route.meta && route.meta.dynamic) {
// 动态路由处理逻辑
}
return customAddRoute(route, parent)
}
return matcher
}
总结与展望
核心要点总结
- pig-ui权限路由基于RBAC模型设计,实现了菜单、角色、权限的三级权限控制
- 通过路由守卫实现了路由访问前的权限检查
- 采用动态路由生成技术,根据用户权限动态构建路由表
- 实现了路由缓存、数据权限等高级功能
- 通过缓存、懒加载等技术优化了权限路由的性能
未来发展方向
- 基于组件的细粒度权限控制:将权限控制下沉到组件级别
- 权限路由的可视化配置:提供可视化界面配置权限与路由的映射关系
- 权限动态更新机制:实现无需刷新页面的权限实时更新
- 微前端架构下的权限路由:解决微前端场景下的跨应用权限控制问题
结语
权限路由是现代前端应用不可或缺的核心功能,它不仅关系到系统的安全性,也影响着用户体验和系统可维护性。pig-ui权限路由实现方案通过前后端协同,构建了一套完整的权限控制体系,为开发者提供了灵活、高效、安全的权限路由解决方案。随着前端技术的不断发展,权限路由也将朝着更智能、更精细化的方向演进。
希望本文能够帮助开发者深入理解权限路由的实现原理,在实际项目中构建出更安全、更灵活的权限控制体系。如有任何问题或建议,欢迎在评论区留言讨论。
点赞、收藏、关注,获取更多前端权限控制的实践经验和最佳实践!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



