RuoYi-Vue3权限控制实战:基于Spring Security的细粒度权限设计

RuoYi-Vue3权限控制实战:基于Spring Security的细粒度权限设计

【免费下载链接】RuoYi-Vue3 :tada: (RuoYi)官方仓库 基于SpringBoot,Spring Security,JWT,Vue3 & Vite、Element Plus 的前后端分离权限管理系统 【免费下载链接】RuoYi-Vue3 项目地址: https://gitcode.com/GitHub_Trending/ruo/RuoYi-Vue3

引言:权限控制的痛点与解决方案

在企业级应用开发中,权限控制是保障系统安全的核心环节。你是否还在为如何实现细粒度的权限控制而烦恼?是否遇到过权限管理与业务逻辑耦合紧密、难以维护的问题?本文将基于RuoYi-Vue3框架,结合Spring Security,为你提供一套完整的权限控制解决方案。读完本文,你将能够:

  • 掌握前后端分离架构下的权限控制流程
  • 实现基于角色(RBAC)和权限的双重控制
  • 动态生成菜单和路由
  • 实现按钮级别的权限控制
  • 解决权限缓存与刷新的常见问题

一、权限控制整体架构

1.1 权限控制模型

RuoYi-Vue3采用了基于角色(RBAC)和权限的双重控制模型,其核心包括:

  • 用户(User):系统操作者
  • 角色(Role):一组权限的集合
  • 权限(Permission):具体操作权限
  • 菜单(Menu):系统功能模块

四者之间的关系如下:

mermaid

1.2 权限控制流程

权限控制整体流程如下:

mermaid

二、前端权限控制实现

2.1 路由权限控制

路由权限控制是权限控制的第一道防线,主要通过路由守卫实现。

2.1.1 路由守卫实现

src/permission.js中,通过 beforeEach 钩子实现路由拦截:

router.beforeEach((to, from, next) => {
  NProgress.start()
  if (getToken()) {
    to.meta.title && useSettingsStore().setTitle(to.meta.title)
    /* has token*/
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done()
    } else if (isWhiteList(to.path)) {
      next()
    } else {
      if (useUserStore().roles.length === 0) {
        isRelogin.show = true
        // 判断当前用户是否已拉取完user_info信息
        useUserStore().getInfo().then(() => {
          isRelogin.show = false
          usePermissionStore().generateRoutes().then(accessRoutes => {
            // 根据roles权限生成可访问的路由表
            accessRoutes.forEach(route => {
              if (!isHttp(route.path)) {
                router.addRoute(route) // 动态添加可访问路由表
              }
            })
            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
          })
        }).catch(err => {
          useUserStore().logOut().then(() => {
            ElMessage.error(err)
            next({ path: '/' })
          })
        })
      } else {
        next()
      }
    }
  } else {
    // 没有token
    if (isWhiteList(to.path)) {
      // 在免登录白名单,直接进入
      next()
    } else {
      next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
      NProgress.done()
    }
  }
})
2.1.2 动态路由生成

src/store/modules/permission.js中,实现动态路由生成逻辑:

generateRoutes(roles) {
  return new Promise(resolve => {
    // 向后端请求路由数据
    getRouters().then(res => {
      const sdata = JSON.parse(JSON.stringify(res.data))
      const rdata = JSON.parse(JSON.stringify(res.data))
      const defaultData = JSON.parse(JSON.stringify(res.data))
      const sidebarRoutes = filterAsyncRouter(sdata)
      const rewriteRoutes = filterAsyncRouter(rdata, false, true)
      const defaultRoutes = filterAsyncRouter(defaultData)
      const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
      asyncRoutes.forEach(route => { router.addRoute(route) })
      this.setRoutes(rewriteRoutes)
      this.setSidebarRouters(constantRoutes.concat(sidebarRoutes))
      this.setDefaultRoutes(sidebarRoutes)
      this.setTopbarRoutes(defaultRoutes)
      resolve(rewriteRoutes)
    })
  })
}

路由过滤函数filterAsyncRouter将后端返回的路由数据转换为前端可识别的路由格式:

function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
  return asyncRouterMap.filter(route => {
    if (type && route.children) {
      route.children = filterChildren(route.children)
    }
    if (route.component) {
      // Layout ParentView 组件特殊处理
      if (route.component === 'Layout') {
        route.component = Layout
      } else if (route.component === 'ParentView') {
        route.component = ParentView
      } else if (route.component === 'InnerLink') {
        route.component = InnerLink
      } else {
        route.component = loadView(route.component)
      }
    }
    if (route.children != null && route.children && route.children.length) {
      route.children = filterAsyncRouter(route.children, route, type)
    } else {
      delete route['children']
      delete route['redirect']
    }
    return true
  })
}

2.2 菜单权限控制

菜单权限控制通过后端返回的菜单数据实现,只有拥有权限的菜单才会显示在侧边栏。

2.2.1 菜单数据请求

src/api/system/menu.js中,定义了菜单相关的API:

// 查询菜单下拉树结构
export function treeselect() {
  return request({
    url: '/system/menu/treeselect',
    method: 'get'
  })
}

// 根据角色ID查询菜单下拉树结构
export function roleMenuTreeselect(roleId) {
  return request({
    url: '/system/menu/roleMenuTreeselect/' + roleId,
    method: 'get'
  })
}
2.2.2 菜单渲染

菜单渲染逻辑在src/layout/components/Sidebar/index.vue中,根据动态生成的路由数据渲染菜单。

2.3 按钮权限控制

按钮权限控制通过自定义指令实现,可以精确控制页面元素的显示与隐藏。

2.3.1 权限指令实现

src/directive/permission/hasPermi.js中,实现了v-hasPermi指令:

export default {
  mounted(el, binding, vnode) {
    const { value } = binding
    const all_permission = "*:*:*"
    const permissions = useUserStore().permissions

    if (value && value instanceof Array && value.length > 0) {
      const permissionFlag = value

      const hasPermissions = permissions.some(permission => {
        return all_permission === permission || permissionFlag.includes(permission)
      })

      if (!hasPermissions) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    } else {
      throw new Error(`请设置角色权限标签值`)
    }
  }
}

同样,在src/directive/permission/hasRole.js中实现了角色权限指令v-hasRole

export default {
  mounted(el, binding, vnode) {
    const { value } = binding
    const super_admin = "admin"
    const roles = useUserStore().roles

    if (value && value instanceof Array && value.length > 0) {
      const roleFlag = value

      const hasRole = roles.some(role => {
        return super_admin === role || roleFlag.includes(role)
      })

      if (!hasRole) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    } else {
      throw new Error(`请设置角色权限标签值`)
    }
  }
}
2.3.2 权限指令使用

在组件中使用权限指令控制按钮显示:

<el-button 
  v-hasPermi="['system:user:add']" 
  type="primary" 
  @click="handleAdd"
>
  <svg-icon icon-class="add"/> 新增
</el-button>

<el-button 
  v-hasRole="['admin']" 
  type="danger" 
  @click="handleDelete"
>
  <svg-icon icon-class="delete"/> 删除
</el-button>

2.4 请求权限控制

请求权限控制通过Axios拦截器实现,处理权限相关的错误。

src/utils/request.js中,响应拦截器处理401错误:

service.interceptors.response.use(res => {
  // 未设置状态码则默认成功状态
  const code = res.data.code || 200
  // 获取错误信息
  const msg = errorCode[code] || res.data.msg || errorCode['default']
  
  if (code === 401) {
    if (!isRelogin.show) {
      isRelogin.show = true
      ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { 
        confirmButtonText: '重新登录', 
        cancelButtonText: '取消', 
        type: 'warning' 
      }).then(() => {
        isRelogin.show = false
        useUserStore().logOut().then(() => {
          location.href = '/index'
        })
      }).catch(() => {
        isRelogin.show = false
      })
    }
    return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
  } else if (code === 500) {
    ElMessage({ message: msg, type: 'error' })
    return Promise.reject(new Error(msg))
  } else if (code !== 200) {
    ElNotification.error({ title: msg })
    return Promise.reject('error')
  } else {
    return  Promise.resolve(res.data)
  }
}, error => {
  // 错误处理逻辑
})

三、权限数据管理

3.1 用户信息存储

用户信息、角色和权限存储在Vuex中,在src/store/modules/user.js中:

const useUserStore = defineStore(
  'user',
  {
    state: () => ({
      token: getToken(),
      id: '',
      name: '',
      nickName: '',
      avatar: '',
      roles: [],
      permissions: []
    }),
    actions:{
      // 获取用户信息
      getInfo() {
        return new Promise((resolve, reject) => {
          getInfo().then(res => {
            const user = res.user
            let avatar = user.avatar || ""
            if (!isHttp(avatar)) {
              avatar = (isEmpty(avatar)) ? defAva : import.meta.env.VITE_APP_BASE_API + avatar
            }
            if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
              this.roles = res.roles
              this.permissions = res.permissions
            } else {
              this.roles = ['ROLE_DEFAULT']
            }
            this.id = user.userId
            this.name = user.userName
            this.nickName = user.nickName
            this.avatar = avatar
            resolve(res)
          }).catch(error => {
            reject(error)
          })
        })
      }
    }
  })

3.2 Token管理

Token存储在Cookie中,在src/utils/auth.js中实现:

import Cookies from 'js-cookie'

const TokenKey = 'Admin-Token'

export function getToken() {
  return Cookies.get(TokenKey)
}

export function setToken(token) {
  return Cookies.set(TokenKey, token)
}

export function removeToken() {
  return Cookies.remove(TokenKey)
}

四、权限控制最佳实践

4.1 权限设计原则

  1. 最小权限原则:只授予用户完成工作所必需的最小权限
  2. 职责分离原则:将不同职责的权限分配给不同的角色
  3. 数据权限与功能权限分离:功能权限控制菜单和按钮访问,数据权限控制数据范围
  4. 权限粒度适中:过粗无法满足细粒度控制,过细增加管理复杂度

4.2 常见问题解决方案

4.2.1 权限缓存问题

问题:用户权限变更后,需要刷新页面才能生效。

解决方案:权限变更后,主动清除权限缓存并重新加载:

// 权限变更后调用此方法刷新权限
refreshPermission() {
  return new Promise(resolve => {
    useUserStore().getInfo().then(() => {
      usePermissionStore().generateRoutes().then(routes => {
        // 清除现有路由
        router.removeRoute('*')
        // 添加新路由
        routes.forEach(route => {
          router.addRoute(route)
        })
        resolve()
      })
    })
  })
}
4.2.2 标签页权限问题

问题:用户通过标签页访问无权限的路由。

解决方案:在标签页组件中添加权限检查:

// 检查标签页权限
checkTabPermission(tabItem) {
  const permissions = useUserStore().permissions
  const route = router.getRoutes().find(r => r.path === tabItem.path)
  if (!route) return false
  // 检查路由是否需要权限
  if (!route.meta.permissions || route.meta.permissions.length === 0) return true
  // 检查是否有权限
  return route.meta.permissions.some(perm => permissions.includes(perm))
}

4.3 权限控制实现对比

控制方式实现方式优点缺点适用场景
路由控制路由守卫+动态路由控制彻底,安全性高实现复杂,需要后端配合菜单级别的权限控制
指令控制自定义指令使用方便,细粒度控制需在每个元素上添加指令按钮、操作级别的权限控制
组件控制条件渲染(v-if)灵活,可自定义逻辑代码冗余,权限判断分散复杂的权限逻辑
请求控制请求拦截器防止非法请求无法阻止前端UI展示接口级别的权限控制

五、总结与展望

5.1 本文总结

本文详细介绍了RuoYi-Vue3框架中基于Spring Security的权限控制实现,包括:

  1. 权限控制整体架构与流程
  2. 路由权限控制实现
  3. 菜单权限控制实现
  4. 按钮权限控制实现
  5. 请求权限控制实现
  6. 权限数据管理
  7. 权限控制最佳实践

通过这套权限控制方案,可以实现从路由到按钮的全链路权限控制,满足企业级应用的安全需求。

5.2 未来展望

  1. 权限可视化配置:开发权限配置可视化界面,降低权限管理难度
  2. 权限审计系统:记录权限变更日志,实现权限变更可追溯
  3. 动态权限刷新:实现权限变更后无需刷新页面即可生效
  4. 数据权限细粒度控制:基于数据行级别的权限控制,实现更精细的数据访问控制

5.3 扩展学习资源

  1. Spring Security官方文档
  2. Vue Router官方文档
  3. RuoYi-Vue3官方文档
  4. RBAC权限模型详解

通过本文的学习,相信你已经掌握了RuoYi-Vue3框架中权限控制的核心技术。在实际项目中,还需要根据具体需求进行灵活调整和扩展,才能构建出既安全又易用的权限控制系统。

如果本文对你有所帮助,请点赞、收藏、关注三连,你的支持是我持续创作的动力!下期将为大家带来《Spring Security与Vue3前后端分离认证实战》,敬请期待!

【免费下载链接】RuoYi-Vue3 :tada: (RuoYi)官方仓库 基于SpringBoot,Spring Security,JWT,Vue3 & Vite、Element Plus 的前后端分离权限管理系统 【免费下载链接】RuoYi-Vue3 项目地址: https://gitcode.com/GitHub_Trending/ruo/RuoYi-Vue3

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

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

抵扣说明:

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

余额充值