前端路由守卫:pig-ui权限路由实现

前端路由守卫:pig-ui权限路由实现

【免费下载链接】pig 【免费下载链接】pig 项目地址: https://gitcode.com/gh_mirrors/pig/pig

引言

在现代前端应用中,路由守卫(Route Guard)是实现用户认证和权限控制的关键技术。它能够在用户访问特定路由前进行权限检查,确保只有具备相应权限的用户才能访问受保护的资源。本文将深入探讨pig-ui权限路由的实现原理,从后端权限模型到前端路由守卫的具体实现,为开发者提供一套完整的权限路由解决方案。

权限路由概述

什么是权限路由

权限路由(Permission-based Routing)是一种根据用户权限动态生成和控制路由访问的机制。它能够根据用户的角色和权限,动态地展示或隐藏应用中的某些路由,从而实现精细化的权限控制。

权限路由的核心价值

  1. 安全性增强:防止未授权用户访问敏感资源
  2. 用户体验优化:根据用户权限展示个性化菜单
  3. 系统可维护性:集中管理权限与路由的映射关系
  4. 动态扩展性:支持运行时动态更新权限路由

权限路由实现的技术挑战

  • 前后端权限模型的一致性
  • 路由守卫与权限检查的性能优化
  • 动态路由的加载与卸载
  • 复杂权限场景的处理(如数据权限)

pig-ui权限路由架构设计

整体架构

mermaid

权限模型设计

pig-ui采用RBAC(Role-Based Access Control,基于角色的访问控制)模型,其核心实体包括:

  1. 用户(User):系统的操作者
  2. 角色(Role):具有相同权限的用户集合
  3. 菜单(Menu):系统的功能模块和路由节点
  4. 权限(Permission):具体的操作权限

数据流转流程

mermaid

后端权限模型实现

菜单实体设计

在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)
      }
    }
  })
}

性能优化策略

权限路由的性能瓶颈

  • 权限检查的频繁执行
  • 动态路由的频繁更新
  • 大型应用中路由数量过多

优化方案

  1. 权限检查结果缓存
// 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()
}
  1. 路由懒加载优化
// 路由组件懒加载
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)
  1. 路由预加载策略
// 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()
    })
  }
}

最佳实践与常见问题

最佳实践

  1. 权限设计原则

    • 最小权限原则:只授予用户必要的权限
    • 权限粒度适中:避免过细或过粗的权限划分
    • 权限命名规范:采用"资源:操作:实例"的命名方式
  2. 路由守卫使用建议

    • 合理使用全局守卫、路由独享守卫和组件内守卫
    • 避免在守卫中执行耗时操作
    • 守卫中异常处理要完善
  3. 动态路由管理

    • 权限变更时及时更新路由
    • 退出登录时清理动态路由
    • 路由更新时处理好当前页面状态

常见问题及解决方案

  1. 刷新页面后权限路由丢失

    解决方案:在页面加载时重新获取权限并生成路由

// src/main.js
// 页面加载时检查权限路由
if (getToken()) {
  store.dispatch('user/getInfo').then(() => {
    store.dispatch('permission/generateRoutes').then(accessRoutes => {
      router.addRoutes(accessRoutes)
    })
  })
}
  1. 动态路由添加后导航不生效

    解决方案:使用next({ ...to, replace: true })重新导航

  2. 复杂权限场景下的路由匹配问题

    解决方案:实现自定义路由匹配逻辑

// 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
}

总结与展望

核心要点总结

  1. pig-ui权限路由基于RBAC模型设计,实现了菜单、角色、权限的三级权限控制
  2. 通过路由守卫实现了路由访问前的权限检查
  3. 采用动态路由生成技术,根据用户权限动态构建路由表
  4. 实现了路由缓存、数据权限等高级功能
  5. 通过缓存、懒加载等技术优化了权限路由的性能

未来发展方向

  1. 基于组件的细粒度权限控制:将权限控制下沉到组件级别
  2. 权限路由的可视化配置:提供可视化界面配置权限与路由的映射关系
  3. 权限动态更新机制:实现无需刷新页面的权限实时更新
  4. 微前端架构下的权限路由:解决微前端场景下的跨应用权限控制问题

结语

权限路由是现代前端应用不可或缺的核心功能,它不仅关系到系统的安全性,也影响着用户体验和系统可维护性。pig-ui权限路由实现方案通过前后端协同,构建了一套完整的权限控制体系,为开发者提供了灵活、高效、安全的权限路由解决方案。随着前端技术的不断发展,权限路由也将朝着更智能、更精细化的方向演进。

希望本文能够帮助开发者深入理解权限路由的实现原理,在实际项目中构建出更安全、更灵活的权限控制体系。如有任何问题或建议,欢迎在评论区留言讨论。

点赞、收藏、关注,获取更多前端权限控制的实践经验和最佳实践!

【免费下载链接】pig 【免费下载链接】pig 项目地址: https://gitcode.com/gh_mirrors/pig/pig

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值