【RuoYi-Vue】RuoYi-Vue权限管理
1. 若依框架权限分类
若依Vue系统中的权限分为以下几类:
1 菜单权限:用户登录系统之后能看到哪些菜单 2 按钮权限:用户在一个页面上能看到哪些按钮,比如新增、删除等按钮 3 接口权限:用户带着认证信息请求后端接口,是否有权限访问,该接口和前端页面上的按钮一一对应 4 数据权限:用户有权限访问后端某个接口,但是不同的用户相同的接口相同的入参,根据权限大小不同,返回的结果应当不一样---权限大的能够看到的数据更多。
2. 若依框架权限的依次介绍
1 菜单权限
这个比较好理解,拥有不同权限的用户登录系统之后看到的菜单是不一样的。(新建菜单、用户分配菜单权限即可)
具体代码1(后端):SysLoginController-------->getRouters()
select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time from sys_menu m left join sys_role_menu rm on m.menu_id = rm.menu_id left join sys_user_role ur on rm.role_id = ur.role_id left join sys_role ro on ur.role_id = ro.role_id left join sys_user u on ur.user_id = u.user_id where u.user_id = #{userId} and m.menu_type in ('M', 'C') and m.status = 0 AND ro.status = 0 order by m.parent_id, m.order_num
这是典型的用户-角色-菜单模型。 菜单类型(M目录 C菜单 F按钮); 菜单状态(0显示 1隐藏) 前端会根据该接口返回的数据渲染出不同的菜单。
数据的结构如下图所示:(前端动态路由)
具体代码2(前端):ruoyi-ui\src\permission.js
permission.js文件中设置了导航守卫,每次路由发生变化的时候就会触发router.beforeEach的回调函数。
// 路由白名单 const whiteList = ['/login', '/auth-redirect', '/bind', '/register'] // 路由守卫 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) { // 判断当前用户是否已拉取完user_info信息 store.dispatch('GetInfo').then(() => { 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 { // 没有token if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入 next() } else { next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页 NProgress.done() } } }) router.afterEach(() => { NProgress.done() })
注意if (store.getters.roles.length === 0) {这段逻辑,可以看出,如果不刷新当前页面,就算给用户添加了新的菜单权限,用户也看不到新的菜单。
2.按钮权限
新增、删除、查看、修改等按钮,当前用户是否能用
具体代码1(后端):SysLoginController-------->getInfo() (前端拿到会存到vuex中)
// 将来这些数据,前端拿到会存到vuex中 @GetMapping("getInfo") public AjaxResult getInfo() { LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); SysUser user = loginUser.getUser(); // 角色集合 Set<String> roles = permissionService.getRolePermission(user); // 权限集合 Set<String> permissions = permissionService.getMenuPermission(user); AjaxResult ajax = AjaxResult.success(); // 根据当前用户,获取当前用户的所有user、roles、permissions信息,并返回 ajax.put("user", user); ajax.put("roles", roles); ajax.put("permissions", permissions); return ajax; }
具体代码2(前端):ruoyi-ui\src\directive\permission
具体用法:
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:menu:edit']" 这里就是按钮权限 >修改 </el-button> <el-button size="mini" type="text" icon="el-icon-plus" @click="handleAdd(scope.row)" v-hasPermi="['system:menu:add']" >新增 </el-button>
按钮权限 v-hasPermi 的具体实现:
export default { inserted(el, binding, vnode) { const { value } = binding const all_permission = "*:*:*"; // 从vuex中获取数据 const permissions = store.getters && store.getters.permissions if (value && value instanceof Array && value.length > 0) { const permissionFlag = value const hasPermissions = permissions.some(permission => { return all_permission === permission || permissionFlag.includes(permission) }) // 如果没有 则移除removeChild if (!hasPermissions) { el.parentNode && el.parentNode.removeChild(el) } } else { throw new Error(`请设置操作权限标签值`) } } }
3 接口权限:
接口权限和前端的按钮权限一一对应。为的是防止用户绕过按钮直接请求后端接口获取数据。在若依Vue系统中,是使用SpringSecurity的注解@PreAuthorize实现的。
具体代码实现(这里只有后端)
@PreAuthorize("@ss.hasPermi('system:menu:edit')") @Log(title = "菜单管理", businessType = BusinessType.UPDATE) @PutMapping public AjaxResult edit(@Validated @RequestBody SysMenu menu) { if (UserConstants.NOT_UNIQUE.equals(menuService.checkMenuNameUnique(menu))) { return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); } else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) { return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); } else if (menu.getMenuId().equals(menu.getParentId())) { return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己"); } menu.setUpdateBy(getUsername()); return toAjax(menuService.updateMenu(menu)); }
通过 @PreAuthorize("@ss.hasPermi(‘system:menu:edit’)")注解,实现了接口权限
接下来我们看 @PreAuthorize("@ss.hasPermi(‘system:menu:edit’)")
/** * 验证用户是否具备某权限 * * @param permission 权限字符串 * @return 用户是否具备某权限 */ public boolean hasPermi(String permission) { if (StringUtils.isEmpty(permission)) { return false; } LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) { return false; } return hasPermissions(loginUser.getPermissions(), permission); }
4 数据权限:
数据权限实现的关键在于com.ruoyi.framework.aspectj.DataScopeAspect类。该类是一个切面类,凡是加上com.ruoyi.common.annotation.DataScope注解的方法,在执行的时候都会被它拦截。
该切面定义了五种权限范围
该切面的核心逻辑是“拼SQL”,方法执行之前,会给参数的一个params属性添加一个dataScope键值对,key为"dataScope",值为AND (" + sqlString.substring(4) + ")"样式的一段SQL,这段SQL会根据当前用户所在的部门以及当前用户角色的权限范围发生变化。
简单来说,这段代码的逻辑就是用户所在的部门权限越高,数据权限范围越大,查出来的结果集将会越大。
DataScope注解分别加到了部门列表查询、角色列表查询、用户列表查询的接口上,很明显,这几个接口需要根据不同的人查出不同的结果。
以用户列表查询为例,执行sql为
<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult"> select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u left join sys_dept d on u.dept_id = d.dept_id where u.del_flag = '0' <if test="userName != null and userName != ''"> AND u.user_name like concat('%', #{userName}, '%') </if> <if test="status != null and status != ''"> AND u.status = #{status} </if> <if test="phonenumber != null and phonenumber != ''"> AND u.phonenumber like concat('%', #{phonenumber}, '%') </if> <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 --> AND date_format(u.create_time,'%y%m%d') >= date_format(#{params.beginTime},'%y%m%d') </if> <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 --> AND date_format(u.create_time,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d') </if> <if test="deptId != null and deptId != 0"> AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE find_in_set(#{deptId}, ancestors) )) </if> <!-- 数据范围过滤 --> ${params.dataScope} </select>
实际上DataScopeAspect切面就只干了填充params的dataScope属性这么一件事情。(把之前拼好的sql扔这里)
3. 若依框架重要接口执行流程
login
getInfo
getRouter
是不是发现上述两步不太严谨,对,我们可以自定义规则搞事情