1000字范文,内容丰富有趣,学习的好帮手!
1000字范文 > 若依的使用(RBAC 页面权限和按钮权限 深入源码理解权限控制)

若依的使用(RBAC 页面权限和按钮权限 深入源码理解权限控制)

时间:2021-06-20 16:46:01

相关推荐

若依的使用(RBAC 页面权限和按钮权限 深入源码理解权限控制)

本文章转载于公众号:王清江唷,仅用于学习和讨论,如有侵权请联系

QQ交流群:298405437

本人QQ:4206359 具体视频地址:8 跑后端_哔哩哔哩_bilibili

1、简介

RBAC是一种基于角色权限控制,进而控制用户的权限。

具体而言,对系统操作的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。每一种角色对应一组相应的权限。一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。这样做的好处是,不必在每次创建用户时都进行分配权限的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,这样将简化用户的权限管理,减少系统的开销。

本次只做简介,不做举例,ry的权限控制模型利用的就是RBAC模型,下面直接来看ry的权限控制。

2、什么是页面权限、按钮权限

所谓页面权限,就是进入某个页面所需要的权限。

按钮权限则是能点击某个按钮所需要的权限。

2.1权限表现形式

对于权限,在程序中往往表现为一个字符串,比如说“system:user:add”表示对用户具有增加权限,当然也可以说“dsadsaddsadsadsa”表示对用户有增加权限,字符串是随意定的,但是为了符合人类的想法,要做到尽量的见名知意。

对于ry而言,在系统管理→菜单管理对每一个菜单对应了某种权限,如下图所示:

下面我们以岗位管理为例子,说明岗位管理的每个字符串的作用,岗位管理的权限字符串如下:

上图的岗位管理对应的字符串为“system:post:list”,该字符串的意思是,有了这个字符串才能查看岗位管理的菜单。

“system:post:query”字符串表示对单个岗位详情的查询权限。

“system:post:add”表示增权限,“system:post:edit”表示修改权限,“system:post:remove”表示删岗位权限,“system:post:export”表示导出岗位数据Excel文件的权限。

岗位管理的“system:post:list”是页面权限,如果没有该权限,连菜单都看不到,自然页面也看不到。

其他的“system:post:add”、“system:post:edit”、“system:post:remove”、“system:post:export”都是按钮权限。

按钮权限在前端页面的表现就是能不能看到按钮,在后端的表现就是能不能执行按钮对应的controller的增删改查方法。

2.2 趁此机会演示userId=2的用户对于页面权限和菜单权限。但是此演示有一个漏洞...

ry如何进行权限控制

在ry中,页面菜单、目录菜单和按钮菜单基本上都是存在数据库中(首页、404等特殊页面除外),在系统管理→菜单管理可以填写每一个菜单对应的权限字符串,不过目前目录菜单不能添加权限字符串。

当我们在菜单管理写好权限字符串之后,在角色管理我们可以添加角色,并把相关的权限赋予给角色。

角色和权限是多对多关系,一个权限可以给多个角色,一个角色可以有多个权限。

当我们让给角色赋权完毕之后,我们可以在角色管理页面直接给角色分配用户,比如把角色1和角色2分给用户1,那么用户1就会拥有角色1和角色2的全部权限。角色1和角色2的权限可以重复,但是没有双倍权限这种快乐。角色和用户的关系也是多对多,一个用户可以有多个角色,一个角色也可以给多个用户。

以上就是ry控制权限的基本思想。

更多关于RBAC思想请看:

/weixin_42688085/article/details/125289494

/m0_5246/article/details/122224362

3、深入源码理解权限控制(一)

3.1何时获得权限

对于ry系统的ry用户(userId=2的用户),它是在登录的时候就在数据库得到了属于自己的权限。回顾登录过程,在执行loadUserByUsername方法的时候,权限就已经被拿到了:

/**

* 用户验证处理

*

* @author ruoyi

*/

@Service

public class UserDetailsServiceImpl implements UserDetailsService

{

private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

@Autowired

private ISysUserService userService;

@Autowired

private SysPermissionService permissionService;

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException

{

SysUser user = userService.selectUserByUserName(username);

if (StringUtils.isNull(user))

{

log.info("登录用户:{} 不存在.", username);

throw new ServiceException("登录用户:" + username + " 不存在");

}

else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))

{

log.info("登录用户:{} 已被删除.", username);

throw new ServiceException("对不起,您的账号:" + username + " 已被删除");

}

else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))

{

log.info("登录用户:{} 已被停用.", username);

throw new ServiceException("对不起,您的账号:" + username + " 已停用");

}

return createLoginUser(user);

}

public UserDetails createLoginUser(SysUser user)

{

return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));

}}

permissionService.getMenuPermission(user)则是根据当前登录的用户查询该用户的权限。转到方法进去看看:

/**

* 获取菜单数据权限

*

* @param user 用户信息

* @return 菜单权限信息

*/

public Set<String> getMenuPermission(SysUser user)

{

Set<String> perms = new HashSet<String>();

// 管理员拥有所有权限

if (user.isAdmin())

{

perms.add("*:*:*");

}

else

{

perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));

}

return perms; }

从上面的代码可以看到,如果是超级管理员就给权限字符串为“*:*:*”,那么什么样的人是超级管理员呢?我们直接看源码:

public boolean isAdmin(){return isAdmin(this.userId);}public static boolean isAdmin(Long userId){return userId != null && 1L == userId;}

原来userId=1的用户就是超级管理员,也就是说超级管理员是代码写死的,牛逼和气质是与生俱来的,在前端判断字符串也要先看看权限字符串是不是“*:*:*”,如果是就知道是大爷来了都让道,直接获得所有权限。

那么如果不是超级管理员呢?代码中是执行了perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId())),也就是获得当前用户的userId,拿着userId去数据库查询权限。继续进入Service层代码:

/*** 根据用户ID查询权限** @param userId 用户ID* @return 权限列表*/@Overridepublic Set<String> selectMenuPermsByUserId(Long userId){List<String> perms = menuMapper.selectMenuPermsByUserId(userId);Set<String> permsSet = new HashSet<>();for (String perm : perms){if (StringUtils.isNotEmpty(perm)){permsSet.addAll(Arrays.asList(perm.trim().split(",")));}}return permsSet;}

可以看出,此时去数据库查询权限,然后迭代查询的每个权限并且split分割了一下,通过“,”做的分割,所以我们写权限的时候可以这样写“system:user:add,system:user:edit”,可以在一个菜单的权限标识写好几个权限字符串,用逗号分割。

话说回来,去数据库怎么查询的权限呢?我们看到传入的只有userId,我们继续上源码:

<select id="selectMenuPermsByUserId" parameterType="Long" resultType="String">

select distinct m.perms

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 r on r.role_id = ur.role_id

where m.status = '0' and r.status = '0' and ur.user_id = #{userId}

</select>

可以看到,到目前为止就已经是到了执行SQL了。我手动在navicat查询一下userId=2用户的权限是什么,如下:

可以看到SQL实际上就是不断在左连接,所谓左连接通俗而言就是左边表展示全部数据,右边表若匹配成功就连接,没有匹配成功就右边表空着。

学习左连接:

/m0_46628605/article/details/119728797

从ry获取userId = 2用户权限的SQL来看,实际就是所有权限左连接了角色,然后左连接用户,从而得到该用户的所有权限。

值得注意的是,我们发现where后面有俩校验,校验status是不是=0,其实在ry系统中status=0表示正常的意思,status=1表示停用的意思。

至此,我们说完了认证的时候获得权限的代码,ry把获得的权限放到了LoginUser类的实例中的字段permissions中存储起来,并把LoginUser的实例对象存储到了Redis中。

也就是说,登录过的用户Redis中存储着该用户的权限【那么这里就涉及到一个问题:当我用超级管理员修改了角色的权限,对于已经登录的和该角色相关联的用户,是不是立刻生效呢?其实对于页面权限是立刻生效的,但是对于按钮权限并不是立刻生效的,而是要重新登录重新从数据库获取权限,然后再存入Redis】。

3.2用户菜单(页面权限)的获取

当用户登录完毕之后,左侧菜单栏展示出来了大量的菜单,对于不同权限的用户,展示的菜单不尽相同。

那么,左侧菜单是怎么来的呢?这就需要打开“Layout”组件,然后找到侧边栏组件,如下:

通过以上代码,发现每一个菜单为一个“SidebarItem”组件,我们也可以用开发者工具确定,如下:

发现Layout组件的菜单栏组件是Sidebar,里面有一个ElMenu组件(对应整个菜单整体),这里面的SidebarItem全部都是一级菜单,一级菜单里面有ElSubmenu组件,对应我们的二级菜单整体,里面的SidebarItem组件为具体的每一个二级菜单。

我们可以发现,ElMenu中的菜单项的迭代数据来自变量sidebarRouters,所以我们到这里就知道,sidebarRouters变量的数据一定是后端查询来的菜单数据。我们继续深入,它是vuex的getters:

...mapGetters(["sidebarRouters", "sidebar"]),

继续追代码:

sidebarRouters:state => state.permission.sidebarRouters,

追:

state: {

routes: [],

addRoutes: [],

defaultRoutes: [],

topbarRouters: [],

sidebarRouters: []

},

到这里就追到底了,那么sidebarRouters是在哪里赋值的呢?我们继续看:

从上面发现,sidebarRouters的数据是来自两部分,一部分来自getRouters方法向后端发请求,返回的res的data经过filterAsyncRouter(ry解释了该方法的作用:遍历后台传来的路由字符串,转换为组件对象)方法过滤后的路由;另一部分来自constantRoutes,constantRoutes是写死的,来自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' }

}

]

}

]

现在的问题就是getRouters方法从后端拿菜单,我们需要深入,追:

// 获取路由

export const getRouters = () => {

return request({

url: '/getRouters',

method: 'get'

})

}

现在可以进入后端的getRouters方法了:

/**

* 获取路由信息

*

* @return 路由信息

*/

@GetMapping("getRouters")

public AjaxResult getRouters()

{

Long userId = SecurityUtils.getUserId();

List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);

return AjaxResult.success(menuService.buildMenus(menus));

}

第一句话拿到了userId,然后通过userId获得了菜单树:

/**

* 根据用户ID查询菜单

*

* @param userId 用户名称

* @return 菜单列表

*/

@Override

public List<SysMenu> selectMenuTreeByUserId(Long userId)

{

List<SysMenu> menus = null;

if (SecurityUtils.isAdmin(userId))

{

menus = menuMapper.selectMenuTreeAll();

}

else

{

menus = menuMapper.selectMenuTreeByUserId(userId);

}

return getChildPerms(menus, 0);

}

从以上代码可以发现,如果是超级管理员直接获得全部菜单(ry在系统中对userId=1的用户采取了硬编码方案,手动制造了一个超级牛逼管理员),普通用户则需要根据权限获得菜单:

select distinct m.menu_id, m.parent_id, m.menu_name, m.path, ponent, m.`query`, 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

上面SQL再明显不过了,查询menu_type类型为M和C的菜单,M表示目录,C表示菜单,从ry的数据库建表语句也可以得到证实:

查询到目录和菜单之后,通过构造树结构,然后再把树结构返回给了前端,关于树结构的构造逻辑不再赘述,布置为作业自己去看。

目前还有一个问题,GenerateRoutes方法是什么时候执行的?如下:

可以发现,是在已经登录,并且不是去“/login”的情况下,会执行生成路由方法。

以上我只是说明了左侧菜单栏能展示那么多菜单,但是要实现跳转,路由器的路由表也必须到位,实际上路由表就在下面一句话就成功添加到了路由器:

另外,左侧的菜单能进行跳转,利用了Applink组件(Sidebar文件夹下的Link.vue组件),如下:

Applink被渲染为了a标签,to=xxxx被渲染为了href=xxxx。关于组件的具体代码,不做展开。

4、深入源码理解权限控制(二)

4.1页面权限补充

上一讲,我们只是说了页面权限的前端,前端为什么能展示出来那么多菜单,因为前端请求后端接口“getRouters”,后端根据前端人员的角色,查询出来了该用户的菜单权限,然后返回给前端,前端所以才显示那么多菜单。

一个新注册的用户,默认是没有任何角色的,那么登录就只有一个菜单,那就是首页,就像下面这样:

页面权限往往不仅仅和前端菜单展示有关,前端点击岗位管理的时候,调用了岗位管理的“system/post/list”接口,难道调用该接口不需要权限吗???

其实,调用该接口是需要权限的,并且权限为“system:post:list”,每一个接口的调用需要什么权限,我们都可以在上面的注解可以看到:

@PreAuthorize注解是SpringSecurity框架的注解,注解的value值所需要填写一个表达式,如果表达式计算的结果是true,那么就允许访问对应的接口,如果表达式计算的结果是false,则表示没有权限。

如果没有权限,那么会怎么样呢?我现在手动把岗位管理的接口权限改成“system:post:list1”,如下:

此时,我们就没有访问该接口的权限了,因为用户获得的权限是从菜单管理的权限标识查询来的,也就是说,菜单标识写的权限是“system:post:list”,而我们接口需要“system:post:list1”,所以自然是无法调用接口。此时访问岗位管理会发生什么呢?如下:

4.2 @PreAuthorize 启动奥义

当我们需要用到@PreAuthorize注解,先需要写另一个注解来让@PreAuthorize注解能生效,ry程序在SecurityConfig类已经写了,如下:

EnableGlobalMethodSecurity注解后面的prePostEnabled方法(Determines if Spring Security's pre post annotations should be enabled. Default is false.)表示@PreAuthorize和@PostAuthorize注解要不要启用。@PreAuthorize表示在执行方法前验证权限,@PostAuthorize表示在执行方法后验证权限。一般用@PreAuthorize就够了。

securedEnabled方法(Determines if Spring Security's Secured annotations should be enabled.)没啥用,ry项目虽然开启使用,但也没用到,不做展开,需要了解的同学自行到下面了解:

/chihaihai/article/details/104678864

4.3 @PreAuthorize 的value方法奥义

刚刚我们说到,@PreAuthorize的value值写SpEL表达式,并且返回值为true则可以执行方法,返回值为false则表示不能执行方法。

现在我们来分析SpEL表达式。SpEL表达式能写方法调用,ry程序就是写的方法表达式。岗位管理的表达式如下:

@PreAuthorize("@ss.hasPermi('system:post:list')")

SpEL表达式可以通过“@”来引用bean,这些知识大家可以通过查阅资料来学习:

所以@ss在引用名为ss的Bean,然后调用该Bean的hasPermi方法,并传入了参数。

@ss的bean为:​​​​​​​

package com.ruoyi.framework.web.service;

import java.util.Set;

import org.springframework.stereotype.Service;

import org.springframework.util.CollectionUtils;

import mon.core.domain.entity.SysRole;

import mon.core.domain.model.LoginUser;

import mon.utils.SecurityUtils;

import mon.utils.StringUtils;

/**

* RuoYi首创 自定义权限实现,ss取自SpringSecurity首字母

*

* @author ruoyi

*/

@Service("ss")

public class PermissionService

{

/** 所有权限标识 */

private static final String ALL_PERMISSION = "*:*:*";

/** 管理员角色权限标识 */

private static final String SUPER_ADMIN = "admin";

private static final String ROLE_DELIMETER = ",";

private static final String PERMISSION_DELIMETER = ",";

/**

* 验证用户是否具备某权限

*

* @param permission 权限字符串

* @return 用户是否具备某权限

*/

public boolean hasPermi(String permission)

{

if (StringUtils.isEmpty(permission))

{

return false;

}

LoginUser loginUser = SecurityUtils.getLoginUser();

if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))

{

return false;

}

return hasPermissions(loginUser.getPermissions(), permission);

}

/**

* 验证用户是否不具备某权限,与 hasPermi逻辑相反

*

* @param permission 权限字符串

* @return 用户是否不具备某权限

*/

public boolean lacksPermi(String permission)

{

return hasPermi(permission) != true;

}

/**

* 验证用户是否具有以下任意一个权限

*

* @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表

* @return 用户是否具有以下任意一个权限

*/

public boolean hasAnyPermi(String permissions)

{

if (StringUtils.isEmpty(permissions))

{

return false;

}

LoginUser loginUser = SecurityUtils.getLoginUser();

if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))

{

return false;

}

Set<String> authorities = loginUser.getPermissions();

for (String permission : permissions.split(PERMISSION_DELIMETER))

{

if (permission != null && hasPermissions(authorities, permission))

{

return true;

}

}

return false;

}

/**

* 判断用户是否拥有某个角色

*

* @param role 角色字符串

* @return 用户是否具备某角色

*/

public boolean hasRole(String role)

{

if (StringUtils.isEmpty(role))

{

return false;

}

LoginUser loginUser = SecurityUtils.getLoginUser();

if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))

{

return false;

}

for (SysRole sysRole : loginUser.getUser().getRoles())

{

String roleKey = sysRole.getRoleKey();

if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role)))

{

return true;

}

}

return false;

}

/**

* 验证用户是否不具备某角色,与 isRole逻辑相反。

*

* @param role 角色名称

* @return 用户是否不具备某角色

*/

public boolean lacksRole(String role)

{

return hasRole(role) != true;

}

/**

* 验证用户是否具有以下任意一个角色

*

* @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表

* @return 用户是否具有以下任意一个角色

*/

public boolean hasAnyRoles(String roles)

{

if (StringUtils.isEmpty(roles))

{

return false;

}

LoginUser loginUser = SecurityUtils.getLoginUser();

if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))

{

return false;

}

for (String role : roles.split(ROLE_DELIMETER))

{

if (hasRole(role))

{

return true;

}

}

return false;

}

/**

* 判断是否包含权限

*

* @param permissions 权限列表

* @param permission 权限字符串

* @return 用户是否具备某权限

*/

private boolean hasPermissions(Set<String> permissions, String permission)

{

return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));

}

综上所述,表达式“@ss.hasPermi('system:post:list')”的意思是调用下面的方法,并传参数为system:post:list,如下:​​​​​​​

/**

* 验证用户是否具备某权限

*

* @param permission 权限字符串

* @return 用户是否具备某权限

*/

public boolean hasPermi(String permission)

{

if (StringUtils.isEmpty(permission))

{

return false;

}

LoginUser loginUser = SecurityUtils.getLoginUser();

if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))

{

return false;

}

return hasPermissions(loginUser.getPermissions(), permission);

}

该方法的作用是验证请求API的用户是否具有接口权限,从源码可以看到,该方法先获得了安全上下文的认证授权相关的信息:​​​​​​​

/**

* 获取用户

**/

public static LoginUser getLoginUser()

{

try

{

return (LoginUser) getAuthentication().getPrincipal();

}

catch (Exception e)

{

throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED);

}

}

/**

* 获取Authentication

*/

public static Authentication getAuthentication()

{

return SecurityContextHolder.getContext().getAuthentication();

}

验证是否有权限的源码:​​​​​​​

/**

* 判断是否包含权限

*

* @param permissions 权限列表

* @param permission 权限字符串

* @return 用户是否具备某权限

*/

private boolean hasPermissions(Set<String> permissions, String permission)

{

return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));

}

值得注意的是,这里验证时给超级管理员开了后门(如果权限是*:*:*则会直接返回true):

以上只要返回true,则表达式“@ss.hasPermi('system:post:list')”返回true,则用户可以执行对应的方法,所以用户自然也正常打开了页面。

5、深入源码理解权限控制(三)

5.1按钮权限在前端的表现形式

按钮权限在前端的表现为vue组件、HTML标签是否显示。在ry程序中,最常用的方式就是控制按钮显示还是不显示,如下:

也就是说,当用户具有“system:post:add”权限时,新增的el-button组件才会显示,否则直接不显示按钮。

5.2 v-hasPermi指令奥义

刚刚我们发现,v-hasPermi指令竟然可以控制按钮是否显示,那么是怎么做到的呢?

v-hasPermi是一个新指令,vue本身自身没有的,那么只能是ry自己写的一个新指令。ry所有自定义指令在src/directive下,在main.js引入了src/directive/index.js:​​​​​​​

...省略其他源码import directive from './directive' // directiveVue.use(directive)...省略其他源码

从上面可以发现,自定义指令是以插件的形式包装的,Vue.use方法会自动执行插件的install方法:​​​​​​​

const install = function(Vue) {Vue.directive('hasRole', hasRole)Vue.directive('hasPermi', hasPermi)Vue.directive('clipboard', clipboard)Vue.directive('dialogDrag', dialogDrag)Vue.directive('dialogDragWidth', dialogDragWidth)Vue.directive('dialogDragHeight', dialogDragHeight)}

install方法中,我们看到v-hasPermi被注册。具体的源码如下:

/**

* v-hasPermi 操作权限处理

* Copyright (c) ruoyi

*/

export default {

inserted(el, binding, vnode) {

const { value } = binding

const all_permission = "*:*:*";

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)

})

if (!hasPermissions) {

el.parentNode && el.parentNode.removeChild(el)

}

} else {

throw new Error(`请设置操作权限标签值`)

}

}}

import store from '@/store'

可以看出,对象里面只有一个方法inserted,也就是在被绑定元素插入父节点时调用,这里正好可以判断有没有权限,有就插入,没有就直接移除dom即可。

inserted有三个形参,介绍如下:

为什么我用红色框框起来了el和value?因为v-hasPermi指令就只用到了el和value,其他参数不做介绍。

下面隆重介绍前端debug来细讲hasPermi的源码。

5.3前端debug,秘奥义·震雷削

首先需要修改vue.config.js:​​​​​​​

configureWebpack: {

...省略其他代码

devtool: process.env.NODE_ENV === 'development' ? 'source-map' : undefined,

...省略其他代码

},

以上的一句话意思是,在dev模式启动程序,则允许源码被浏览器看到。关于devtool的配置选项其他还有很多,大家可以自行学习:

/weixin_40599109/article/details/107845431

/sshuai131400/article/details/121223357

此时此刻,前端就可以看到源码,现在我们为我们hasPermi打一个断点:

此时再到前端,运行的时候记得按F12打开开发者模式,当运行到debugger时,程序会卡住。如下:

debug时需要看作用域下的各个变量的值来确认程序是不是按照自己的想法来进行走向。

经过debug,发现关键代码就是如果没权限,那么移除当前el,源码:​​​​​​​

const hasPermissions = permissions.some(permission => {

return all_permission === permission || permissionFlag.includes(permission)

})

if (!hasPermissions) {

el.parentNode && el.parentNode.removeChild(el)

}

上面涉及JavaScript的Array的some方法,需要详细了解的话:

/jsref/jsref-some.html

6、深入源码理解权限控制(四)

6.1 按钮权限后端的表现形式

以岗位管理为例子,当我们前端的导出按钮出现,那么点击导出按钮,会执行后端的某个方法,执行后端的方法也需要权限,道理和页面权限的后端简直是一模一样。

当我们点击岗位管理的导出按钮,会发送请求:

对应后端的如下方法:

@Log(title = "岗位管理", businessType = BusinessType.EXPORT)@PreAuthorize("@ss.hasPermi('system:post:export')")@PostMapping("/export")public void export(HttpServletResponse response, SysPost post){List<SysPost> list = postService.selectPostList(post);ExcelUtil<SysPost> util = new ExcelUtil<SysPost>(SysPost.class);util.exportExcel(response, list, "岗位数据");}

到这里,要执行方法,需要计算表达式“@ss.hasPermi('system:post:export')”的值,是true则可以访问接口,是false则不可以访问,和页面权限的一模一样,不再赘述。

6.2作业请debug,岗位管理每一个API,看看都是干嘛的,另外,岗位管理有什么事实上的作用。

提示:岗位管理没什么用,拿来看的,单纯的一个CRUD。

6.3按钮权限补充

上一节我们debug按钮权限的时候,漏了一个细节:

以上变量的值怎么来的?原来是在mutations里面的如下方法赋值的:​​​​​​​

SET_PERMISSIONS: (state, permissions) => {state.permissions = permissions}

那么SET_PERMISSIONS方法是谁调用的呢?如下:​​​​​​​

// 获取用户信息

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)

})

})

},

原来是在GetInfo的时候获取到的,并且拿到了res的permissions。那么GetInfo又是什么时候调用的呢?继续追代码:

到这里就追到底了,原来是全局前置路由守卫,只要带了token,不访问/login路由,并且vuex的user的roles的size为0,就会执行一次GetInfo方法。如果要细细理解,建议看懂每一句话:​​​​​​​

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()) {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 = falsestore.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 {// 没有tokenif (whiteList.indexOf(to.path) !== -1) {// 在免登录白名单,直接进入next()} else {next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页NProgress.done()}}})router.afterEach(() => {NProgress.done()})

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。