医疗系统的权限就该这样设计,稳!

laomugua
发布于 2023-9-13 15:02
浏览
0收藏

大家好,我是不才陈某~

前面一节介绍了​​码猿慢病云管理系统​​​的多租户架构的设计,相信大家对业务也相对了解了一些,这节就来聊一聊​​码猿慢病云管理系统​​中的权限是如何设计的?

权限管控可以通俗的理解为权力限制,即不同的人由于拥有不同权力,他所看到的、能使用的可能不一样。对应到一个应用系统,其实就是一个用户可能拥有不同的数据权限(看到的)和操作权限(使用的)。

主流的权限模型主要分为以下五种:

  • ACL模型:访问控制列表
  • DAC模型:自主访问控制
  • MAC模型:强制访问控制
  • ABAC模型:基于属性的访问控制
  • RBAC模型:基于角色的权限访问控制

目前主流的权限模型是RBAC模型,​​码猿慢病云管理系统​​则是使用RBAC模型进行权限控制。

关于以上5种权限模型在之前一篇文章中详细介绍过:​​权限系统就该这么设计,yyds​

RBAC 基于角色的权限访问控制

Role-Based Access Control,核心在于用户只和角色关联,而角色代表对了权限,是一系列权限的集合。

RBAC三要素:

  1. 用户:系统中所有的账户
  2. 角色:一系列权限的集合(如:管理员,开发者,审计管理员等)
  3. 权限:菜单,按钮,数据的增删改查等详细权限。

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-删除

其中比较重要的字段:

  1. ​username​​​:用户登录系统的账号,医疗系统中则是HIS系统中的工号,​​(username,hos_id,del_flag)​​组成唯一索引,同一家医院这个账号必须是唯一
  2. ​hos_id​​:医院的ID,多租户架构模式下区分租户的字段
  3. ​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. 按钮/接口的权限:客户端按钮/接口的权限,比如添加患者这个
  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是一样的,里面可以看到大致四块内容(当然还有其他):

  1. 患者管理:这个是显示所有的住院患者,护士可以选择对应的患者进行数据采集,这样采集的数据才能和患者自定绑定
  2. 检测任务:这个则是医生下的医嘱任务,按照时间段显示,比如医生下的三餐前后测血糖这个医嘱任务,则经过护士拆分后,则变成了6个子任务:空腹测血糖、早餐后测血糖、午餐前测血糖、午餐后测血糖、晚餐前测血糖、晚餐后测血糖。那么在每个时间段显示要测血糖的患者床位即可,护士选择对应的床位则可以测血糖
  3. 检测记录:这里是展示所有患者测量的血糖数据,按照时间段表格形式的展示
  4. 质控:这个则是设备的质控,护士每天要定时对设备进行质控(质量检测),查看这台设备测量是否准确

那么问题来了,PDA上需要显示的数据是整个医院的数据吗?显然不可能,也是需要根据护士科室/病区权限过滤,可以看到上方有一个科室的筛选项,默认是所有科室,则是当前登录用户的所有科室权限

那么PDA端的医护权限和WEB端一样吗?

当然不是,因为设备存在以下两种网络情况:

  1. 在线:连上wifi或者SIM卡,这样能够时刻保持网络畅通的情况下,属于在线状态,拉数据/上传数据直接访问服务端即可
  2. 离线:有些医院没有内网的wifi或者SIM卡,只能连有线网络,一旦设备拔掉网线去病房检测,则是离线的状态

离线情况下就需要设备本地做缓存了,那么这个缓存的数据到底拉哪些数据?不可能将整个医院的数据都拉下来,设备的内存是有限的。这个时候就需要用到设备的权限了,在添加设备的时候有两个的选项,如下:

医疗系统的权限就该这样设计,稳!-鸿蒙开发者社区

科室和关联科室这两个选项,一般一台设备只供一个病区使用,此时将科室选项选择对应的病区即可,那么特殊情况下,比如一病区和二病区共用一台设备,此时就需要用到关联科室了。

此时应该明白了设备的权限=科室+关联科室

只要设备连上网络,在用户不登录的情况下,调用接口获取数据的时候就应该获取的是设备权限的数据。

那登录该台设备的用户权限呢?应该怎么取值呢?很简单,分为两种情况:

  1. 用户的权限和设备的权限取并集
  2. 用户的权限和设备的权限取交集

按照正常的逻辑是应该是第二种情况取交集,因为你护士没有这个权限就不应该看到该科室/病区的数据,但是实际情况是很多医院的护士都是轮转的,比如这个月到一病区,下个月到二病区,他们的科室/病区权限的维护并不是很及时,主要是信息科的人员太懒了,这样的话就会导致如果按照第二种情况取交集,那么这个护士登录该台设备就看不到自己所管的科室/病区的数据了。所以在​​码猿慢病云管理系统​​中是采用的第一种方案。

相关代码在​​com.code.ape.codeape.common.security.service.CodeapePDAUserDetailsServiceImpl#getUserDetails​​,如下图:

医疗系统的权限就该这样设计,稳!-鸿蒙开发者社区

总结

这节内容介绍了RBAC权限模型以及​​码猿慢病云管理系统​​中权限是如何设计的,最重要的是科室/病区权限的设计,大家一定要理解其中的逻辑,几乎所有的医疗系统都是按照这个逻辑处理的。

本节内容摘录了部分代码,关于代码的详细介绍会在后面文章中介绍,现在先了解一下。


文章转载自公众号: 码猿技术专栏

分类
已于2023-9-13 15:02:43修改
收藏
回复
举报
回复
    相关推荐