RBAC用户权限设计
用户权限是指用户完成身份认证,成功登录系统后能做什么,能使用哪些功能。这也是几乎所有企业系统都必须具备的功能。开放式的互联网系统中所有普通用户的权限都是相同的,只有当用户等级不同时,才会在基础功能之上提供区别于普通用户的增值功能。
企业系统主要采用RBAC权限模型进行权限设计。RBAC可以演化出用户角色权限模型、继承模型、用户组模型、权限组模型等。
5.7.1 RBAC权限模型
RBAC(Role Based Access Control,基于角色的访问控制)是业界使用较多的权限模型,它较好地解决了用户与权限之间的耦合性问题。
例如,现有表5-6所示的系统权限需要分配,应该如何设计呢?
表5-6 权限分配矩阵示例
在不使用RBAC的情况下,直接将权限分配给用户,情况会变得非常复杂,每个人都会有很多的权限,关系如图 5-20所示。如果一个系统有成千上万个用户,那么将变得不可想象,简直就是一张蜘蛛网,对系统管理人员也会造成巨大的困扰。
在用户与权限之间增加角色的概念,就形成了RBAC的基础模型— 用户角色权限模型(图5-21),以此达到了解耦的目的,这也是架构师经常采用的分层设计原理。用户角色权限模型也是应用最广、最基础的权限模型。
图5-20 用户权限直接关联
图5-21 用户角色权限模型
引入RBAC模型之后,权限都分配在角色上,再将角色与用户关联,如此使用户权限的分配变得十分清晰,如图5-22所示。
图5-22 用户角色权限关联示例
1.权限的分类和区别
权限包含页面权限、操作权限和数据权限,每一层权限都是递进关系,从而形成树结构,设计者必须要厘清三者所包含的内容,如图5-
23所示。
(1) 页面权限:一般是指页面或菜单权限,它处于权限的最顶层,一般是其他权限获取的先决条件,通常采用控制用户是否可以看到某个页面或菜单来控制访问权限。例如,普通员工只能看到报销系统中的申请记录、办理进度等基本菜单,而看不到财务转账、财务报表等菜单。
(2) 操作权限:是指增、删、改、查权限,一般通过页面中的按钮、热点区域和某些事件触发。控制方式主要有两种:一种是用户可以看到,但是不一定可以操作,如禁用按钮或在用户点击按钮时返回错误提示;另一种是让用户看不到,不同权限的人进入同一个功能页面,看到的内容是不同的。例如,作为部门负责人、财务主管,都可以使用收支明细账管理功能,但是部门负责人只能看,不能改,而财务主管既可以看,也可以改。
(3) 数据权限:是指数据的可见范围和可操作范围,属于操作权限的下一层能级。例如,同样的员工信息表,有些人可以看到工资栏,有些人却看不到。用户只可以查看自己的订单,而不可以查看其他人的订单。
图5-23 页面权限、操作权限和数据权限
2.权限控制原则
页面权限控制优先于操作权限控制,操作权限控制优先于数据权限控制。那是不是意味着做好了页面权限和操作权限就不需要考虑数据权限了呢?
例如,订单查询RESTful接口http://xxx:xx/order/id是根据id查询订
单的详细信息。只要更换id参数,就可以查询任意订单信息,就算不属于自己的订单也可以查,这样就造成了数据泄露。因此,只在表面上做安全控制是远远不够的。
3.关系型数据库设计
如图5-24所示,用户与角色为多对多关系,一个用户可以有多个角色,一个角色也可以授权给多个用户。角色与权限也为多对多关系,一个角色可以有多个权限,一个权限也可以分配给多个角色。
图5-24 用户、角色、权限ER图(1)
通过图5-25所示的ER图可以得出用户表、角色表、权限表、用户角色关联表、角色权限关联表的结构,如表5-7~表5-11所示。
图5-25 用户、角色、权限ER图(2)
表5-7 用户表的结构(主要字段)
表5-8 角色表的结构(主要字段)
表5-9 权限表的结构(主要字段)
表5-10 用户角色关联表的结构
表5-11 角色权限关联表的结构
5.7.2 RBAC权限继承
虽然RBAC权限模型可以简化用户和权限之间的配置复杂度,但是依然会面临一些问题。例如,有这样一个需求,普通用户只具有修改密码、修改手机号等基础权限;初级财务人员不仅具有普通用户权限,还具有制作工资单权限;中级财务人员具有初级财务人员的全部权限,同时具有工资单审核权限、转账申请权限;高级财务人员具有中级财务人员的全部权限,同时具有转账审批权限。
如果使用图5-26这种用户角色权限模型,则授权模型如图5-27所示。每种角色都要重复勾选很多的权限,大量的权限交叉,当公司内的岗位、职责很多,或者相同的岗位只是权限存在细微差别时,就要重新创建一个角色,并且把所有权限重新分配给它。长此以往,系统内的角色越来越多,最极端的情况就是,每个用户都对应一个角色。
图5-26 用户角色权限模型
图5-27 用户、角色、权限关系实例
当然,可以采用一个用户授予多个角色的方式来完成,可以将用户同时授予普通用户、初级财务、中级财务和高级财务4个角色,但是并不能很好地解决这个问题。仔细分析一下,在本需求中角色之间是存在继承关系的,高级职位所具有的权限包含低级职位所具有的权限,即高级职位继承了低级职位。利用继承关系,可以进一步简化授权复杂度,如图5-28所示。
图5-28 角色继承关系
如图5-29所示,每种角色只需要关注自身所独有的权限即可,每种角色与其他角色的功能均不存在交叉,授权结构也变得清晰、直观。
图5-29 角色继承关系实例
继承模式需要维护角色之间的继承关系,以便于找到某个角色所具有的全部权限,从而增加了系统的实现难度。例如,要不断地找到高级财务角色的父节点,父节点的父节点,以此类推,并且将每个父节点的权限进行叠加。
这种方式固然简化了管理员的维护成本,也建立起了角色之间的关系,但是却增加了系统的复杂度。此种结构适用于公司管理组织架构简单清晰、职责交叉度较低的企业。
应该尽量避免出现多继承,以免角色关系混乱,难以控制,如图5-30所示。
图5-30 复杂的角色继承关系
角色A继承自角色B、C,而角色C又继承自角色E、D,当角色特别多时,就会出现混乱,数据关系复杂,管理员无法管理。因此,继承模型应该尽量采用单继承的模式,这适合组织架构以树结构为主的场景,最终应该形成一棵单继承树,如图5-31所示。
图5-31 角色单继承
注意没有完美通用的设计,指望使用一套模型适用于所有场景是很难实现的。所以,架构设计的原则是以业务为驱动,去寻找满足业务场景的最佳设计。
关系型数据库表设计:继承模式下,只需要改变角色表的设计即可,表结构如表5-12所示。
表5-12 角色表的结构
5.7.3 RBAC权限模型演进
对于RBAC权限模型还可以做一些改进,但是这些改进完全取决于公司内的用户数量和角色数量。
1.用户组模型
图 5-32所示的用户组模型,适用于经常要把多个角色授权给一类用户的情况,或者为同一批人增加相同角色的情况。例如,新入职的每个后端开发人员都要授予普通用户、文档管理、代码管理、后端开发和数据库管理4个角色,每个用户都要选择4个角色就很麻烦。
图5-32 用户组模型
如图5-33所示,抽象出一层用户组,将每个前端工程师放入前端组,每个后端工程师放入后端组,让前端组、后端组分别绑定所需的角色就可以大大地简化授权复杂度。
图5-33 用户组权限关系示例
如图5-34所示,如果要为每个后端员工都新增一个知识库角色,允许他们去查阅和编辑知识库的信息,则只需要将新的角色设计好之后,直接赋权给后端组即可,这样所有的后端员工就都拥有了新的权限。如果没有用户组的抽象,一个公司就算只有100名后端员工,管理员也要操作100次才可以。在Windows、macOS等操作系统中,都具有用户组的设计理念。
图5-34 用户组权限增加角色示例
关系型数据库表设计:需要新增用户组定义表、用户与用户组关联表、用户组角色关联表,各表结构分别如表5-13~表5-15所示。
表5-13 用户组定义表的结构
表5-14 用户与用户组关联表的结构
表5-15 用户组角色关联表的结构
2.权限组模型
图5-35所示的权限组模型与用户组模型原理相同,主要是为了解决权限过多、需要反复授权的情况。 因此,抽象出一层权限组,作为角色与权限之间的纽带。
图5-35 权限组模型
权限组模型的授权关系如图5-36所示,由于与用户组模型原理相同,表结构设计也类似,因此也需要新增权限组定义表、角色与权限组关联表、权限定义与权限组关联表。
图5-36 权限组模型的授权关系
无论采用哪种权限模型,都可以解决用户与权限之间的关联问题,并没有哪个设计是最好的,而是要根据用户数量、企业的组织架构等实际情况进行选择。不能将简单的问题复杂化,也不能将复杂的问题简单化。
5.8 互联网权限架构设计
随着分布式架构的占有率逐年攀升,无论是传统企业还是互联网企业,现在都在构建自己的分布式微服务架构体系。传统的基于
Session的权限控制存在共享、内存占用过高等问题,因此现在普遍采用基于Token的轻量化解决方案,对于分布式系统具有良好的支持。
这涉及Token的生成、发放、有效期、刷新、延期等事件。Token 的使用根据不同的场景可以有很多种变化,包含基于Token的访问控制、SecretID和SecretKey模式、JWT模式等多种设计方案。
5.8.1 基于Token的访问控制
有状态接口设计会导致程序必须要考虑到Session的存储、迁移、共享问题,所以最适合弹性伸缩的设计为无状态接口设计。注意无状态接口并不是说所有接口都不需要登录就可以访问,那样系统将完全暴露在危险之中。
1.基于Token(令牌)的无状态接口设计名词解释如下。
(1) 认证服务:持有用户的信息,可以比对用户名和密码的服务。用于发放、验证、查询、删除和刷新Token的服务。
(2) 资源服务:完成真正的业务处理的服务,如订单服务、支付服务等。
基于Token的访问控制流程如图 5-37所示。
(1) 客户端请求授权(登录),提交用户名、密码和客户端标识。客户端标识就是代表客户端身份的证明。例如,用户不可以直接发送请求去调用某银行的登录接口,必须使用银行自己的App才可以调用,因为客户端必须是银行服务器认可的客户端。
(2) 认证授权中心校验用户名、密码、客户端身份,如果核对无误,则会生成Token(唯一的字符串),同时将Token存入缓存,并设置有效期。
(3) 认证服务将Token返回给客户端。
(4) 客户端收到Token后,将Token存储到Local Storage中。
(5) 客户端后续访问系统其他资源(API)时,携带此Token并放入HTTP Header的Authorization中,保证Token的安全性。
(6) 资源服务器收到Token后,请求认证授权中心,验证Token的合法性。
(7) 认证服务检查Token是否有效和是否过期,并将验证结果返回给资源服务器。
(8) 如果Token无效,则拒绝访问受保护资源,有效则允许访问,完成业务处理。
(9) 资源服务器返回业务处理结果。
这种思想是客户端先通过认证,获取访问令牌;然后再携带令牌访问服务器,其本质其实与Cookie很像,但是规避了Cookie的各种弊端。利用Token机制的协议和技术有很多,最常用的就是OAuth 2.0协议和JWT技术。
2.架构优点
(1) 集中认证和授权,便于集中化地管理Token,系统架构更清晰,职责更单一。
(2) 使用缓存服务器存储Token,访问效率更高,可以对Token做主动失效处理。
(3) 一旦篡改Token,则Token立即无效,必须重新获取,保证了安全性。
(4) 无限制水平扩展,架构弹性更好。
3.架构缺点
(1) 认证中心除了要负责制作和发放Token,还要负责刷新、删除和验证Token。
(2) 当服务请求量巨大时,认证服务器压力会较大。
图5-37 基于Token的访问控制流程
5.8.2 SecretID和SecretKey模式
如果对接微信、支付宝、淘宝、阿里云等多租户的开放平台的API 接口,就会看到SecretID和SecretKey模式设计。可以直接将SecretID和 SecretKey模式理解为OAuth 2.0协议的密码模式,区别是这个用户名和密码不是直接给用户使用的,而是给接入方的系统使用的。
基于SecretID和SecretKey的安全控制流程如图5-38所示。
(1) 首先会在云厂商的后台系统中新建自己的应用,平台会为应用生成SecretID和SecretKey,这就是客户端标识,也可以通俗地理解为服务器的用户名和密码。此时调用方的服务器相较于第三方平台,就变为了客户端的身份。
(2) 使用SecretID和SecretKey请求第三方服务器。
(3) 第三方服务器验证SecretID和SecretKey的有效性,并生成
Token。
(4) 第三方服务器将Token、有效期、刷新Token等信息返回给客户端。
(5) 客户端携带Token去访问第三方服务接口,如发送短信、支付等。
图5-38 基于SecretID和SecretKey的安全控制流程
其实SecretID和SecretKey模式本质上就属于Token模式,只是Token 模式主要集中在服务端调用。
如果是处在内网环境中的服务器之间发生相互的调用,则也可以采用此种模式做安全控制,但是这会增加系统的复杂度、降低接口调用的效率,并且内网环境相对安全,因此不推荐采用此种模式,而是推荐采用直接调用的模式,或者OAuth 2.0协议的客户端模式。如果将自己系统本身的某个接口开放给外部调用,非自己系统体系,也非公司内的其他服务调用,则一定要使用Token模式进行控制来增加安全性,并在网络设备上增加白名单,只允许特定IP的服务访问。
5.8.3 JWT模式
JWT(JSON Web Token)是一个开放的标准,定义了用于在各方之间交互的安全JSON对象。
1. JWT 的组成结构
JWT本质上也是一个Token,它由三部分构成:Header(头部)、
Payload(有效负荷)和Signature(签名),中间用符号“.”分隔。例如,下面的就是一个JWT串。
(1)Header(头部)。
Header通常由两部分组成。
① type:类型,一般为JWT。
② alg:加密算法,通常是HMAC SHA256或RSA。
例如,{"typ":"JWT", "alg":"RSA"}。
Header信息经过Base64UrlEncode加密,从而得到第1部分的加密串。
(2)Payload(有效负荷)。
Payload是用来携带有效信息的载体。
① Registered claims:注册声明,这是一组预定的声明,但并不强制要求。它提供了一套有用的、能共同使用的声明。主要有iss(JWT 签发者)、exp(JWT过期时间)、sub(JWT面向的用户)、aud(接收JWT的一方)等。
② Public claims:公开声明,公开声明中可以添加任何信息,一般是用户信息或业务扩展信息等。
③ Private claims:私有声明,由JWT提供者和消费者共同定义的声明,既不属于注册声明,也不属于公开声明。
④ 不建议在Payload中添加任何敏感信息,因为Base64是对称加解密的,这意味着Payload中的内容都是可见的。
例如,{"AccountID":"19898921", "name":"Kevin" , "timestamp":9878178281}。
Payload信息经过Base64UrlEncode加密,从而得到第2部分的加密串。
(3)Signature(签名)。
Signature用来放置签名信息,起到防篡改的作用。
① 将Header加密串和Payload加密串使用“.”连接起来,然后使用 Header中alg的算法,再加上密钥进行加密,从而获得Signature加密串。
② 算法简写:
RSA(Base64UrlEncode(header)+"."+Base64UrlEncode(payload), secrt)。最后将3段加密串使用“.”连接在一起,就得到了最终的JWT。
2. JWT 安全控制流程
基于JWT的安全控制流程如图5-39所示。
(1) 客户端请求认证服务,需要携带自身客户端标识、用户名和密码。
(2) 认证服务验证无误,则将用户信息(用户ID、姓名等基本信息)封装在JWT的Payload部分,生成JWT,然后使用公钥进行加密,使用私钥进行签名。
(3) 将JWT返回给客户端。
(4) 客户端将JWT存储到Local Storage中。
(5) 客户端携带JWT访问后端其他资源(接口)。
(6) 资源服务器对JWT进行公钥验签和私钥解密,获取用户信息和有效时长,验证JWT是否有效,如果有效,则继续处理业务请求。
(7) 将处理结果返回给客户端。
图5-39 基于JWT的安全控制流程
3. 架构优点
(1) 在第2步并没有将JWT串存入Redis等缓存中,降低了系统设计的复杂度。
(2) 在第6步资源服务器收到业务请求后,也没有携带JWT去认证中心验证其有效性。只要验签通过、解密正常,则认为JWT是一个合法的Token。解密后即可获取Token的过期时间、用户ID等信息,从而判断Token是否过期。因此,系统的压力被分散到各个资源服务器上,而不会与Token模式一样,压力集中在认证服务器上。
4. 架构缺点(1)服务端的验签和解密会对服务器造成一定的压力,高并发场景下CPU承压增加。
(2) JWT一旦生成有效时间被固定,则不可更改,只能更换
JWT。
(3) 会话保持无法自动延长,只能由客户端自己控制,重新申请。
(4) JWT中可以存储业务敏感信息,存在一定风险。
5.8.4 微服务模式下的Token权限设计
在微服务架构下,可以充分利用网关来进行统一的Token验证,从而不需要每个资源服务器都实现Token验证,极大地简化了开发模式。
1.微服务架构下的Token权限设计微服务架构下的Token权限设计如图5-40所示。
(1) 客户端向网关发起Token申请,网关进行基础校验,校验客户端身份是否正确。
(2) 网关将请求转发给认证授权中心(持有用户数据),申请新的Token。
(3) 校验用户名和密码,并生成Token,存储到缓存中,设置有效期。
(4) 认证授权中心将Token返回给网关。(5)网关将Token返回给客户端,携带有效期和刷新码(用于刷新Token使用,与Token成对出现)。
(6) 客户端将Token存储到Local Storage或Session Storage中,后续携带Token并放置在HTTP Header的Authorization中访问。
(7) 当客户端需要访问资源服务器时,需要携带客户端标识和
Token串,网关先进行客户端身份校验,如果通过,则直接从Redis中查询Token信息。
(8) 如果网关可以查询到有效的Token数据,则通过校验并放行资源访问,否则拒绝访问。
图5-40 微服务架构下的Token权限设计
2.架构优点
(1) 认证授权中心统一发放、刷新和删除Token,集中管理。
(2) 网关做统一的客户端身份校验、Token验证,避免了所有资源服务器开发。(3)微服务网关除与认证中心搭配使用外,还可以结合JWT模式使用。
3.架构缺点
(1) 由于所有的认证工作均由认证授权中心完成,因此认证授权中心的实现复杂度也相应提高。
(2) 同样的原因,导致认证授权中心服务压力较大,需要做好高可用设计(避免单节点故障)和缓存设计(提高查询速度和并发能力)。
5.8.5 Token的延时与刷新
无论使用Token还是使用JWT,都会面临Token的延时和刷新问题。例如,用户成功登录了系统A,Token的有效时长为10分钟,用户正在进行支付操作,而此时Token刚好过期,就会导致用户被踢出,只能再重新登录和重新支付,显然是不合理的。
这就需要Token具有动态延时和刷新等能力,类似Session的自动延时功能。首先要区分延时和刷新这两个概念。
(1) Token延时:是指原Token不发生变化,只是重置Token的有效时长。例如,原Token还剩余1分钟过期,则延时之后Token有效时长恢复为10分钟。
(2) Token刷新:是指重新生成新的Token,Token串发生了变化,同时有效期恢复为10分钟。
1.自动延时设计自动延时设计时模仿Session的延时效果,客户端每次携带Token访问服务端时,如果服务端发现该Token有效,则会将Token的有效期恢复为10分钟。因此,除非客户端10分钟内没有任何操作Token才会失效,超过10分钟后再操作Token才会被踢出。
自动延时有一个致命的缺陷,就是一旦Token丢失,攻击方就可以使用此Token,持续与服务端保持会话,Token相当于永久有效,所以笔者一般不建议采用此种延时设计,而是采用刷新设计。
2. Token刷新设计
Token刷新一般有两种方式,即客户端主动刷新和服务端定时任务刷新。
(1) 客户端主动刷新。
服务端生成Token后,会连同Token的有效时长和截止失效时间返回给客户端。Web或原生客户端在每次发起请求时都需要先判断Token
是否即将过期,如果即将过期,则调用Token刷新接口重新获取
Token,再使用新的Token去访问系统。
也可以在每次调用接口前,都重新获取一次Token,显然这种方式效率极低,因此不建议采用。
(2) 服务端定时任务刷新。
可以在服务端定义定时任务,每隔几分钟去刷新一次Token,从而保证Token得到持续的延长和更换。
Token刷新设计流程如图5-41所示,包含以下4个步骤。
(1)客户端申请Token。
(2)认证服务生成并返回Token信息,Token一般由Token串和刷新Token串和有效时长三部分构成,例如:
(3) 客户端请求刷新Token,需要携带原Token串和refreshToken 串。
(4) 认证服务端需要比对原Token串是否有效,并且Token与
refreshToken是否相互匹配,才会生成新的Token和refreshToken返回给客户端。
图5-41 Token刷新设计流程
思考:为什么一定要客户端主动刷新,而不能服务端主动延时呢?
如果由服务端进行Token自动延时,则Token不会主动更换,除非客户端主动发起刷新操作。一旦Token泄露,则可以无限制调用后端接口,因为Token始终不变,并且持续有效。Token刷新设计的特点是必须定期更换,以保证更高的安全性。更换Token的方式有两种。
第一种是重新获取,那就必须提供客户端身份信息,同时提供用户信息(用户名和密码),除非所有信息均丢失,才会引发安全性问题,但是与平台无关,属于用户保存不当。第二种是使用刷新码进行刷新,这种获取方式必须提供客户端身份信息、原Token和刷新码,三者全部丢失才会引发安全性问题。