RuoYi-Vue3权限控制实战:基于Spring Security的细粒度权限设计
引言:权限控制的痛点与解决方案
在企业级应用开发中,权限控制是保障系统安全的核心环节。你是否还在为如何实现细粒度的权限控制而烦恼?是否遇到过权限管理与业务逻辑耦合紧密、难以维护的问题?本文将基于RuoYi-Vue3框架,结合Spring Security,为你提供一套完整的权限控制解决方案。读完本文,你将能够:
- 掌握前后端分离架构下的权限控制流程
- 实现基于角色(RBAC)和权限的双重控制
- 动态生成菜单和路由
- 实现按钮级别的权限控制
- 解决权限缓存与刷新的常见问题
一、权限控制整体架构
1.1 权限控制模型
RuoYi-Vue3采用了基于角色(RBAC)和权限的双重控制模型,其核心包括:
- 用户(User):系统操作者
- 角色(Role):一组权限的集合
- 权限(Permission):具体操作权限
- 菜单(Menu):系统功能模块
四者之间的关系如下:
1.2 权限控制流程
权限控制整体流程如下:
二、前端权限控制实现
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 权限设计原则
- 最小权限原则:只授予用户完成工作所必需的最小权限
- 职责分离原则:将不同职责的权限分配给不同的角色
- 数据权限与功能权限分离:功能权限控制菜单和按钮访问,数据权限控制数据范围
- 权限粒度适中:过粗无法满足细粒度控制,过细增加管理复杂度
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的权限控制实现,包括:
- 权限控制整体架构与流程
- 路由权限控制实现
- 菜单权限控制实现
- 按钮权限控制实现
- 请求权限控制实现
- 权限数据管理
- 权限控制最佳实践
通过这套权限控制方案,可以实现从路由到按钮的全链路权限控制,满足企业级应用的安全需求。
5.2 未来展望
- 权限可视化配置:开发权限配置可视化界面,降低权限管理难度
- 权限审计系统:记录权限变更日志,实现权限变更可追溯
- 动态权限刷新:实现权限变更后无需刷新页面即可生效
- 数据权限细粒度控制:基于数据行级别的权限控制,实现更精细的数据访问控制
5.3 扩展学习资源
通过本文的学习,相信你已经掌握了RuoYi-Vue3框架中权限控制的核心技术。在实际项目中,还需要根据具体需求进行灵活调整和扩展,才能构建出既安全又易用的权限控制系统。
如果本文对你有所帮助,请点赞、收藏、关注三连,你的支持是我持续创作的动力!下期将为大家带来《Spring Security与Vue3前后端分离认证实战》,敬请期待!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



