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

权限,控制,产品,实践 · 浏览次数 : 381

小编点评

**权限方案** **获取权限阶段** * 用户登入或进入项目时,第一时间根据用户信息获取相 corresponding权限校验权限 * 通过用户的权限,与当前模块的准入权限进行比对 **权限阶段** * 通过用户的权限,与当前模块的准入权限进行比对 * 在根据结果进行操作知道了这些之后,就可以结合自身的场景,制定出相应的权限方案

正文

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

前言

访问控制(Access control)是指对访问者向受保护资源进行访问操作的控制管理。该控制管理保证被授权者可访问受保护资源,未被授权者不能访问受保护资源。

现实生活中的访问控制可以由付费或者认证达成。例如:进电影院看电影,需要够买电影票,否则检票员就不让你进去。

访问控制有很多模型,比如:

  • 自主访问控制模型 (Discretionary Access Control)
  • 强制访问控制模型 (MAC: Mandatory Access Control)
  • 角色访问控制模型 (RBAC: Role-based Access Control)
  • 属性访问控制模型 (ABAC: Attribute-Based Access Control)

DAC

自主访问控制(DAC: Discretionary Access Control),系统会识别用户,然后根据访问对象的权限控制列表(ACL: Access Control List)或者权限控制矩阵(ACL: Access Control Matrix)的信息来决定用户是否能对其进行哪些操作,例如读取或修改。而拥有对象权限的用户,又可以将该对象的权限分配给其他用户,所以称之为“自主(Discretionary)”控制。

自主访问控制模型是一种相对比较宽松但是却很有效的保护资源不被非法访问和使用的手段。说它宽松,是因为他是自主控制的,在保护资源的时候是以个人意志为转移的;说它有效,是因为可以明确的显式的指出主体在访问或使用某个客体时究竟是以何种权限来实施的,任何超越规定权限的访问行为都会被访问控制列表判定后而被阻止。

比较典型的场景是在 Linux 的文件系统中:
系统中的每个文件(一些特殊文件可能没有,如块设备文件等)都有所有者。文件的所有者是创建这个文件的计算机的使用者(或事件,或另一个文件)。那么此文件的自主访问控制权限由它的创建者来决定如何设置和分配。文件的所有者拥有访问权限,并且可以将访问权限分配给自己及其他用户

MAC

强制访问控制(MAC: Mandatory Access Control),用于将系统中的信息分密级和类进行管理,以保证每个用户只能访问到那些被标 制访问控制下,用户(或其他主体)与文件(或其他客体)都被标记了固定的安全属性(如安全级、访问权限等),在每次访问发生时,系统检测安全属性以便确定一个用户是否有权访问该文件。

MAC 最早主要用于军方的应用中,通常与 DAC 结合使用,两种访问控制机制的过滤结果将累积,以此来达到更佳的访问控制效果。也就是说,一个主体只有通过了 DAC 限制检查与 MAC 限制检查的双重过滤装置之后,才能真正访问某个客体。一方面,用户可以利用 DAC 来防范其它用户对那些所有权归属于自己的客体的攻击;另一方面,由于用户不能直接改变 MAC 属性,所以 MAC 提供了一个不可逾越的、更强的安全保护层以防止其它用户偶然或故意地滥用 DAC。

RBAC

角色访问控制 (RBAC: Role-based Access Control),各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。 每一种角色对应一组相应的权限。 一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限目前来说基于角色的访问控制模型是应用较广的一个,特别是 2B 方向 SAAS 领域,应用尤其常见,角色访问也就是我们今天要介绍的重点。

RBAC 虽然简化了权限的管理,但是对于复杂场景的角色管理,它依然不够灵活。比如主体和客体之间的权限复杂多变,可能就需要维护大量的角色及其授权关系;新增客体也需要对所有相关角色进行处理。基于属性的角色访问控制就是为了解决这个问题。

ABAC

属性访问控制(Attributes-based Access Control)是一种非常灵活的访问控制模型。属性包括请求主体的属性、请求客体的属性、请求上下文的属性、操作的属性等。如身为班主任(主体的属性)的老张在上课(上下文的属性)时可以踢(操作属性)身为普通学生(客体的属性)的小明一脚。可以看到,只要对属性进行精确定义及划分,ABAC可以实现非常复杂的权限控制。

比如:大二(年级)计科(专业)二班(班级)的班干(职位)可以在学校内网(环境)上传(操作)班级的照片。

但是由于 ABAC 比较复杂,对于目前的 SAAS 领域,就显得有点大材小用了,所以在 SAAS 领域很少见到有使用ABAC 的平台,目前使用 ABAC 比较多的就是一些云服务。

数栈中的 RBAC

我们产品中采用的是 RBAC 的权限方案,所以我们目前只对 RBAC 进行分析。

RBAC 是角色访问控制,那么首先我们需要知道的是用户的角色,在这个方面,我们项目中存在了用户管理以及角色管理两个模块。

用户管理

在登陆门户的用户管理中提供用户账户的创建、编辑和删除等功能。

file

在数栈的产品中,存在租户的概念,每个租户下都有一个自己的用户管理,对租户内的用户进行管理。能够设置当前用户的角色,这些角色包括租户所有者、项目所有者和项目管理者等。

file

角色管理

在角色管理中可以看到角色的定义,以及它所拥有的访问权限。

file

我们通过在用户管理和角色管理中的用户定义,可以得到当前用户完整的产品访问权限,当用户进入某个功能时,我们就可以通过当前的准入权限以及用户的访问权限,进行比较,进而得出是否准入的结论。

对于我们前端开发者而言,我们需要的其实就是

  1. 用户具体的角色权限
  2. 通过用户具体的角色权限, 对权限进行校验

那我们来看看 ant design pro 的权限方案是如何处理的。

ant design pro 中的权限方案

业界比较通用的 ant design pro 中的权限方案是如何设计的呢?

获取用户角色权限

一开始在进入页面的同时,会进行登陆校验。如果未登录会跳转到登录页面,进行登陆操作,登陆成功后,会把当前用户的角色数据通过 setAuthority 方法存进 localStorage 中,方便我们重新进入页面时获取。

而对于已经登录校验通过的,会直接进入项目中,进行渲染页面基础布局 BasicLayout 组件,在 BasicLayout 组件中我们使用到了Authorized组件,在挂载Authorized的时候,触发renderAuthorizeCURRENT进行赋值。后续的权限校验都会使用CURRENT,比较关键。

下面是这两种情况的方法调用流程图:

file

renderAuthorize 方法是一个柯里化函数,在内部使用getAuthority获取到角色数据时对 CURRENT
进行赋值。

let CURRENT: string | string[] = 'NULL';

type CurrentAuthorityType = string | string[] | (() => typeof CURRENT);
/**
 * use  authority or getAuthority
 * @param {string|()=>String} currentAuthority
 */
const renderAuthorize = (Authorized: any) => (currentAuthority: CurrentAuthorityType) => {
  if (currentAuthority) {
    if (typeof currentAuthority === 'function') {
      CURRENT = currentAuthority();
    }
    if (
      Object.prototype.toString.call(currentAuthority) === '[object String]' ||
      Array.isArray(currentAuthority)
    ) {
      CURRENT = currentAuthority as string[];
    }
  } else {
    CURRENT = 'NULL';
  }
  return Authorized;
};

export { CURRENT };
export default (Authorized: any) => renderAuthorize(Authorized);

到这,项目的权限获取以及更新就完成了。接下来就是权限的校验了

校验权限

对于权限校验,需要以下环境参数:

  1. authority:当前访问权限也就是准入权限
  2. currentAuthority:当前用户的角色,也就是 CURRENT
  3. target:校验成功展示的组件
  4. Exception:校验失败展示的组件

对于需要进行权限校验的组件,使用Authorized组件进行组合,在Authorized组件内部,实现了checkPermissions方法,用来校验当前用户角色,是否有权限的进行访问。如果有权限,则直接展示当前的组件,如果没有则展示无权限等消息。

file

Authorized组件的实现,

type IAuthorizedType = React.FunctionComponent<AuthorizedProps> & {
  Secured: typeof Secured;
  check: typeof check;
  AuthorizedRoute: typeof AuthorizedRoute;
};

const Authorized: React.FunctionComponent<AuthorizedProps> = ({
  children,
  authority,
  noMatch = (
    <Result
      status="403"
      title="403"
      subTitle="Sorry, you are not authorized to access this page."
    />
  ),
}) => {
  const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children;
  const dom = check(authority, childrenRender, noMatch);
  return <>{dom}</>;
};

function check<T, K>(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode {
  return checkPermissions<T, K>(authority, CURRENT, target, Exception);
}
/**
 * 通用权限检查方法
 * Common check permissions method
 * @param { 权限判定 | Permission judgment } authority
 * @param { 你的权限 | Your permission description } currentAuthority
 * @param { 通过的组件 | Passing components } target
 * @param { 未通过的组件 | no pass components } Exception
 */
const checkPermissions = <T, K>(
  authority: IAuthorityType,
  currentAuthority: string | string[],
  target: T,
  Exception: K,
): T | K | React.ReactNode => {
  // 没有判定权限.默认查看所有
  // Retirement authority, return target;
  if (!authority) {
    return target;
  }
  // 数组处理
  if (Array.isArray(authority)) {
    if (Array.isArray(currentAuthority)) {
      if (currentAuthority.some((item) => authority.includes(item))) {
        return target;
      }
    } else if (authority.includes(currentAuthority)) {
      return target;
    }
    return Exception;
  }
  // string 处理
  if (typeof authority === 'string') {
    if (Array.isArray(currentAuthority)) {
      if (currentAuthority.some((item) => authority === item)) {
        return target;
      }
    } else if (authority === currentAuthority) {
      return target;
    }
    return Exception;
  }
  // Promise 处理
  if (authority instanceof Promise) {
    return <PromiseRender<T, K> ok={target} error={Exception} promise={authority} />;
  }
  // Function 处理
  if (typeof authority === 'function') {
    const bool = authority(currentAuthority);
    // 函数执行后返回值是 Promise
    if (bool instanceof Promise) {
      return <PromiseRender<T, K> ok={target} error={Exception} promise={bool} />;
    }
    if (bool) {
      return target;
    }
    return Exception;
  }
  throw new Error('unsupported parameters');
};

使用 Authorized 组件

在页面上使用则非常的方便,对需要进行权限管控的组件,使用 Authorized组件进行组合即可。

function NoMatch = () => {
	return <div>404</div>
}

<Authorized authority={'admin'} noMatch={NoMatch}>
  {children}
</Authorized>

我们还可以利用路由进行组件的匹配。

<Authorized
    authority={authority}
    noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}
  >
    <Route
      {...rest}
      render={(props: any) => (Component ? <Component {...props} /> : render(props))}
    />
</Authorized>

我们的权限方案

旧权限方案

在旧方案中,通过接口请求后端维护的权限数据,这部分权限数据只维护了菜单这一级别。将请求到的数据存入缓存中,便于后续的使用。

在我们内部的业务工具包中监听页面地址的改变,根据缓存的数据判断是否有进入当前页面的权限,根据结果来进行相应的处理,实际就是做了个路由守卫的功能。

而在子产品中,根据缓存的数据来判断是否显示当前的菜单入口。这两者组合,形成了我们旧方案。

随着数栈的成长,旧方案慢慢的也暴露出了许多的问题。

  • 对权限控制的范围太小,我们只控制到了菜单这一级别,而对于特殊页面和某些场景下需要对功能的控制(如:编辑,新增、删除等),目前只有后端接口进行限制,页面上并没有进行限制,如果需要实现这个功能,就需要添加额外的接口和处理逻辑,
  • 我们把权限的处理分成两部分,业务工具包和子产品中,但是两者间的耦合度是非常高的,往往改动了一个地方,另一个也需要跟着更改。
  • 我们在研发过程中,每当需要增加一个菜单,就需要增加一条对应的菜单处理逻辑,增加一个产品,就需要增加这个产品对应的所有菜单逻辑,目前数栈的子产品已经超过了 10+ ,可以想象这部分处理逻辑是有多么的臃肿。
  • ......

实际的问题不止以上列的三点,但是这三点就足够我们进行新的权限方案的探索。

新权限方案

在新方案中,业务工具包只保留权限的公共方法,把页面权限判断的逻辑进行的下放,子产品自己维护自己的权限判断逻辑,修改一条权限的逻辑也非常的容易

更改后的流程如下:

file

相比起 ant design pro 中通过角色进行判断,新方案中我们把角色权限的判断逻辑移交给了后端,后端经过了相应的处理后,返回对应的 code 码集合。

我们为每个需要设置准入权限的模块,定义一个 code 码,去比较后端返回的集合中,是否能够找到相同的 code,如果能找到说明就有访问当前模块的权限,反之则没有。

经过这样处理后,我们只需要关心是否能够进入。

在获取到权限点的时候,还会根据这个权限点,去缓存有权限访问的路由列表,当路由改变时,就可以去有权的路由列表里进行查找,如果没有找到就进行重定向之类的操作,也就是路由守卫的功能。

总结

经过上面的介绍,我们对权限方案已经有所了解,主要分为两个阶段:

  1. 获取权限阶段:在获取权限阶段,往往是用户登入或进入项目时,第一时间根据用户信息获取相对应的权限
  2. 校验权限阶段:通过用户的权限,与当前模块的准入权限进行比对,在根据结果进行操作

知道了这些之后,就可以结合自身的场景,制定出相应的权限方案。

与权限控制在数栈产品的实践相似的内容:

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

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。 前言 访问控制(Access control)是指对访问者向受保护资源进行访问操作的控制管理。该控制管理保证被授权者可访问受保护资源,未被授权者不能访问受保护资源。 现

手把手带你搞定用户权限控制

在实际的软件项目开发过程中,用户权限控制可以说是所有运营系统中必不可少的一个重点功能,根据业务的复杂度,设计的时候可深可浅,但无论怎么变化,设计的思路基本都是围绕着用户、角色、菜单这三个部分展开。 如何设计一套可以精确到按钮级别的用户权限功能呢? 今天通过这篇文章一起来了解一下相关的实现逻辑,不多说

详解Web应用安全系列(4)失效的访问控制

在Web安全中,失效的访问控制(也称为权限控制失效或越权访问)是指用户在不具备相应权限的情况下访问了受限制的资源或执行了不允许的操作。这通常是由于Web应用系统未能建立合理的权限控制机制,或者权限控制机制失效所导致的。 危害 数据泄漏:攻击者可能通过越权访问获取敏感数据,如用户个人信息、财务数据、家

上周热点回顾(9.25-10.1)

热点随笔: · 在小公司编程是一种什么样的体验? (公众号_陶朱公Boy)· 一个混乱千万级软件项目 (烂人)· 《优化接口设计的思路》系列:第四篇—接口的权限控制 (sum墨)· C#开源且免费的Windows桌面快速预览神器 - QuickLook (追逐时光者)· .NET开发工作效率提升利器

《优化接口设计的思路》系列:第四篇—接口的权限控制

前言 大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。 作为一名从业已达六年的老码农,我的工作主要是开发后端Java业务系统,包括各种管理后台和小程序等。在这些项目中,我设计过单/多租户体系系统,对接

Web通用漏洞--文件上传

# Web通用漏洞--文件上传 ## 概述 文件上传安全指的是攻击者通过利用上传实现后门的写入连接后门进行权限控制的安全问题,对于如何确保这类安全问题,一般会从原生态功能中的文件内容,文件后缀,文件类型等方面判断,但是漏洞可能不仅在本身的代码验证逻辑中出现安全问题,也会在语言版本,语言函数,中间件,

C#特性

目录C#特性1. 概括2. 语法定义特性类应用特性获取特性3. 应用场景数据验证序列化和反序列化描述性元数据依赖注入单元测试权限控制AOP(面向切面编程)总结引用 C#特性 1. 概括 C#中的特性是一种用于向代码元素添加元数据的机制。它们允许程序员在代码中添加额外的信息,以影响程序的行为、编译过程

[转帖]数据可视化之redash(支持43种数据源) (转自https://anjia0532.github.io/2019/07/08/redash/)

https://www.cnblogs.com/a00ium/p/13177272.html 人类都是视觉动物,讲究一图胜千言。如果没了可视化,那么你在跟领导汇报工作时,很大程度会鸡同鸭讲。其实 excel2016+已经是一个不错的数据分析及可视化工具了(支持几十种数据源),但是,不方便权限控制,集

每日一题:vue3自定义指令大全(呕心沥血所作,附可运行项目源码)

1.VUE常用指令大全 本项目所有指令均为全局注册,使用时直接在组件中使用即可。 指令目录:src/directives 页面目录:src/views 具体可查看源码 1.1 权限指令 封装一个权限指令,在模板中根据用户权限来控制元素的显示或隐藏。 permission.js import { re

KES数据库实践指南:探索KES数据库的事务隔离级别

本文深入探讨了KES数据库中的并发控制机制和事务隔离级别的重要性及实施方法。我们从并发控制的基本概念出发,详细解释了ACID原则如何通过不同的隔离级别得以实现,以及在串行化与并行执行之间的权衡取舍。通过实际操作和示例,我们展示了不同隔离级别下可能出现的脏读、不可重复读和幻读现象,以及KES数据库是如...