一、权限控制引发的思考
引言
最近接手了公司的一个项目,实施反馈说,客户那边要求对不同的权限的用户操作权限做限制。场景就是,比如一个项目列表,这部分数据有可能是针对某个公司某个部门的,对应不同的部门用户能看到的东西是被约束的,同时这个部门有些人员只能查看,而有些人员可以做一些修改,这部分权限控制应当如何实现呢?我当然是懵懵的,毕竟在我看来这样普适性很差,我需要做出很多个性的配置才行,甚至单单是一个前端的按钮显示我都要在每增加一个角色就做一些修改。所以我去了解了一下系统的权限体系,简单来说就是通过给不同用户不同角色,不同角色不同菜单权限来实现的。而这个用户的权限字符串,是在登录的时候进行传回的,至于这个权限字符串,是否安全存储、是否不可修改,这部分内容并不在今天文章的讨论中,这里就不多做介绍,这里我们就假设安全不可篡改。
实现思路
基本的实现思路,我们需要保证当前系统有着完善的菜单配置能力,加上一个权限字符的字段,如图,这里的系统展示图片展示均来自若依,感谢若依开源系统提供的思路。
有了这个字段,我们在配角色的时候,就可以通过查角色菜单,返回对应的权限字符了。而这个权限字符就是我们整个实现的重中之重。然后我们这个时候去配置角色
通过给角色不同的菜单,给不同用户不同角色,这样就可以实现权限控制了。
这边梳理一下整个流程,如图
二、具体实现
1、前端
首先我们先梳理一下前端,后端整体和前端方法相同。
我们要知道前端是如何做到对不同的按钮、菜单做权限判断的,我们就需要先了解一下,vue的一个特性——自定义指令。
我们先来看看什么是指令,在vue中比较常见的指令:v-if、v-model、v-show,如此我们对指令就有一个比较清晰的判断了。那么我们这边为啥要用自定义指令呢,而不用v-if来进行判断呢?为什么呢,主要就是我们希望可以全局使用,而不用在每个vue文件进行编写,那我们就需要全局注册,这个时候我们可以使用v-if和挂载js文件方法来实现,还可以通过自定义指令v-xxx来实现。
这边选择了自定义指令,为什么呢,因为这更加方便且高效,可以在组件全生命周期下进行控制,且整体实现更加的优雅,且也符合vue自定义指令的初衷。
那么如何实现自定义指令呢?
先看看使用方式吧
<el-button @click="handleAdd" v-hasPermi="['system:menu:add']">新增</el-button>
就是像使用v-if一样使用就行,传入的是一个数组类型的数据,很好理解,可查看权限是多样的,只单一配置显然是不够。
然后就开始写一下通用验证方法吧,代码如下
/**
* 操作权限处理
* Copyright (c) 2019 ruoyi
*/
import store from '@/store'
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(`请设置操作权限标签值`)
}
}
}
接下来我来解释一下这整个代码,首先这个用户权限数据的来源,可以自定,这边不多赘述,接下来我们来重点看一下这个inserted,这个是什么?这其实是组件的一个生命周期,在插入页面时做判断。
接下来那三个参数分别是:
-
el:指令所绑定的元素,可以用来直接操作 DOM,进而实现组件的隐藏。
-
binding:我们通过自定义指令传递的各种参数,大多存在于这个对象中。
-
vnode:Vue 编译生成的虚拟节点。
而我们这边其实只要使用binding的传的value,也就是我们传入的字符串数组,通过获取用户权限后与配置权限对比,通过就显示,不通过不显示。具体实现细节可以看代码,Arrays.some这个方法的逻辑就是遍历,只要有一个符合,就立刻中止返回true。如此我们的方法已经完成,接下来就是要变成指令。
import hasPermi from './permission/hasPermi'
const install = function(Vue) {
Vue.directive('hasPermi', hasPermi)
}
if (window.Vue) {
window['hasPermi'] = hasPermi
}
export default install
如此进行注册,而后在main.js中全局注册就可以正常使用了。
如此我们前端就算是完成了,整体思路就是配置一个自定义指令,通过判断用户权限串,是否有和配置权限相同的权限字符,有则显示。
2、后端
接下来就是后端,整体实现也是获取用户的权限字符串,然后对比,不过使用的方式不同(肯定不一样啊)
这边我们需要了解一个注解 @PreAuthorize,这个注解是SpringSecurity下的一个注解,这个注解的主要作用就是,在方法执行前进行权限验证,支持Spring EL表达式,它是基于方法注解的权限解决方案。
那么这个注解就是一个天然的切点,如此我们就可以通过使用这个注解,从而对接口进行鉴权,实现方法级别的权限控制。
/**
* 获取部门列表
*/
@PreAuthorize("@ss.hasPermi('system:dept:list')")
@GetMapping("/list")
public AjaxResult list(SysDept dept)
{
List<SysDept> depts = deptService.selectDeptList(dept);
return success(depts);
}
具体使用就如上述,至于这个@ss是一个具名的Service,通过.调用这个Service的具体方法,这个方法返回类型为Boolean类型。
我们来看看这个Service
package com.ruoyi.framework.web.service;
import java.util.Set;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.context.PermissionContextHolder;
/**
* RuoYi首创 自定义权限实现,ss取自SpringSecurity首字母
*
* @author ruoyi
*/
@Service("ss")
public class PermissionService
{
/**
* 验证用户是否具备某权限
*
* @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;
}
PermissionContextHolder.setContext(permission);
return hasPermissions(loginUser.getPermissions(), permission);
}
/**
* 验证用户是否不具备某权限,与 hasPermi逻辑相反
*
* @param permission 权限字符串
* @return 用户是否不具备某权限
*/
public boolean lacksPermi(String permission)
{
return hasPermi(permission) != true;
}
/**
* 验证用户是否具有以下任意一个权限
*
* @param permissions 以 PERMISSION_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;
}
PermissionContextHolder.setContext(permissions);
Set<String> authorities = loginUser.getPermissions();
for (String permission : permissions.split(Constants.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 (Constants.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(Constants.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(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
}
}
具体实现逻辑就如上述代码所示,具体验证细节,这肯定是可以自定的,我们可以通过修改代码验证细节就可以实现简单的权限验证。
不过这个注解的我们必须要在SecurityConfig类下开启才可以正常使用。
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
结语
如此我们便可实现这些基本的接口级别的鉴权。整个实现下来,我们发现这必须要求整个系统在构建的伊始就必须要完善的配置结构,否则这个方案是很难流畅的使用的,所以我们在整个项目构建时就应该确定权限配置的基本结构。
然后整个方案其实就是简单的字符串匹配,直接for循环都可以,没什么高大上的,但是整个思路还是非常新奇的,而且也可以开拓视野,毕竟自定义指令、@PreAuthorize注解,这些都可以进行一个实战,整体学习下来收获还是很多的。
如果文章内容有错误欢迎大家留言指正。
整个思路来源若依,再次感谢若依管理系统对我学习的帮助。