@[toc]
因为写了不少 Spring Security 文章的缘故,所以总是有小伙伴来问松哥:按钮级别的权限怎么实现?甚至有一些看过 vhr 的小伙伴也问这种问题,其实有的时候搞得我的确挺郁闷的,最近刚好要做 TienChin 我的项目,我就再把这个问题拎进去和小伙伴们认真捋一捋。
1. 权限颗粒度
首先小伙伴们都晓得权限有不同的颗粒度,在 vhr 我的项目中,整体上我是基于申请地址去解决权限的,这个粒度算粗还是算细呢?
有的小伙伴们可能认为这个权限粒度太粗,所谓细粒度的权限应该是基于按钮的。
如果有小伙伴们做过前后端不分的开发,应该会有这样的领会:在 Shiro 或者 Spring Security 框架中,都提供了一些标签,通过这些标签能够做到在满足某种角色或者权限的状况下,显示某个按钮;当用户不具备某种角色或者权限的时候,按钮则会自动隐藏起来。
然而大家想想,按钮的显示与暗藏不过是前端页面为了进步用户体验而作出的款式的变动而已,实质上,当你点击一个按钮的时候,还是发送了一个 HTTP 申请,那么服务端解决该申请的接口,必须要进行权限管制。既然要在接口上进行权限管制,那么跟 vhr 的区别在哪里呢?
当初风行前后端拆散开发,所以 Shiro 或者 Spring Security 中的那些前端标签当初基本上都不必了,取而代之的做法是用户在登录胜利之后,向服务端发送申请,获取以后登录用户的权限以及角色信息,而后依据这些权限、角色等信息,在前端主动的去判断一个菜单或者按钮应该是显示还是暗藏,这么做的目标是为了进步用户体验,防止用户点击一个没有权限的按钮。前端的显示或者暗藏仅仅只是为了进步用户体验,真正的权限管制还是要后端来做。
后端能够在接口或者业务层对权限进行解决,具体在哪里做,就要看各自的我的项目了。
所以,vhr 中的权限,从设计上来说,粒度并不算粗,也是细粒度的,只不过跟菜单表放在了一起,小伙伴们可能感觉有点粗。然而,菜单表是能够持续细化的,咱们能够持续在菜单表中增加新的记录,新记录的 hidden 字段为 true,则菜单是暗藏的,就单纯只是细化权限而已。
如下图能够持续增加新的拜访规定,只不过把 enabled 字段设置为 false 即可(这样菜单就不会显示进去了,单纯就只是权限的配置)。
所以 vhr 的权限设计是 OK 的。
当你了解了 vhr 中的权限设计,再来看 TienChin 这个我的项目,或者说看 RuoYi-Vue 这个脚手架,就会发现十分 easy 了。
2. 权限表
首先咱们来看看资源表的定义,也就是 sys_menu
。
CREATE TABLE `sys_menu` (`menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单 ID',
`menu_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜单名称',
`parent_id` bigint(20) DEFAULT '0' COMMENT '父菜单 ID',
`order_num` int(4) DEFAULT '0' COMMENT '显示程序',
`path` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT ''COMMENT' 路由地址 ',
`component` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '组件门路',
`query` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '路由参数',
`is_frame` int(1) DEFAULT '1' COMMENT '是否为外链(0 是 1 否)',
`is_cache` int(1) DEFAULT '0' COMMENT '是否缓存(0 缓存 1 不缓存)',
`menu_type` char(1) COLLATE utf8mb4_unicode_ci DEFAULT ''COMMENT' 菜单类型(M 目录 C 菜单 F 按钮)',
`visible` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '菜单状态(0 显示 1 暗藏)',
`status` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '菜单状态(0 失常 1 停用)',
`perms` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '#' COMMENT '菜单图标',
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT ''COMMENT' 创建者 ',
`create_time` datetime DEFAULT NULL COMMENT '创立工夫',
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT ''COMMENT' 更新者 ',
`update_time` datetime DEFAULT NULL COMMENT '更新工夫',
`remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT ''COMMENT' 备注 ',
PRIMARY KEY (`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3054 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单权限表';
其实这里很多字段都和咱们 vhr 我的项目我的项目很类似,我也就不反复啰嗦了,我这里次要和小伙伴们说一个字段,那就是 menu_type
。
menu_type
示意一个菜单字段的类型,一个菜单有三种类型,别离是目录(M)、菜单(C)以及按钮(F)。这里所说的目录,相当于咱们在 vhr 中所说的一级菜单,菜单相当于咱们在 vhr 中所说的二级菜单。
当用户从前端登录胜利后,要去动静加载的菜单的时候,就查问 M 和 C 类型的数据即可,F 类型的数据不是菜单项,查问的时候间接过滤掉即可,通过 menu_type
这个字段能够轻松的过滤掉 F 类型的数据。小伙伴们想想,F 类型的数据过滤掉之后,剩下的数据不就是一级菜单和二级菜单了,那不就和 vhr 又一样了么!
最初再来说说 F 类型的,F 类型的就是按钮级别的权限了,前端每一个按钮的执行,须要哪些权限,当初就在这里定义好。
举一个简略的例子大家来看下:
当须要展现 用户治理 这个菜单的时候,须要 system:user:list
这个权限,当须要点击 用户批改 这个按钮的时候,则须要 system:user:edit
这个权限。
其余相干的表基本上和 vhr 都是一样的,用户有用户表 sys_user
,角色有角色表 sys_role
,用户和角色关联的表是 sys_user_role
,资源和角色关联的表是 sys_role_menu
。
当用户登录胜利后,后端会提供一个接口,将以后用户的角色和权限通通返回给前端:
- 查问角色思路:依据用户 id,先去
sys_user_role
表中查问到角色 id,再依据角色 id 去sys_role
表中查问到对应的角色(这里为了不便大家了解这么形容,实际上一个多表联结查问即可)。 - 查问权限思路:依据用户 id,先去
sys_user_role
表中查问到角色 id,再依据角色 id 去sys_role
表中查问到对应的角色,再拿着角色 id 去sys_role_menu
表中查问到对应的menu_id
,再依据menu_id
去sys_menu
表中查问到对应的 menu 中的权限(这里为了不便大家了解这么形容,实际上一个多表联结查问即可)。
前端有了用户的权限以及角色之后,就能够自行决定是否显示某一个菜单或者是否展现某一个按钮了。
3. 后端权限判断
我先来说说这块 TienChin 我的项目中是怎么做的(即 RuoYi 脚手架的实现计划),再来和 vhr 进行一个比照。
在 TienChin 我的项目中是通过注解来管制权限的,接口的拜访权限都是通过注解来标记的,例如上面这种:
@PreAuthorize("@ss.hasPermi('system:menu:add')")
@PostMapping
public AjaxResult add(@Validated @RequestBody SysMenu menu) {// 省略}
/**
* 批改菜单
*/
@PreAuthorize("@ss.hasPermi('system:menu:edit')")
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysMenu menu) {// 省略}
/**
* 删除菜单
*/
@PreAuthorize("@ss.hasPermi('system:menu:remove')")
@DeleteMapping("/{menuId}")
public AjaxResult remove(@PathVariable("menuId") Long menuId) {// 省略}
每一个接口须要什么权限,都是通过 @PreAuthorize
注解来实现的,对于这个注解的应用原理,松哥之前也有两篇文章:
- 想要管制好权限,这八个注解你必须晓得!
- Spring Security 中的权限注解很神奇吗?
看懂了这两篇文章,下面这个注解就懂了,我这里不赘述。
不过下面这种写法说到底还是有一点“硬编码”,因为拜访哪个接口须要哪些权限,在代码中固定了,如果接口和权限间接的关系可能保留到数据库中,那么用户就能够在本人须要的时候,随时进行灵便批改,岂不美哉!
在 vhr 我的项目中,松哥利用 Spring Security 中自定义 FilterInvocationSecurityMetadataSource 和 AccessDecisionManager 实现了服务端动态控制权限。这个具体的实现思路之前的文章中也和大家分享过了,传送门:Spring Security 动静权限实现计划!,这里就不赘述了。
相对来说,vhr 中的实现计划更灵便一些,因为能够配置接口和权限之间的关系。不过怎么说呢?其实像 RuoYi-Vue 这样硬编码其实也不是不能够,毕竟接口和权限之间的映射关系还是稍显“业余”一些,普通用户可能并不懂该如何配置,这个退出说零碎提供了这个性能,那么更多的还是面向程序员这一类业余人员的,那么程序员到底是否须要这个性能呢?我感觉还是得具体情况具体分析。
总之,小伙伴们能够联合本人我的项目的理论状况,来决定接口和权限之间的映射关系是否须要动静治理,如果须要动静治理,那么能够依照 vhr 中的计划来,如果不须要动静治理,那么就依照 RuoYi-Vue 脚手架中的形式来就行了。
那么用户的权限该如何设置?明天咱们就来聊聊这个话题。
4. 角色与权限
首先咱们先来看看角色与权限,该如何设计角色与权限,其实有很多十分成熟的实践,最为常见的莫过于 RBAC 了。
4.1 RBAC 简介
RBAC(Role-based access control)是一种以角色为根底的访问控制(Role-based access control,RBAC),它是一种较新且广为应用的权限管制机制,这种机制不是间接给用户赋予权限,而是将权限赋予角色。
RBAC 权限模型将用户按角色进行归类,通过用户的角色来确定用户对某项资源是否具备操作权限。RBAC 简化了用户与权限的治理,它将用户与角色关联、角色与权限关联、权限与资源关联,这种模式使得用户的受权治理变得非常简单和易于保护。
4.2 RBAC 的提出
权限、角色这些货色,在晚期 1970 年代的商业计算机程序中就能够找到相干的利用,然而晚期的程序绝对简略,而且并不存在一个明确的、通用的、公认的权限治理模型。
Ferraiolo 和 Kuhn 两位大佬于 1992 年提出了一种基于通用角色的访问控制模型(看来这个模型比松哥年龄还大),首次提出了 RBAC 权限模型用来代替传统的 MAC 和 DAC 两种权限管制计划,并且就 RBAC 中的相干概念给出了解释。
Ferraiolo,Cugini 和 Kuhn 于 1995 年扩大了 1992 年提出的权限模型。该模型的次要性能是所有拜访都是通过角色进行的,而角色实质上是权限的汇合,并且所有用户只能通过角色取得权限。在组织内,角色绝对稳固,而用户和权限都很多,并且可能会迅速变动。因而,通过角色管制权限能够简化访问控制的治理和查看。
到了 1996 年,Sandhu,Coyne,Feinstein 和 Youman 正式提出了 RBAC 模型,该模型以模块化形式细化了 RBAC,并提出了基于该实践的 RBAC0-RBAC3 四种不同模型。
明天,大多数信息技术供应商已将 RBAC 纳入其产品线,除了惯例的企业级利用,RBAC 也广泛应用在医疗、国防等畛域。
目前网上对于 RBAC 理论性的货色松哥只找到英文的,感兴趣的小伙伴能够看下,地址是:
- https://csrc.nist.gov/project…
如果小伙伴们有中文的材料链接,欢送留言阐明。
4.3 RBAC 三准则
- 最小权限:给角色配置的权限是其实现工作所须要的最小权限汇合。
- 责任拆散:通过互相独立互斥的角色来共同完成工作。
- 数据抽象:通过权限的形象来体现,RBAC 反对的数据抽象水平与 RBAC 的实现细节无关。
4.4 RBAC 模型分类
说到 RBAC,咱们就得从它的模型分类开始看起。
4.4.1 RBAC0
RBAC0 是最简略的用户、角色、权限模型。RBAC0 是 RBAC 权限模型中最外围的一部分,前面其余模型都是在此基础上建设。
在 RBAC0 中,一个用户能够具备多个角色,一个角色能够具备多个权限,最终用户所具备的权限是用户所具备的角色的权限并集。
4.4.2 RBAC1
RBAC1 则是在 RABC0 的根底上引入了角色继承,让角色有了上下级关系。
在本系列后面的文章中,松哥也曾多次向大家介绍过 Spring Security 中的角色继承。
4.4.3 RBAC2
RBAC2 也是在 RBAC0 的根底上进行扩大,引入了动态职责拆散和动静职责拆散。
要了解职责拆散,咱们得先明确角色互斥。
在理论我的项目中,有一些角色是互斥的,对抗的,例如财务这个角色个别是不能和其余角色专任的,否则本人报账本人审批,岂不是爽歪歪!
通过职责拆散能够解决这个问题:
动态职责拆散
在设置阶段就做好了限度。比方同一用户不能授予互斥的角色,用户只能有无限个角色,用户取得高级权限之前要有低级权限等等。
动静职责拆散
在运行阶段进行限度。比方运行时同一用户下 5 个角色中只能同时有 2 个角色激活等等。
4.4.4 RBAC3
将 RBAC1 和 RBAC2 联合起来,就造成了 RBAC3。
4.5 扩大
咱们日常见到的很多权限模型都是在 RBAC 的根底上扩大进去的。
例如在有的零碎中咱们能够见到用户组的概念,就是将用户分组,用户同时具备本身的角色以及分组的角色。
咱们 TienChin 我的项目所用的脚手架中的权限,就基本上是依照 RBAC 这套权限模型来的。
5. 表设计
咱们来看下 RuoYi-Vue 脚手架中跟用户、角色以及权限相干的表。
这里次要波及到如下几张表:
sys_user
:这个是用户表。sys_role
:这个是角色表。sys_user_role
:这个是用户角色关联表。sys_menu
:这个是菜单表,也能够了解为是资源表。sys_role_menu
:这个是资源角色关联表。
通过用户的 id,能够去 sys_user_role
表中查问到这个用户具备的角色 id,再依据角色 id,去 sys_role_menu
表中查问到这个角色能够操作的资源 id,再依据资源 id,去 sys_menu
表中查问到对应的资源,基本上就是这个样一个流程。
那么 Java 代码中该怎么做呢?
6. 代码实现
首先定义了一个 Java 类 SysUser,这个跟数据库中的 sys_user
表是对应的,咱们来看 UserDetailsService
的具体实现:
@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));
}
}
从数据库中查问到的就是 SysUser 对象,而后对该对象稍作革新,将之革新成为一个 LoginUser 对象,这个 LoginUser 则是 UserDetails 接口的实现类,里边保留了以后登录用户的要害信息。
在创立 LoginUser 对象的时候,有一个 permissionService.getMenuPermission 办法用来查问用户的权限,依据以后用户的 id,查问到用户的角色,再依据用户角色,查问到用户的权限,另外,如果以后用户的角色是 admin,那么就设置用户角色为 *:*:*
,这是一段硬编码。
咱们再来看看 LoginUser 的设计:
public class LoginUser implements UserDetails {
/**
* 权限列表
*/
private Set<String> permissions;
/**
* 用户信息
*/
private SysUser user;
public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions) {
this.userId = userId;
this.deptId = deptId;
this.user = user;
this.permissions = permissions;
}
@JSONField(serialize = false)
@Override
public String getPassword() {return user.getPassword();
}
@Override
public String getUsername() {return user.getUserName();
}
/**
* 账户是否未过期, 过期无奈验证
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonExpired() {return true;}
/**
* 指定用户是否解锁, 锁定的用户无奈进行身份验证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonLocked() {return true;}
/**
* 批示是否已过期的用户的凭据(明码), 过期的凭据避免认证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isCredentialsNonExpired() {return true;}
/**
* 是否可用 , 禁用的用户不能身份验证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isEnabled() {return true;}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {return null;}
}
有一些属性我省略掉了,大家能够文末下载源码查看。
小伙伴们看到,这个 LoginUser 实现了 UserDetails 接口,然而和 vhr 中有一个很大的不同,就是这里没有解决 getAuthorities 办法,也就是说当零碎想要去获取用户权限的时候,二话不说间接返回一个 null。这是咋回事呢?
因为在这个脚手架中,未来进行权限校验的时候,是依照上面这样来的:
@PreAuthorize("@ss.hasPermi('system:menu:add')")
@PostMapping
public AjaxResult add(@Validated @RequestBody SysMenu menu) {// 省略}
@PreAuthorize 注解中的 @ss.hasPermi('system:menu:add')
表达式,示意调用 Spring 容器中一个名为 ss 的 Bean 的 hasPermi 办法,去判断以后用户是否具备一个名为 system:menu:add
的权限。一个名为 ss 的 Bean 的 hasPermi 办法如下:
@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);
}
/**
* 判断是否蕴含权限
*
* @param permissions 权限列表
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
private boolean hasPermissions(Set<String> permissions, String permission) {return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
}
}
因为这里是纯手工操作,在比拟的时候,间接获取到以后登录用户对象 LoginUser,再手动调用他的 hasPermissions 办法去判断权限是否满足,因为都是自定义操作,所以是否实现 UserDetails#getAuthorities 办法曾经不重要了,不过依照这里的比对计划,是不反对通配符的比对的。
例如用户具备针对字典表的所有操作权限,示意为 system:dict:*
,然而当和 system:dict:list
进行比拟的时候,发现比拟后果为 false,这块想要比对胜利也是能够的,例如能够通过正则表达式或者其余形式来操作,反正都是字符串比拟,置信大家都能本人搞得定。
当初,前端提供操作页面,也能够配置每一个用户的角色,也能够配置每一个角色能够操作的权限就行了,这个就比较简单了,不多说。
好啦,这就是 TienChin 我的项目中的 RBAC 权限实现计划,前面松哥也会录制相干的视频教程,对视频感兴趣的小伙伴戳这里:TienChin 我的项目配套视频来啦。