医疗系统的权限就该这样设计,稳!
大家好,我是不才陈某~
前面一节介绍了码猿慢病云管理系统的多租户架构的设计,相信大家对业务也相对了解了一些,这节就来聊一聊码猿慢病云管理系统中的权限是如何设计的?
权限管控可以通俗的理解为权力限制,即不同的人由于拥有不同权力,他所看到的、能使用的可能不一样。对应到一个应用系统,其实就是一个用户可能拥有不同的数据权限(看到的)和操作权限(使用的)。
主流的权限模型主要分为以下五种:
- ACL模型:访问控制列表
- DAC模型:自主访问控制
- MAC模型:强制访问控制
- ABAC模型:基于属性的访问控制
- RBAC模型:基于角色的权限访问控制
目前主流的权限模型是RBAC模型,码猿慢病云管理系统则是使用RBAC模型进行权限控制。
关于以上5种权限模型在之前一篇文章中详细介绍过:权限系统就该这么设计,yyds
RBAC 基于角色的权限访问控制
Role-Based Access Control,核心在于用户只和角色关联,而角色代表对了权限,是一系列权限的集合。
RBAC三要素:
- 用户:系统中所有的账户
- 角色:一系列权限的集合(如:管理员,开发者,审计管理员等)
- 权限:菜单,按钮,数据的增删改查等详细权限。
在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。
角色是为了完成各种工作而创造,用户则依据它的责任和资格来被指派相应的角色,用户可以很容易地从一个角色被指派到另一个角色。
角色可依新的需求和系统的合并而赋予新的权限,而权限也可根据需要而从某角色中回收。角色与角色的关系同样也存在继承关系防止越权。
优点:便于角色划分,更灵活的授权管理;最小颗粒度授权;
在码猿慢病云管理系统(医疗系统)的用户、角色、权限代表什么呢?
1. 用户
这里的用户和其他系统并无区别,则是能登录系统的用户,对应的表为:codepae/sys_user
,字段属性如下:
字段 | 类型 | 注释 |
user_id | bigint | 用户唯一ID |
name | varchar | 姓名 |
gender | char | 性别,字典 |
username | varchar | 用户名/账号 |
password | varchar | 密码 |
dept_id | bigint | 科室ID |
hos_id | bigint | 医院ID |
salt | varchar | 随机盐 |
phone | varchar | 手机号 |
avatar | varchar | 头像 |
lock_flag | char | 0-正常,9-锁定 |
del_flag | char | 0-正常,1-删除 |
其中比较重要的字段:
-
username
:用户登录系统的账号,医疗系统中则是HIS系统中的工号,(username,hos_id,del_flag)
组成唯一索引,同一家医院这个账号必须是唯一 -
hos_id
:医院的ID,多租户架构模式下区分租户的字段 -
dept_id
:科室ID/病区ID,医院中的医生、护士是按照科室/病区管理患者的,因此在入职时就会分配到对应的科室/病区,比如女人的妇科病,去医院看病挂的肯定是妇科,这个妇科门诊则是有对应的医生坐诊,不可能找个骨科的医生去看妇科的毛病
为什么要有科室、病区这两个概念?
科室则是平常我们医院挂号经常看到的科室,比如妇科、骨科、内分泌科、心内科、心脑血管科等
病区这个概念是针对住院来说,住过院的应该都知道护士照顾病人是通过病区->病房->床位号
定位病人,比如二十病区->302房间->10号床
医院为了病房能够更好的管理,节省医护的资源,一个病区中包含多个科室的患者,比如新生儿科、产科、儿科这三个科室你会看到里面的住院患者都是住在同一个病区,同一批护士管理,比如十病区。
这样应该就能理解了,大部分的HIS系统中,医生是划分到科室管理,比如妇科,骨科,毕竟术业有专攻,护士是按照病区划分管理,因为护士本质上是照顾病人,打打针等一些工作,专一性不是那么高;因此在sys_user
用户表中的dept_id
既能表示科室也能表示病区了。
PS:一些比较落后的小医院,HIS系统比较老旧,医生、护士还是按照科室管理
2. 角色
在医院中的主要角色则是:医生和护士,这个想必大家都能理解
码猿慢病云管理系统中内置了七个角色,已经完全够用了,如下:
- 管理员:这个是每个医院的系统管理员,在添加医院的时候会指定一个管理员
- 系统管理员:这个是整个系统的管理员,拥有最高权限,可以看到所有医院的数据
- 医生:医生的角色
- 护士:护士的角色
对应数据库:codeape/sys_role
,如下:
字段 | 类型 | 注释 |
role_id | bigint | 唯一Id |
role_name | varchar | 角色名称 |
role_code | varchar | 角色代码 |
role_desc | varchar | 角色描述 |
del_flag | char | 删除标识:0-正常,1-删除 |
和用户通过另外一张表存储关联关系:codeape/sys_user_role
字段 | 类型 | 注释 |
user_id | bigint | 用户唯一ID |
role_id | bigint | 角色唯一ID |
码猿慢病云管理系统中医生和护士这两个角色的最大区别:护士需要手持PDA(数据采集设备)采集数据(血糖、尿酸、血酮),添加数据等操作,医生则是每天查看患者的数据为治疗提供辅助
3. 权限
码猿慢病云管理系统中的权限有如下三类:
- 菜单的权限:客户端菜单的权限
- 按钮/接口的权限:客户端按钮/接口的权限,比如添加患者这个
- 科室/病区的权限:
1. 菜单的权限
控制客户端的菜单显示,如下:
目前有这几个根菜单+子菜单。
2. 按钮权限
客户端按钮的权限,比如新增、删除、编辑按钮的权限,比如住院患者的4个按钮,如下:
每个按钮都有一个权限标识编码,比如inhos_patinfohot_get
,客户端只需要判断当前登录用户的权限树中是否存在这个权限,有则显示,没有则不显示
3. 接口权限
客户端的接口权限和按钮权限共用,比如查询住院患者这个权限对应的标识也是:inhos_patinfohot_get
那么这个接口权限如何控制?码猿慢病云管理系统中是将接口权限的鉴权下沉到各个微服务,交给开发者在开发接口时通过@PreAuthorize
注解控制权限,比如查询住院患者列表的这个接口,如下:
//com.code.ape.codeape.inhos.controller.PatInfoHotController#getPatInfoHotPage
@Operation(summary = "分页查询在院患者", description = "分页查询在院患者")
@GetMapping("/page" )
@InjectAuth
@PreAuthorize("@pms.hasPermission('inhos_patinfohot_get')" )
public R<Page<PatInfoVO>> getPatInfoHotPage(Page<PatInfoVO> page, PatInfoDTO dto) {
return R.ok(patInfoHotService.listPage(page,dto));
}
@PreAuthorize
这个注解是Spring Security内置的鉴权接口,其中的value
这个属性支持SPEL表达式,真实的实现代码如下:
/**
* @author 公众号:码猿技术专栏 版权:不才陈某所署,侵权必究
* { @link com.code.ape.codeape.common.security.component.PermissionService}
*/
public class PermissionService {
/**
* 判断接口是否有任意xxx,xxx权限
* @param permissions 权限
* @return {boolean}
*/
public boolean hasPermission(String... permissions) {
if (ArrayUtil.isEmpty(permissions)) {
return false;
}
//代码(1)
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return false;
}
//代码(2)
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
////代码(3)
return authorities.stream()
.map(GrantedAuthority::getAuthority)
.filter(StringUtils::hasText)
.anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x));
}
}
逻辑其实很简单,上述代码三个部分:
- 代码(1):从Spring Security 上下文中获取当前用户登录的身份信息
Authentication
,其中就包括权限 - 代码(2):从用户身份信息Authentication中获取用户的权限树
- 代码(3):将当前登录用户的权限和
@PreAuthorize
传入的权限比较,判断是否鉴权通过
4. 科室/病区权限
这个权限则是比较特殊了,先描述一下场景:
在医疗系统中,医生/护士是无权限查看全部科室的数据的,只能查看自己负责科室/病区的数据,这样是为了避免医疗事故;你想想如果有个心怀不轨的医生,随意更改其他科室/病区患者的数据,导致医生误判病情,这样的责任谁来承担?
所以医疗系统中都需要控制医护科室/病区这个权限,这样才能保证不发生不必要的医疗事故。
码猿慢病云管理系统中的科室/病区权限如何控制的呢?
在新增医护的时候有个科室权限的多选器,如下图:
这样就能轻松设置医护的科室/病区权限了,这部分对应关系是持久化在codeape/sys_user_dept
这张表中,如下:
字段 | 类型 | 注释 |
user_id | bigint | 用户ID |
dept_id | bigint | 科室/病区ID |
del_flag | varchar | 删除标志 |
那么此时当前医护的科室/病区权限如何计算呢?
先给答案,分为两种情况;
第一种:WEB端/PAD端
先说第一种:在WEB端/PAD端,医护的权限=医护所在的科室+医护的科室权限+关联科室
什么意思?比如白鹿这个护士,如下图:
所属科室为内分泌科,科室权限为神经内科+内分泌科+心内科,那么前面两层的权限则是神经内科+内分泌科+心内科(注意去重)
那么关联科室什么意思?比如神经内科下面有个神经内科病区,但是医护的科室在入职时都设置在了神经内科,那么他们如何能看到神经内科病区的数据呢?
一种方案可以在科室权限那一栏再加上神经内科病区,这种当然可行,但是你要为每个医护都设置一遍。
第二种方案:可以将神经内科和神经内科病区关联起来,这样只要有神经内科这个权限,那么就必然有神经内科病区这个权限,这个在码猿慢病云管理系统中也有设置关联关系,如下图:
在添加科室的时候可以选择根节点科室。
对应的关系持久化在codeape/sys_dept_relation
中,结构如下:
字段 | 类型 | 注释 |
ancestor | bigint | 祖先节点(科室/病区ID) |
descendant | bigint | 子节点(科室/病区ID) |
科室/病区权限在用户登录时就会查询出来放到SecurityContext上下文中,具体的方法如下图:
那在医护查询数据如何去根据这个科室/病区权限过滤呢?
在请求DTO中有个基础实体类com.code.ape.codeape.common.core.entity.BaseParam
,如下:
@Data
public class BaseParam {
/**
* 医院Id
*/
private Long hosId;
/**
* 用户ID
*/
private Long userId;
/**
* 医护的科室权限
*/
private List<Long> dataAuth;
/**
* 请求来源客户端ID
*/
private String clientId;
/**
* 设备的SN号
*/
private String sn;
}
这个类中的所有属性都会自动注入,只需要在controller方法中标注一个注解:@InjectAuth
,如何做到的呢?
@InjectAuth
这个注解是通过AOP方式自动注入参数,代码如下:
@Slf4j
@RequiredArgsConstructor
@Aspect
public class CodeapeInjectAuthAspect implements Ordered {
@SneakyThrows
@Around("@annotation(injectAuth) &&" +
"(@annotation(org.springframework.web.bind.annotation.PostMapping)||" +
"@annotation(org.springframework.web.bind.annotation.GetMapping)||" +
"@annotation(org.springframework.web.bind.annotation.DeleteMapping)||" +
"@annotation(org.springframework.web.bind.annotation.PutMapping)||" +
"@annotation(org.springframework.web.bind.annotation.RequestMapping))")
public Object around(ProceedingJoinPoint joinPoint, InjectAuth injectAuth) {
if (injectAuth.enable()){
CodeapeUser codeapeUser = Objects.requireNonNull(SecurityUtils.getUser());
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
if (!(args[i] instanceof BaseParam)) {
continue;
}
BaseParam baseParam = (BaseParam) args[i];
/**
* 1. web端:权限就是当前登录用户的权限
* 2. pda端,权限则是设备的权限+当前登录用户的权限
* 这里的权限取值是token中的deptAuths,这个在登录的时候就查询出来,缓存在redis中,所以这里直接用就可以
*/
//医院管理员+系统管理员不赋予权限
boolean flag= ArrayUtil.contains(codeapeUser.getRoleCodes(),SecurityConstants.SYSTEM_ADMIN_CODE)
||ArrayUtil.contains(codeapeUser.getRoleCodes(),SecurityConstants.HOS_ADMIN_CODE);
baseParam.setDataAuth(flag?null:Arrays.asList(codeapeUser.getDeptAuths()));
baseParam.setHosId(codeapeUser.getHosId());
baseParam.setUserId(codeapeUser.getId());
baseParam.setClientId(codeapeUser.getClientId());
baseParam.setSn(codeapeUser.getSn());
}
}
return joinPoint.proceed();
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 4;
}
}
直接取的是CodeapeUser
中的deptAuths
,CodeapeUser则是SecurityContext上下文的用户身份信息
这样则能够取到用户的科室/病区权限,然后则能在SQL中根据这个dataAuth属性去过滤数据了,比如分页查询住院患者的接口,controller方法如下:
com.code.ape.codeape.inhos.controller.PatInfoHotController#getPatInfoHotPage
SQL如下:
com.code.ape.codeape.inhos.mapper.PatInfoHotMapper#selectPatInfoPage
这部分的SQL片段则是根据住院患者的科室去过滤。
第二种PDA端
这里的PDA指的是护士的手持设备,这个设备和智能手机一样,根据自己账号登录上去,给大家大致画一下PDA上都有哪些内容,如下图:
其实就和APP是一样的,里面可以看到大致四块内容(当然还有其他):
- 患者管理:这个是显示所有的住院患者,护士可以选择对应的患者进行数据采集,这样采集的数据才能和患者自定绑定
- 检测任务:这个则是医生下的医嘱任务,按照时间段显示,比如医生下的三餐前后测血糖这个医嘱任务,则经过护士拆分后,则变成了6个子任务:空腹测血糖、早餐后测血糖、午餐前测血糖、午餐后测血糖、晚餐前测血糖、晚餐后测血糖。那么在每个时间段显示要测血糖的患者床位即可,护士选择对应的床位则可以测血糖
- 检测记录:这里是展示所有患者测量的血糖数据,按照时间段表格形式的展示
- 质控:这个则是设备的质控,护士每天要定时对设备进行质控(质量检测),查看这台设备测量是否准确
那么问题来了,PDA上需要显示的数据是整个医院的数据吗?显然不可能,也是需要根据护士科室/病区权限过滤,可以看到上方有一个科室的筛选项,默认是所有科室,则是当前登录用户的所有科室权限
那么PDA端的医护权限和WEB端一样吗?
当然不是,因为设备存在以下两种网络情况:
- 在线:连上wifi或者SIM卡,这样能够时刻保持网络畅通的情况下,属于在线状态,拉数据/上传数据直接访问服务端即可
- 离线:有些医院没有内网的wifi或者SIM卡,只能连有线网络,一旦设备拔掉网线去病房检测,则是离线的状态
离线情况下就需要设备本地做缓存了,那么这个缓存的数据到底拉哪些数据?不可能将整个医院的数据都拉下来,设备的内存是有限的。这个时候就需要用到设备的权限了,在添加设备的时候有两个的选项,如下:
科室和关联科室这两个选项,一般一台设备只供一个病区使用,此时将科室选项选择对应的病区即可,那么特殊情况下,比如一病区和二病区共用一台设备,此时就需要用到关联科室了。
此时应该明白了设备的权限=科室+关联科室
只要设备连上网络,在用户不登录的情况下,调用接口获取数据的时候就应该获取的是设备权限的数据。
那登录该台设备的用户权限呢?应该怎么取值呢?很简单,分为两种情况:
- 用户的权限和设备的权限取并集
- 用户的权限和设备的权限取交集
按照正常的逻辑是应该是第二种情况取交集,因为你护士没有这个权限就不应该看到该科室/病区的数据,但是实际情况是很多医院的护士都是轮转的,比如这个月到一病区,下个月到二病区,他们的科室/病区权限的维护并不是很及时,主要是信息科的人员太懒了,这样的话就会导致如果按照第二种情况取交集,那么这个护士登录该台设备就看不到自己所管的科室/病区的数据了。所以在码猿慢病云管理系统中是采用的第一种方案。
相关代码在com.code.ape.codeape.common.security.service.CodeapePDAUserDetailsServiceImpl#getUserDetails
,如下图:
总结
这节内容介绍了RBAC权限模型以及码猿慢病云管理系统中权限是如何设计的,最重要的是科室/病区权限的设计,大家一定要理解其中的逻辑,几乎所有的医疗系统都是按照这个逻辑处理的。
本节内容摘录了部分代码,关于代码的详细介绍会在后面文章中介绍,现在先了解一下。
文章转载自公众号: 码猿技术专栏