3.4、编写用户菜单权限查询服务
在上面,我们介绍到了用户通过角色来关联菜单,因此,很容易想到,流程如下:
- 第一步:先通过用户查询到对应的角色;
- 第二步:然后再通过角色查询到对应的菜单;
- 第三步:最后将菜单查询出来之后进行渲染;
实现过程相比菜单查询服务多了前2个步骤,过程如下:
@Override public List<MenuVo> queryMenus(Long userId) { //1、先查询当前用户对应的角色 Wrapper queryUserRoleObj = new QueryWrapper<>().eq("user_id", userId); List<UserRole> userRoles = userRoleService.list(queryUserRoleObj); if(!CollectionUtils.isEmpty(userRoles)){ //2、通过角色查询菜单(默认取第一个角色) Wrapper queryRoleMenuObj = new QueryWrapper<>().eq("role_id", userRoles.get(0).getRoleId()); List<RoleMenu> roleMenus = roleMenuService.list(queryRoleMenuObj); if(!CollectionUtils.isEmpty(roleMenus)){ Set<Long> menuIds = new HashSet<>(); for (RoleMenu roleMenu : roleMenus) { menuIds.add(roleMenu.getMenuId()); } //查询对应的菜单 Wrapper queryMenuObj = new QueryWrapper<>().in("id", new ArrayList<>(menuIds)); List<Menu> menus = super.list(queryMenuObj); if(!CollectionUtils.isEmpty(menus)){ //将菜单下对应的父节点也一并全部查询出来 Set<Long> allMenuIds = new HashSet<>(); for (Menu menu : menus) { allMenuIds.add(menu.getId()); if(StringUtils.isNotEmpty(menu.getPath())){ String[] pathIds = StringUtils.split(",", menu.getPath()); for (String pathId : pathIds) { allMenuIds.add(Long.valueOf(pathId)); } } } //3、查询对应的所有菜单,并进行封装展示 List<Menu> allMenus = super.list(new QueryWrapper<Menu>().in("id", new ArrayList<>(allMenuIds))); List<MenuVo> resultList = transferMenuVo(allMenus, 0L); return resultList; } } } return null; }
- 编写一个用户菜单查询接口,如下:
@PostMapping(value = "/queryMenus") public List<MenuVo> queryMenus(Long userId){ //查询当前用户下的菜单权限 return menuService.queryMenus(userId); }
有的同学,可能觉得没必要存放path
这个字段,的确在某些场景下不需要。
为什么要存放这个字段呢?
小编在跟前端进行对接的时候,发现这么一个问题,有些前端的树型组件,在勾选子集的时候,不会将对应的父ID传给后端,例如,我在勾选【列表查询】的时候,前端无法将父节点【菜单管理】ID也传给后端,所有后端实际存放的是一个尾节点,需要一个字段path
,来存放节点对应的父节点路径。
其实,前端也可以传,只不过需要修改组件的属性,前端修改完成之后,树型组件就无法全选,不满足业务需求。
所以,有些时候得根据实际得情况来进行取舍。
3.5、编写后端权限控制
后端进行权限控制目标,主要是为了防止无权限的用户,进行接口请求查询。
其中菜单编码menuCode
就是一个前、后端联系的桥梁,细心的你会发现,所有后端的接口,与前端对应的都是按钮操作,所以我们可以以按钮为基准,实现前后端双向控制。
以【角色管理-查询】这个为例,前端可以通过菜单编码实现是否展示这个查询按钮,后端可以通过菜单编码来判断,当前用户是否具备请求接口的权限。
以后端为例,我们只需编写一个权限注解和代理拦截器即可!
- 编写一个权限注解
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface CheckPermissions { String value() default ""; }
- 编写一个代理拦截器,拦截有
@CheckPermissions
注解的方法
@Aspect @Component public class CheckPermissionsAspect { @Autowired private MenuMapper menuMapper; @Pointcut("@annotation(com.company.project.core.annotation.CheckPermissions)") public void checkPermissions() {} @Before("checkPermissions()") public void doBefore(JoinPoint joinPoint) throws Throwable { Long userId = null; Object[] args = joinPoint.getArgs(); Object parobj = args[0]; //用户请求参数实体类中的用户ID if(!Objects.isNull(parobj)){ Class userCla = parobj.getClass(); Field field = userCla.getDeclaredField("userId"); field.setAccessible(true); userId = (Long) field.get(parobj); } if(!Objects.isNull(userId)){ //获取方法上有CheckPermissions注解的参数 Class clazz = joinPoint.getTarget().getClass(); String methodName = joinPoint.getSignature().getName(); Class[] parameterTypes = ((MethodSignature)joinPoint.getSignature()).getMethod().getParameterTypes(); Method method = clazz.getMethod(methodName, parameterTypes); if(method.getAnnotation(CheckPermissions.class) != null){ CheckPermissions annotation = method.getAnnotation(CheckPermissions.class); String menuCode = annotation.value(); if (StringUtils.isNotBlank(menuCode)) { //通过用户ID、菜单编码查询是否有关联 int count = menuMapper.selectAuthByUserIdAndMenuCode(userId, menuCode); if(count == 0){ throw new CommonException("接口无访问权限"); } } } } } }
- 我们以【角色管理-查询】为例,先新建一个请求实体类
RoleDto
,添加用户ID属性
@Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) public class RoleDto extends Role { //添加用户ID private Long userId; }
- 在需要的接口上,添加
@CheckPermissions
注解,增加权限控制
@RestController @RequestMapping("/role") public class RoleController { private RoleService roleService; @CheckPermissions(value="roleMgr:list") @PostMapping(value = "/queryRole") public List<Role> queryRole(RoleDto roleDto){ return roleService.list(); } @CheckPermissions(value="roleMgr:add") @PostMapping(value = "/addRole") public void addRole(RoleDto roleDto){ roleService.add(roleDto); } @CheckPermissions(value="roleMgr:delete") @PostMapping(value = "/deleteRole") public void deleteRole(RoleDto roleDto){ roleService.delete(roleDto); } }
依次类推,当我们想对某个接口进行权限控制的时候,只需要添加一个注解@CheckPermissions
,并填写对应的菜单编码即可!
四、用户权限测试
我们先初始化一个用户【张三】,然后给他分配一个角色【访客人员】,同时给这个角色分配一下2个菜单权限【系统配置】、【用户管理】,等会用于权限测试。
初始内容如下:
数据初始化完成之后,我们来启动项目,传入用户【张三】的ID,查询用户具备的菜单权限,结果如下:
查询结果,用户【张三】有两个菜单权限!
接着,我们来验证一下,用户【张三】是否有角色查询权限,请求角色查询接口如下:
因为没有配置角色查询接口,所以无权访问!
五、总结
整片内容,只介绍了后端关键的服务实现过程,可能也有遗漏的地方,欢迎网友点评、吐槽!