【ruoyi前后端分离版学习】002--权限框架
概述
上篇我们大概了解了框架的整体功能,也决定好了研究路线,今天就按计划开始,从权限开始入手,我们直接查看数据表一窥究竟
从数据表我们可以看出,ruoyi的权限管理使用的是rbac模型,即基于角色的权限访问控制。用户的权限一般包括,接口权限,菜单权限,按钮权限,数据权限等,这里我们把这些权限统称为资源。rbac模型需要我们至少需要用户表,角色表,资源表,用户角色表,角色资源表,
首先是用户表:
用户表大多数字段还OK,其中一个用户类型我们先记下,看有什么用。
角色表:
菜单表:
可以看出,ruoyi把框架的权限大概分成了目录,菜单和按钮三种,这是关系到前端动态路由的主角。
接下来我们开始我们的主线:任何系统都会从登陆开始,ruoyi应该也一样,我们猜想应该是:
登陆->账号验证->获取权限->前端动态渲染,
带着我们的疑问,我们直接翻开ruoyi的登录源码:
/**
* 登录验证
*
* @author ruoyi
*/
@RestController
public class SysLoginController
{
@Autowired
private SysLoginService loginService;
@Autowired
private ISysMenuService menuService;
@Autowired
private SysPermissionService permissionService;
/**
* 登录方法
*
* @param loginBody 登录信息
* @return 结果
*/
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
ajax.put(Constants.TOKEN, token);
return ajax;
}
...
从调试台的输入输出内容及登录源码可以看出,登录成功后只返回了token。
从调试窗口可以看到,用户登录后,接口只返回了token,接下来前端马上执行了获取用户信息和路由信息的请求,而这两个请求返回的数据正是动态渲染的数据,我们继续深入这两个请求:
/**
* 获取用户信息
*
* @return 用户信息
*/
@GetMapping("getInfo")
public AjaxResult getInfo()
{
SysUser user = SecurityUtils.getLoginUser().getUser();
// 角色集合
Set<String> roles = permissionService.getRolePermission(user);
// 权限集合
Set<String> permissions = permissionService.getMenuPermission(user);
AjaxResult ajax = AjaxResult.success();
ajax.put("user", user);
ajax.put("roles", roles);
ajax.put("permissions", permissions);
return ajax;
}
源码中我们看到,这个接口返回了用户信息,角色集合,权限集合,具体内容我们看控制台的返回信息:
我们可以清楚看到,权限返回一段符号,具体什么用我们还需要深入,角色返回的是角色名称。
接下来是路由信息接口:
/**
* 获取路由信息
*
* @return 路由信息
*/
@GetMapping("getRouters")
public AjaxResult getRouters()
{
Long userId = SecurityUtils.getUserId();
List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
return AjaxResult.success(menuService.buildMenus(menus));
}
其实本篇我们把前后端的权限动态渲染整明白了,就事就算结了。我们直接看前端vue代码
我们看到,返回的路由信息里没有首页,但是导航栏中确有首页。其实若依把路由分成了静态路由和动态路由量部分,
静态路由代表那些不需要动态判断权限的路由,如登录页、404、等通用页面,在@/router/index.js配置对应的公共路由
// 公共路由
export const constantRoutes = [
{
path: '/redirect',
component: Layout,
hidden: true,
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/redirect')
}
]
},
{
path: '/login',
component: () => import('@/views/login'),
hidden: true
},
{
path: '/register',
component: () => import('@/views/register'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/error/404'),
hidden: true
},
{
path: '/401',
component: () => import('@/views/error/401'),
hidden: true
},
{
path: '',
component: Layout,
redirect: 'index',
children: [
{
path: 'index',
component: () => import('@/views/index'),
name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true }
}
]
},
{
path: '/user',
component: Layout,
hidden: true,
redirect: 'noredirect',
children: [
{
path: 'profile',
component: () => import('@/views/system/user/profile/index'),
name: 'Profile',
meta: { title: '个人中心', icon: 'user' }
}
]
}
]
动态路由代表那些需要根据用户动态判断权限并通过addRoutes动态添加的页面,不通的用户不通的角色拥有不用的目录菜单,因此返回不通的路由信息。
我们可以从1个请求的最开始分析,我们第一次进入了系统,还没登录过,进入如下逻辑:scr/permission.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'
import { isRelogin } from '@/utils/request'
NProgress.configure({ showSpinner: false })
const whiteList = ['/login', '/auth-redirect', '/bind', '/register']
router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
...
} else {
// 没有token
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
NProgress.done()
}
}
})
我们先分析没登录的逻辑,如果没有本地没token说明没登录过,如果在白名单内则直接进入,如果不是则任何路由都跳转到登录页。
我们进入store/modules/user.js查看登录逻辑:
// 登录
Login({ commit }, userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
setToken(res.token)
commit('SET_TOKEN', res.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
登陆成功了再cookie中保存了ADMIN-TOKEN
登录完成后,路由进入"/"根路径,因此触发了全局前置守卫执行,这个时候本地已经有了token,因此走了有token的逻辑,如下:
router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
if (store.getters.roles.length === 0) {
isRelogin.show = true
// 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(() => {
isRelogin.show = false
store.dispatch('GenerateRoutes').then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
})
}).catch(err => {
store.dispatch('LogOut').then(() => {
Message.error(err)
next({ path: '/' })
})
})
} else {
next()
}
}
} else {
...
}
})
// 获取用户信息
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo().then(res => {
const user = res.user
const avatar = (user.avatar == "" || user.avatar == null) ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;
if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', res.roles)
commit('SET_PERMISSIONS', res.permissions)
} else {
commit('SET_ROLES', ['ROLE_DEFAULT'])
}
commit('SET_NAME', user.userName)
commit('SET_AVATAR', avatar)
resolve(res)
}).catch(error => {
reject(error)
})
})
},
返回的数据中比较重要的是权限标识字符串,这个代表着用户可以进行哪些操作,对前端而言,是进哪些按钮进行渲染。
接下来就是本篇最关键的部分,怎么生成动态路由。
从路由守卫里看出,去请求了获取路由信息接口:
// 遍历后台传来的路由字符串,转换为组件对象
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
})
}
//生成完整的路径
function filterChildren(childrenMap, lastRouter = false) {
var children = []
childrenMap.forEach((el, index) => {
if (el.children && el.children.length) {
if (el.component === 'ParentView' && !lastRouter) {
el.children.forEach(c => {
c.path = el.path + '/' + c.path
if (c.children && c.children.length) {
children = children.concat(filterChildren(c.children, c))
return
}
children.push(c)
})
return
}
}
if (lastRouter) {
el.path = lastRouter.path + '/' + el.path
}
children = children.concat(el)
})
return children
}
用户的路由有如下几种:
一类静态路由:这些路由不需要权限,如登录,注册,错误页等。
二类前端预置的动态路由:这类路由需要权限但没有菜单入口,但是可以跳转,如分配角色和分配账号,字典数据等页面
三类后台返回的路由:这类路由需要权限有菜单入口
现在我们来捋一遍思路:
首先router/index.js生成的路由只包含了一类静态路由,接着在GenerateRoutes方法中按照用户权限加入二类路由,最后在router.beforeEach方法中加入三类路由。
导航栏在SidebarItem中进行渲染
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET10 - 预览版1新功能体验(一)