这篇文章最后留下来一个问题,项目就是项目用户的权限该如何设置?今天我们就来聊聊这个话题。 1. 角色与权限首先我们先来看看角色与权限,项目该如何设计角色与权限,项目其实有很多非常成熟的项目理论,最为常见的项目莫过于 RBAC 了。 1.1 RBAC 简介RBAC(Role-based access control)是项目一种以角色为基础的访问控制(Role-based access control,RBAC),项目它是项目一种较新且广为使用的权限控制机制,这种机制不是项目直接给用户赋予权限,而是项目将权限赋予角色。 RBAC 权限模型将用户按角色进行归类,项目通过用户的项目角色来确定用户对某项资源是否具备操作权限。RBAC 简化了用户与权限的项目管理,它将用户与角色关联、项目角色与权限关联、权限与资源关联,这种模式使得用户的授权管理变得非常简单和易于维护。 1.2 RBAC 的提出权限、b2b供应网角色这些东西,在早期 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/projects/Role-Based-Access-Control 如果小伙伴们有中文的资料链接,欢迎留言说明。 1.3 RBAC 三原则最小权限:给角色配置的权限是其完成任务所需要的最小权限集合。责任分离:通过相互独立互斥的角色来共同完成任务。亿华云数据抽象:通过权限的抽象来体现,RBAC 支持的数据抽象程度与 RBAC 的实现细节有关。数据抽象:通过权限的抽象来体现,RBAC 支持的数据抽象程度与 RBAC 的实现细节有关。 1.4 RBAC 模型分类1.4.1 RBAC0RBAC0 是最简单的用户、角色、权限模型。RBAC0 是 RBAC 权限模型中最核心的一部分,后面其他模型都是在此基础上建立。 
在 RBAC0 中,一个用户可以具备多个角色,一个角色可以具备多个权限,最终用户所具备的权限是用户所具备的角色的权限并集。 1.4.2 RBAC1RBAC1 则是在 RABC0 的基础上引入了角色继承,让角色有了上下级关系。 
在本系列前面的文章中,松哥也曾多次向大家介绍过 Spring Security 中的角色继承。 1.4.3 RBAC2RBAC2 也是在 RBAC0 的基础上进行扩展,引入了静态职责分离和动态职责分离。 
要理解职责分离,我们得先明白角色互斥。 在实际项目中,有一些角色是互斥的,对立的,例如财务这个角色一般是不能和其他角色兼任的,否则自己报账自己审批,岂不是爽歪歪! 通过职责分离可以解决这个问题: 静态职责分离 在设置阶段就做好了限制。比如同一用户不能授予互斥的角色,用户只能有有限个角色,用户获得高级权限之前要有低级权限等等。 动态职责分离 在运行阶段进行限制。比如运行时同一用户下5个角色中只能同时有2个角色激活等等。 1.4.4 RBAC3将 RBAC1 和 RBAC2 结合起来,就形成了 RBAC3。 
1.5 扩展我们日常见到的很多权限模型都是在 RBAC 的基础上扩展出来的。 例如在有的系统中我们可以见到用户组的概念,就是将用户分组,用户同时具备自身的角色以及分组的角色。 我们 TienChin 项目所用的脚手架中的权限,就基本上是按照 RBAC 这套权限模型来的。 2. 表设计我们来看下 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 代码中该怎么做呢? 3. 代码实现首先定义了一个 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 Setpermissions; / *** 用户信息 */ private SysUser user; public LoginUser(Long userId, Long deptId, SysUser user, Setpermissions) { 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 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(Setpermissions, String permission) { return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission)); } }由于这里是纯手工操作,在比较的时候,直接获取到当前登录用户对象 LoginUser,再手动调用他的 hasPermissions 方法去判断权限是否满足,由于都是自定义操作,所以是否实现 UserDetails#getAuthorities 方法已经不重要了,不过按照这里的比对方案,是不支持通配符的比对的。 例如用户具备针对字典表的所有操作权限,表示为 system:dict:*,但是当和 system:dict:list 进行比较的时候,发现比较结果为 false,这块想要比对成功也是可以的,例如可以通过正则表达式或者其他方式来操作,反正都是字符串比较,相信大家都能自己搞得定。 现在,前端提供操作页面,也可以配置每一个用户的角色,也可以配置每一个角色可以操作的权限就行了,这个就比较简单了,不多说。 |