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

大家好,我是不才陈某~

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

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

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

  • ACL模型:访问控制列表

  • DAC模型:自主访问控制

  • MAC模型:强制访问控制

  • ABAC模型:基于属性的访问控制

  • RBAC模型:基于角色的权限访问控制

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

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

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

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

RBAC三要素:

  1. 用户:系统中所有的账户

  2. 角色:一系列权限的集合(如:管理员,开发者,审计管理员等)

  3. 权限:菜单,按钮,数据的增删改查等详细权限。

在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。

角色是为了完成各种工作而创造,用户则依据它的责任和资格来被指派相应的角色,用户可以很容易地从一个角色被指派到另一个角色。

角色可依新的需求和系统的合并而赋予新的权限,而权限也可根据需要而从某角色中回收。角色与角色的关系同样也存在继承关系防止越权。

优点:便于角色划分,更灵活的授权管理;最小颗粒度授权;

码猿慢病云管理系统(医疗系统)的用户、角色、权限代表什么呢?

1. 用户

这里的用户和其他系统并无区别,则是能登录系统的用户,对应的表为:codepae/sys_user,字段属性如下:

字段类型注释
user_idbigint用户唯一ID
namevarchar姓名
genderchar性别,字典
usernamevarchar用户名/账号
passwordvarchar密码
dept_idbigint科室ID
hos_idbigint医院ID
saltvarchar随机盐
phonevarchar手机号
avatarvarchar头像
lock_flagchar0-正常,9-锁定
del_flagchar0-正常,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. 角色

在医院中的主要角色则是:医生和护士,这个想必大家都能理解

码猿慢病云管理系统中内置了七个角色,已经完全够用了,如下:

b029d548b687d2e013a2c3c75078634f.png
  • 管理员:这个是每个医院的系统管理员,在添加医院的时候会指定一个管理员

  • 系统管理员:这个是整个系统的管理员,拥有最高权限,可以看到所有医院的数据

  • 医生:医生的角色

  • 护士:护士的角色

对应数据库:codeape/sys_role,如下:

字段类型注释
role_idbigint唯一Id
role_namevarchar角色名称
role_codevarchar角色代码
role_descvarchar角色描述
del_flagchar删除标识:0-正常,1-删除

和用户通过另外一张表存储关联关系:codeape/sys_user_role

字段类型注释
user_idbigint用户唯一ID
role_idbigint角色唯一ID

码猿慢病云管理系统中医生和护士这两个角色的最大区别:护士需要手持PDA(数据采集设备)采集数据(血糖、尿酸、血酮),添加数据等操作,医生则是每天查看患者的数据为治疗提供辅助

3. 权限

码猿慢病云管理系统中的权限有如下三类:

  1. 菜单的权限:客户端菜单的权限

  2. 按钮/接口的权限:客户端按钮/接口的权限,比如添加患者这个

  3. 科室/病区的权限:

1. 菜单的权限

控制客户端的菜单显示,如下:

31476f3ccaf2ab76b0adf3cfd8bda5cd.png

目前有这几个根菜单+子菜单。

2. 按钮权限

客户端按钮的权限,比如新增、删除、编辑按钮的权限,比如住院患者的4个按钮,如下:

b613700022fa662efd5f8b84b0c60bff.png

每个按钮都有一个权限标识编码,比如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. 科室/病区权限

这个权限则是比较特殊了,先描述一下场景:

在医疗系统中,医生/护士是无权限查看全部科室的数据的,只能查看自己负责科室/病区的数据,这样是为了避免医疗事故;你想想如果有个心怀不轨的医生,随意更改其他科室/病区患者的数据,导致医生误判病情,这样的责任谁来承担?

所以医疗系统中都需要控制医护科室/病区这个权限,这样才能保证不发生不必要的医疗事故。

码猿慢病云管理系统中的科室/病区权限如何控制的呢?

在新增医护的时候有个科室权限的多选器,如下图:

03e27894440228007030e2e28daa83b6.png

这样就能轻松设置医护的科室/病区权限了,这部分对应关系是持久化在codeape/sys_user_dept这张表中,如下:

字段类型注释
user_idbigint用户ID
dept_idbigint科室/病区ID
del_flagvarchar删除标志

那么此时当前医护的科室/病区权限如何计算呢?

先给答案,分为两种情况;

第一种:WEB端/PAD端

先说第一种:在WEB端/PAD端,医护的权限=医护所在的科室+医护的科室权限+关联科室

什么意思?比如白鹿这个护士,如下图:

cb9b9be5d26b580cdc4d45fba829e2bb.png

所属科室为内分泌科,科室权限为神经内科+内分泌科+心内科,那么前面两层的权限则是神经内科+内分泌科+心内科(注意去重)

那么关联科室什么意思?比如神经内科下面有个神经内科病区,但是医护的科室在入职时都设置在了神经内科,那么他们如何能看到神经内科病区的数据呢?

一种方案可以在科室权限那一栏再加上神经内科病区,这种当然可行,但是你要为每个医护都设置一遍。

第二种方案:可以将神经内科和神经内科病区关联起来,这样只要有神经内科这个权限,那么就必然有神经内科病区这个权限,这个在码猿慢病云管理系统中也有设置关联关系,如下图:

426c8e095687995ca20b16ce41204209.png

在添加科室的时候可以选择根节点科室。

对应的关系持久化在codeape/sys_dept_relation中,结构如下:

字段类型注释
ancestorbigint祖先节点(科室/病区ID)
descendantbigint子节点(科室/病区ID)

科室/病区权限在用户登录时就会查询出来放到SecurityContext上下文中,具体的方法如下图:

218796b10b9a2ebeb602223a3b53259f.png

那在医护查询数据如何去根据这个科室/病区权限过滤呢?

在请求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

0a2bccef40026efefce0d6ef0d9d62ae.png a330e68aa30d99ef45e733d60d2d287f.png

SQL如下:

com.code.ape.codeape.inhos.mapper.PatInfoHotMapper#selectPatInfoPage

a04e108dd52bd86f58e09c1180abbee5.png

这部分的SQL片段则是根据住院患者的科室去过滤。

第二种PDA端

这里的PDA指的是护士的手持设备,这个设备和智能手机一样,根据自己账号登录上去,给大家大致画一下PDA上都有哪些内容,如下图:

40a6950a643a7d7f22fc1915d5c5642e.png

其实就和APP是一样的,里面可以看到大致四块内容(当然还有其他):

  1. 患者管理:这个是显示所有的住院患者,护士可以选择对应的患者进行数据采集,这样采集的数据才能和患者自定绑定

  2. 检测任务:这个则是医生下的医嘱任务,按照时间段显示,比如医生下的三餐前后测血糖这个医嘱任务,则经过护士拆分后,则变成了6个子任务:空腹测血糖、早餐后测血糖、午餐前测血糖、午餐后测血糖、晚餐前测血糖、晚餐后测血糖。那么在每个时间段显示要测血糖的患者床位即可,护士选择对应的床位则可以测血糖

  3. 检测记录:这里是展示所有患者测量的血糖数据,按照时间段表格形式的展示

  4. 质控:这个则是设备的质控,护士每天要定时对设备进行质控(质量检测),查看这台设备测量是否准确

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

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

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

  1. 在线:连上wifi或者SIM卡,这样能够时刻保持网络畅通的情况下,属于在线状态,拉数据/上传数据直接访问服务端即可

  2. 离线:有些医院没有内网的wifi或者SIM卡,只能连有线网络,一旦设备拔掉网线去病房检测,则是离线的状态

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

27d7f854ce06b6330ab24bd974a5c770.png

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

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

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

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

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

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

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

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

b186d74f8e9df6e138085dc297907ae0.png

总结

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

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

最后说一句(别白嫖,求关注)

陈某每一篇文章都是精心输出,如果这篇文章对你有所帮助,或者有所启发的话,帮忙点赞、在看、转发、收藏,你的支持就是我坚持下去的最大动力!

  1. 点击阅读:《Spring Cloud 进阶》

  2. 点击阅读:《Spring Boot 进阶》

  3. 点击阅读:《Mybatis 进阶》

关注公众号:【码猿技术专栏】,公众号内有超赞的粉丝福利,回复:加群,可以加入技术讨论群,和大家一起讨论技术,吹牛逼!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不才陈某

欢迎关注公众号【码猿技术专栏】

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值