文章目录
A 系统概述
a 设计目标
业务服务无感知
本系统设计的一个重要目标就是将权限的管理从业务系统中完全抽离出来。对于后端服务而言,他接受到的请求就一定是合法的,不单操作是合法的,包括操作中间的参数,也应该是符合权限管控的要求。
权限相关的配置,例如角色配置,用户与角色的绑定都由权限管理系统完成。业务服务与权限系统尽可能少的交互仅限于用户添加租户添加的少数情况。
微服务系统支持
在一个良好的微服务架构中,有多个关注不同业务域的微服务组成,当用户的一个操作涉及到多个领域的时候需要BFF来完成聚合,
在不需要BFF的情况下,权限系统的网关方式也可以起到反向代理的作用。
另外作为网关可以记录所有用户的操作,形成操作日志。
数据权限管控
该系统与spring security等权限管理系统最大的差别就是可以对数据权限进行管控。保障用户读取和操作的都是它被授权的数据权限。
SaaS多租户支持
系统支持SaaS多租户的方式,当SssS租户的费用到期或者购买的部分服务到期的时候可以对该服务涉及到的请求进行拦截。
微服务之间调用的鉴权(不支持)
按照微服务的最佳实践来看,微服务之间调用不应该进行鉴权,因为用户进行了一个操作,就需要保证该业务操作的完备性,所以微服务之间不应当进行鉴权,系统也不支持。
b 架构(外部视角)
Gateway模式:
网关模式作为推荐模式。采用网关模式时,权限系统分为两类服务,Security Gateway和Security Service。Gateway负责对所有来自外部请求进行拦截处理,对于合法的请求予以放行,Service是负责涉及到权限管理的配置操作。
对于网关背后的服务而言,不再进行权限的检查,也就是说微服务之间Feign调用,不再进行权限检查。
这种做法的好处是大家有相互独立的数据库,并且大家的服务也是分开部署,各微服务的pom.xml无需增加新的依赖,也适用于python等异构系统。
SDK模式:
在SDK模式下,个微服务需要引入security sdk依赖,sdk会提供对所有的请求进行拦截和权限检查。权限相关的配置信息需要同步到各业务数据库。
这种做法的好处是减少了转发。
c 约束条件
在理想的情况下,接入的系统无需做任何改动便可以接入权限系统,但是这样会大大增加权限相关的配置信息。如果原系统已经遵守以下规范,那么将大大减少配置相关的操作。
全局唯一ID
唯一ID是指任何的东西,比如部门员工都有自己唯一的ID,不使用联合主键来确定唯一性,并且不同类型的数据之间的ID也不会重复,即部门的ID和员工的ID也不应当重复。不同租户之间的员工ID也不应当重复。
REST风格接口
业务系统的API符合rest风格,这里不仅仅是指使用json来作为传送对象,还对于各种业务而言需要遵循以下的约定。
所有的读操作应该使用get请求。用户输入、选择的查询条件应当出现在get请求的parameter里边,而无需用户输入的ID应该出现在URL路径里边。
所有的写操作不应当使用get请求。用户输入请求的参数应当出现在request的body里面,而无需用户输入的ID等信息应当出现在URL里边,并且没有parameter。
更理想的情况下,系统的API设计遵循hateoas风格,参见https://spring.io/guides/gs/rest-hateoas/
d 功能点开发计划
milestone | function | refactor |
---|---|---|
一 防君子: | B端用户在页面上正常操作,不能看到和操作自己没有权限的数据。 | |
添加租户,用户,数据范围 | ||
用户登录,菜单 | ||
像页面提供按钮权限 | 按权限显示按钮(前) | |
操作权限树的维护 | ||
角色管理,用户与角色的绑定 | ||
过滤器对于下拉框接口的处理 | 下拉框对接 | |
原系统添加数据范围表 | ||
原系统对下拉框接口的改造 | ||
二 SaaS: | 各租户的用户不能使用自己没有购买的服务。 | |
租户&子系统管理 | ||
过滤器过滤未购买的服务 | ||
三 集成: | 将一个外部系统无需改造便接入权限系统。 | |
单点登录 | ||
BFF:用户新增,登录 | ||
网关:cookie保持 | ||
S端网关对数据加解密 | ||
四 C端: | C端用户的权限管理。 | |
微信登录,手机号登录等 | ||
网关对请求URL的处理 | ||
五 防小人: | 对绕过页面,使用postman非法调用接口的拦截。 | |
过滤器列表页请求参数的校验 查询SQL加权限(后) | ||
详情页等操作权限的验证 | ||
批量操作的权限验证 | ||
六 其他优化 | ||
使用swagger快速导入操作 | ||
通过模板添加角色 | ||
多数据维度的支持 |
B 对接说明
a 租户SaasTenant
SaaS
Security提供两类接口
Security提供saas用户的维护,包括新增删除有效期。
Security提供子系统的维护,包括SaaS用户是否购买此系统以及有效期。
另外对于使用子系统而言,需要将子系统与页面之间的关系维护在security里面。
在用户登录的时候,需要在页面上填入SssS租户的登录名,其他接口无需提供SaaS信息
非SaaS
对于私有化部署的版本无需提供任何信息,只需使将security配置为私有化部署模式。
b 操作Operation
菜单&页面
Security采用白名单策略,即用户默认无任何操作权限。所有的页面和菜单的权限需要通过相关配置页面给予授权后才能访问。
操作&接口
操作是面向用户而言,一般用户的一个点击会是一个操作。
接口是面向前后端的交互而言,后端controller中间的一个方法是一个接口。
在大多数场景下一个操作对应一个接口,但是在少数的场景下一个操作,可能会对应多个接口,但是在用户的一次点击时只会触发一个接口。例如在设备列表中间可能会显示摄像头和闸机两类设备,在点击详情的时候,若点击的设备为闸机会调用闸机的详情接口,摄像头会调用摄像头的详情接口。系统不应当用将用户的一个操作转换成多次HTTP请求发往后方这样不利于鉴权也不利于操作日志的记录,更加无法保证操作的原子性。
另一种情况是在两个不同的操作会调用后端的同一个接口这里,又要分两种情况来讨论:
一种情况是两个操作有不同的业务含义,例如查询作废订单和查询未作废订单,前端通过传一个boolean值来确定。这样的话权限网关难以判断,该请求是对应的哪一个用户操作难以健全,建议把这两个操作对应成两个接口,如果后端想减少开发工作量,可以再接收请求后调用一个公共的方法来完成请求。
一种是这两种操作是同样的业务含义,在这种情况下两个操作的请求参数是一模一样的,但是却无法分辨从哪一个网页中间发起的该操作,所以难以鉴权,建议分开为两个接口。
数据范围DataSpace
在大多数情况下,系统对数据权限的管理可以依赖于一个数据权限网来完成。
所有的数据都挂载在树上的某一个节点,可以是非叶子节点。而对用户的授权,则将其权限赋予树上的多个节点,当用户拥有某个节点的权限时,就可以看到挂载在该节点下的数据以及该节点的所有子孙节点下挂载的数据。这些数据节点本身也可能蕴含着某种业务含义,比如说从总公司到子公司到分部门,到部门,到小组,或者从社区到小区到楼栋到单元,但是我们在Security系统里不加区别地认为他们都是一个DataSpace,所以当业务系统里增加相应的数据时,需要调用security的DataSpace接口,把数据维护入security系统。示意图如下
注意在更复杂的权限管理中,可能出现以下三种变形,将在后期支持。
按接口鉴权:就是用户在不同的操作时候拥有不同的数据权限,例如可以查看整个公司的订单,但是只能删除某一部门的订单,那么针对这两个操作需要单独的配置数据权限。
倒立树:在某些情况下,用户拥有某个节点的权限后,就拥有了这个节点以及其所有的祖先节点的相应数据权限,例如查看公告,当用户拥有某个部门的公告权限时,他也可以查看这个子公司以及总公司的相关公告。
多维度:在有些场景下数据可以按照多个维度进行划分,不同的人会按照不同的维度来对它进行管理,例如报警事件,既可以按照其所在的空间位置来进行划分,又可以按照其报警的类型来划分。一个楼栋的管理员也许能查看这个楼栋的所有的报警事件,包括火警和盗窃,而一个消防队员有可能去能够查看所有小区的火警报警事件,但是却无法查看盗窃事件。
用户User
在大多数情况下,业务系统中间没有一个明确的新增用户的方法,而往往是伴随着其他的业务操作而新增,例如新增员工和新增业主的时候,这些员工和业主就会成为系统的用户。如同DataSpace一样,业务系统需要在完成这些业务操作的同时,调用是Security相关接口来完成对用户的维护。
在某些场景下,用户的权限可能与其岗位存在着密切相关的联系,那么在这种情况下,需要在security里面预设与岗位对应的角色,并且在新增用户的时候,完成与角色的绑定。
登录Login
为了安全起见,整个登录需要前端向后端请求两个接口。
第一步通过用户输入的租户登录名(saasTenantLoginName)和用户登录名(userLoginName)来获取盐(salt)和时间戳(timestamp)。
第二步根据返回值对用户输入的密码(password4Input)进行加密后,将两个登录名和加密后密码(password4Network)发回后端完成登录,获取菜单栏。(加密方案如下)
在security系统中间并不存有用户的原始密码,而是存有加密过的密码(password4DB)和盐。对加密后的密码进行比较从而判断用户输入的密码是否正确。
加密方案:
password4DB=md5(password4Input+salt)
password4Network=md5(password4DB+timestamp)