当前位置: 首页 > Web前端 > HTML

权限控制在数据栈产品中的实践

时间:2023-03-29 11:52:23 HTML

我们是袋鼠云数据栈UED团队,致力于打造优秀的一站式数据中心产品。我们始终保持工匠精神,探索前端道路,为社区积累和传播经验价值。前言访问控制(Accesscontrol)是指访问者对受保护资源的访问操作的控制和管理。控制管理确保授权的人可以访问受保护的资源,而未授权的人不能访问受保护的资源。现实生活中的访问控制可以通过支付或者认证来实现。例如:去电影院看电影,需要买电影票,否则验票员是不会让你进去的。门禁控制的模型有很多,比如:全权访问控制模型(DiscretionaryAccessControl)强制访问控制模型(MAC:MandatoryAccessControl)角色访问控制模型(RBAC:Role-basedAccessControl)属性访问控制模型(ABAC:Attribute-BasedAccessControl)DAC自主访问控制(DAC:DiscretionaryAccessControl),系统会识别用户,然后根据访问对象的访问控制列表(ACL:AccessControlList)或访问控制矩阵(ACL:AccessControlMatrix)信息来确定用户可以对其进行哪些操作,比如读或调整。拥有对象权限的用户可以将对象权限分配给其他用户,因此称为“全权委托”控制。自主访问控制模型是一种相对宽松但有效的保护资源不被非法访问和使用的手段。说松是因为它是自主控制的,在保护资源的时候是以个人意志为转移的;之所以说有效,是因为它能清楚明确地指出主体是在访问还是在使用客体。实现什么样的权限,任何超出指定权限的访问行为,经过访问控制列表的判断,都会被阻止。一个比较典型的场景是在Linux文件系统中:系统中的每一个文件(有些特殊文件可能没有,比如块设备文件等)都有一个所有者。文件的所有者是创建该文件的计算机的用户(或事件,或另一个文件)。那么这个文件的自主访问控制权限就由它的创建者如何设置和分配来决定了。文件的拥有者拥有访问权限,可以为自己和其他用户分配访问权限只有那些用户(或其他主体)和文件(或其他对象)在标记的访问控制下被标记为固定的安全属性(如安全级别、访问权限等)。每次访问发生时,系统检查安全属性以确定用户是否具有访问该文件的权限。MAC最早主要用于军事应用,通常与DAC结合使用。将两种访问控制机制的过滤结果累加起来,以达到更好的访问控制效果。也就是说,只有通过了DAC限制检查和MAC限制检查双重过滤装置,主体才能真正访问客体。一方面,用户可以使用DAC来防止其他用户攻击那些属于自己的对象;另一方面,由于用户不能直接更改MAC属性,MAC提供了一个不可逾越的更强大的安全保护层,以防止其他用户无意或有意地滥用DAC。RBAC角色访问控制(RBAC:Role-basedAccessControl),各种权限不是直接授予具体的用户,而是在用户集和权限集之间建立一个角色集。每个角色对应一组相应的权限。一旦为用户分配了合适的角色,该用户就拥有该角色的所有操作权限。目前基于角色的访问控制模型被广泛应用,尤其是在2B方向的SAAS领域。这就是我们今天要关注的。RBAC虽然简化了权限的管理,但是对于复杂场景下的角色管理仍然不够灵活。例如,主体与客体之间的权限复杂多变,可能需要维护大量的角色及其授权关系;新对象还需要处理所有相关角色。基于属性的角色访问控制就是为了解决这个问题而设计的。ABAC属性访问控制(Attributes-basedAccessControl)是一种非常灵活的访问控制模型。属性包括请求体的属性、请求对象的属性、请求上下文的属性、操作的属性等。比如老张是班主任(主体的属性),上课的时候可以踢(操作的属性)普通学生的小明(客体的属性)(上下文的属性)。可见,只要对属性进行精确的定义和划分,ABAC就可以实现非常复杂的权限控制。例如:大二(年级)会计(专业)二班(班)班长(职务)可以在校内网(环境)上传(操作)班级照片。但是由于ABAC的复杂性,对于现在的SAAS领域来说有点大材小用。因此,在SAAS领域很少见到使用ABAC的平台。目前一些云服务使用ABAC较多。数据栈中的RBAC我们产品使用的是RBAC权限方案,所以目前只分析RBAC。RBAC是角色访问控制,所以我们首先要知道的是用户的角色。对此,我们的项目中有用户管理和角色管理两个模块。用户管理在登录入口的用户管理中提供创建、编辑、删除用户账号等功能。在Datastack产品中,有租户的概念,每个租户都有自己的用户管理来管理租户内的用户。能够设置当前用户的角色,包括租户所有者、项目所有者、项目经理等。角色管理在角色管理中,您可以看到角色的定义及其拥有的访问权限。通过用户管理和角色管理中的用户定义,我们可以获得当前用户完整的产品访问权限。当用户进入某个功能时,我们可以通过对比当前的访问权限和用户的访问权限,进而得出准入的结论。对于我们前端开发人员来说,我们需要的其实是用户的特定角色权限,通过用户的特定角色权限来验证权限。我们来看看antdesignpro的权限方案是怎么处理的。antdesignpro中的权限方案,业界比较常见的antdesignpro中的权限方案是如何设计的?在获取用户角色权限之初,进入页面时会进行登录验证。如果没有登录,会跳转到登录页面,进行登录操作。登录成功后,会通过setAuthority方法将当前用户的角色数据保存到localStorage中,方便我们重新进入页面时获取。通过登录验证的,直接进入项目渲染页面基本布局的BasicLayout组件。在BasicLayout组件中,我们使用Authorized组件。Authorized挂载时,会触发renderAuthorize给CURRENT赋值。后面的权限验证会用到CURRENT,这个比较关键。下面是这两种情况下方法调用的流程图:renderAuthorize方法是柯里化函数,在内部使用getAuthority获取角色数据时,为CURRENT赋值。让当前:字符串|string[]='NULL';类型CurrentAuthorityType=string|字符串[]|(()=>typeofCURRENT);/***使用权限或getAuthority*@param{string|()=>String}currentAuthority*/constrenderAuthorize=(Authorized:any)=>(currentAuthority:CurrentAuthorityType)=>{if(currentAuthority){if(typeofcurrentAuthority==='function'){CURRENT=currentAuthority();}if(Object.prototype.toString.call(currentAuthority)==='[objectString]'||Array.isArray(currentAuthority)){CURRENT=currentAuthorityasstring[];}}else{CURRENT='NULL';}returnAuthorized;};export{CURRENT};exportdefault(Authorized:any)=>renderAuthorize(Authorized);至此,项目的权限获取和更新完成。下一步是验证权限。权限验证需要以下环境参数:authority:当前访问权限为访问权限currentAuthority:当前用户的角色,即CURRENTtarget:验证成功后显示的组件Exception:验证失败的组件verified与Authorizedcomponents结合用于需要权限验证的组件。Authorized组件内部,实现了checkPermissions方法,验证当前用户角色是否有访问权限。如果有权限,则直接显示当前组件,如果没有,则显示无权限等信息。授权组件的实际情况,typeIAuthorizedType=React.FunctionComponent&{Secured:typeofSecured;检查:检查类型;AuthorizedRoute:typeofAuthorizedRoute;};constAuthorized:React.FunctionComponent=({children,authority,noMatch=(),})=>{constchildrenRender:React.ReactNode=typeofchildren==='undefined'?空:儿童;constdom=check(authority,childrenRender,noMatch);return<>{dom};};functioncheck(权限:IAuthorityType,目标:T,异常:K):T|K|React.ReactNode{returncheckPermissions(authority,CURRENT,target,Exception);}/***通用权限检查方法*常用权限检查方法*@param{权限判断|权限判断}authority*@param{你的权限|您的许可cription}currentAuthority*@param{通过的组件|传递组件}target*@param{未通过的组件|没有传递组件}Exception*/constcheckPermissions=(authority:IAuthorityType,currentAuthority:string|string[],target:T,Exception:K,):T|K|React.ReactNode=>{//没有判断权限。默认查看所有//退出权限,返回目标;如果(!authority){返回目标;}//数据处理if(Array.isArray(authority)){if(Array.isArray(currentAuthority)){if(currentAuthority.some((item)=>authority.includes(item))){returntarget;}}elseif(authority.includes(currentAuthority)){返回目标;}返回异常;}//string处理if(typeofauthority==='string'){if(Array.isArray(currentAuthority)){if(currentAuthority.some((item)=>authority===item)){returntarget;}}elseif(authority===currentAuthority){返回目标;}返回异常}//Promise处理if(authorityinstanceofPromise){returnok={target}error={Exception}promise={authority}/>;}//函数处理if(typeofauthority==='function'){constbool=authority(currentAuthority);//函数执行后的返回值为Promiseif(boolinstanceofPromise){returnok={target}error={Exception}promise={bool}/>;}if(bool){返回目标;}返回异常;}thrownewError('不支持的参数');};在页面上使用Authorized组件来控制需要的权限非常方便可以使用Authorized组件函数NoMatch=()=>{return

404
}{children}我们也可以使用路由来匹配组件。}/>}>(Component?:render(props))}/>我们的权限方案Oldpermissionscheme在旧方案中,通过接口请求后台维护的权限数据,这部分权限数据只维护菜单层级。将请求的数据存储在缓存中,以供后续使用。在我们内部的业务工具包中,我们监听页面地址的变化,根据缓存的数据判断是否有权限进入当前页面,并根据结果进行相应的处理。其实就是一个路由守卫的作用。子产品中根据缓存数据判断是否显示当前菜单项。这两者的结合形成了我们的旧方案。随着数据栈的增长,老方案逐渐暴露出很多问题。权限控制范围太小,我们只控制菜单的层级,对于特殊页面和某些需要控制功能的场景(如:编辑、添加、删除等),目前只控制后台界面有限制,页面没有限制。如果需要实现这个功能,需要增加额外的接口和处理逻辑。我们把权限的处理分为两个部分,业务工具包和子产品,但是两者之间的耦合度很高。是的,如果一个地方改变了,另一个地方也需要相应地改变。在我们的研发过程中,每当我们需要添加菜单时,我们都需要添加相应的菜单处理逻辑。当我们添加一个商品时,我们需要添加这个商品对应的所有菜单逻辑。目前stack中的子产品数量已经超过10+。可以想象这部分处理逻辑有多臃肿。……实际问题不止以上三点,但这三点足以让我们去探索新的权限方案。新的权限方案在新的方案中,业务工具包只保留了权限的公共方法,页面权限判断的逻辑是去中心化的。子产品维护自己的权限判断逻辑,变更后很容易修改一个权限的逻辑。流程如下:与antdesignpro中的角色判断相比,在新的方案中,我们将角色权限的判断逻辑交给了后端,后端经过相应的处理后返回相应的代码集。对于每个需要设置访问权限的模块,我们定义一个代码code来比较在后端返回的集合中是否可以找到相同的代码。如果能找到描述,则有访问当前模块的权限,否则没有。这样做之后,我们只需要关心是否可以进入即可。当获取到权限点时,也会根据权限点缓存有权限访问的路由列表。当路由发生变化时,可以在权威路由列表中查找。如果没有找到,它会重定向等等。运行,也就是路由守卫的功能。总结经过上面的介绍,我们已经了解了权限方案,主要分为两个阶段:获取权限阶段:在获取权限阶段,往往是在用户登录或者进入项目时,第一时间获取相应的用户信息。权限验证权限阶段:将用户的权限与当前模块的访问权限进行比较。根据结果??操作知道这些后,就可以根据自己的场景制定相应的权限方案了。