写点什么

企业级 Web 应用系统权限设计

作者:得大自在
  • 2022 年 6 月 16 日
  • 本文字数:5420 字

    阅读完需:约 18 分钟

企业级Web应用系统权限设计

前言


权限体系是一个涉及用户数据的业务系统的核心部分。但见过的很多系统的权限体系设计都不够灵活,导致扩展和维护困难。原因在于设计者在系统建模时对一些概念的理解不够深入清晰,对于不同的概念之间的关联没有厘清,导致设计出来的系统可用,但在更复杂灵活的业务实现过程中产生各种各样的矛盾,导致扩展困难。


本文尝试对企业级应用系统权限体系的设计做一个系统性的阐述,建立一个最合理地权限设计的理论框架,并针对落地过程中的功能设计以及技术实现中的关键点做一些阐述。

概念与术语解释


企业级 web 应用中的权限体系中包含以下的关键概念:


  1. 资源:系统中参与业务过程实现的主体,这是一个很宽泛的描述,系统中所有用名词表示的部分都可以定义为资源。例如:流程、菜单、接口、文章、表格....。

  2. 权限:顾名思义,是指对系统资源的操作权利和操作限制。权利指可以做什么,限制指不可以做什么。一般用一个动词描述,例如;查看、编辑、下载等。一个系统可以没有权限要求,例如一些公开访问的资讯类网站。只有涉及具体的个人数据访问时,才有设计权限的必要。

  3. 用户:系统的使用者,一般是人,但也可能是其它系统。一个系统如果没有权限要求,也可以没有用户(或者说,匿名)体系。

  4. 角色:角色是权限的集合。

  5. 组织:用户的集合。组织一般是和特定的业务场景关联的,用于控制数据访问权限。一个系统可以没有组织。

  6. 岗位:某项业务工作的职责分工设定。岗位是存在于组织当中的概念,和系统权限不在同一个领域。系统建模中一般把岗位映射成系统的角色。

  7. 功能权限:对某个资源或者资源集合的操作限制。如果是对某个集合,那么默认是集合中的所有个体。

  8. 数据权限:在功能权限的基础之上,对某个资源或和资源集合中的部分数据赋予的访问权限。


概念之间的关联关系:


  1. 用户与角色:多对多,1 个用户可以拥有多个角色,一个角色可以被赋予多个用户。

  2. 用户与组织:多对多,1 个用户可以属于多个组织,一个组织包含多个用户。组织与角色:没有对应关系。

  3. 岗位与角色:一对多,一般把组织中的岗位映射成系统的角色(岗位 --> 角色 单向映射)。一个岗位可以映射成多个角色。

  4. 组织与岗位:1 对多,每个组织部门包含多个岗位。

系统权限初始化


系统初始化时,我们应该怎样初始化权限?或者最小需要初始化什么权限?


很多系统初始化时都会建立一个叫“admin”的上帝用户,这个用户拥有超级管理员的角色,超级管理员拥有所有系统权限,可以做任何事,包括系统管理,发起业务流程等。这样的初始化设定是非常危险的!万一这个账号被泄露了可以任意破坏系统。


这样的设定也导致系统到处都是类似这样的硬编码:

if ("admin".equals(currentUser) || currentUser.hasRole("superadmin")) {  // 放开权限}
复制代码


试想一下,如果一个系统后面还会不断的添加功能,这样的硬编码逻辑对于系统的可维护性和可扩展性将是一个噩梦。

诚然系统必须要有一个初始化的用户,但这个用户不应该是上帝,只能拥有最必要的权限集合。那这个最小的权限集合是什么呢?


其实就是权限体系中最必要的几个点:


  1. 管理用户的权限

  2. 管理角色的权限

  3. 授权的权限:包括给角色分配权限,给用户分配角色。


除此之外,这个用户不再拥有其它任何权限,更不能干涉具体的业务。这体现了一种分权的思想,授权的人不能干涉业务。这样的设计,即使这个用户的账号被泄露,破坏者最多只能破坏系统用户的权限,而不能破坏任何的业务的数据。


这样的设定实际上上基于分权的思想,没有人能拥有所有权限。每个人都是一个局部,合在一起才是整体。


当我们创建一个系统,我们相当于创造了一个宇宙,我们写代码设定这个宇宙中有什么资源,对资源的访问需要什么权限,这在系统上线时就应该是确定了的,系统在运行过程中无法改变,除非重现上线新的版本进行修改。例如:一个业务实现方法,需要什么样的权限才能调用,应该是硬编码在系统中的,不允许运行时改变。


我见过一些系统的实现,可以让管理员在运行时修改对某些资源(例如:后端的某个接口方法)的访问权限,这是相当危险的,因为很有可能或由于不小心的授权,而修改了系统的行为,从而导致系统的不稳定。

资源权限控制实现


一个系统有各种各样的资源,包括:


  1. 用户、角色、菜单

  2. 流程、待办、作业等

  3. 股票、债券、报表等

  4. .....


由于企业级 Web 应用系统的物理实现,往往是由前端、后端、数据库三部分代码组成,一个资源在系统的呈现,往往有 UI 界面,有后端业务逻辑方法,也有数据库的表,因此对一个资源的访问权限控制,也往往需要在这几个层面进行控制。举个例子,对于用户资源,有前端的用户管理界面,有后端的用户管理方法,有用户对应的数据库表。


实际操作过程中,对于数据库表的访问一般不做控制,后端统一使用一个数据库账户访问所有的表,主要的访问控制点在后端方法的访问上。因此一个的权限控制点在于前端和后端。系统资源的权限控制点应该是硬编码在系统中的,无法在运行时篡改,因此可以确保系统按照预设的逻辑运行。

资源权限码


为了实现权限的控制,我们要给每一种资源的操作进行编码, 编码格式为“资源:操作”。例如添加用户,编码为 user:add。很多系统把权限码的配置放在菜单管理中,新建一个按钮时,让用户直接填写对应的权限码。这样的做法很容易出错。


权限码并不是依附于菜单和按钮的,而是系统本身的资源属性,系统和菜单只是使用者。最好的做法是:建立一个资源权限码的统一管理界面。在新建菜单按钮时,直接引用这个权限码。这样表达的含义也很清楚:我将使用某个资源的权限码来控制这个按钮的显示。


一个资源的某个操作逻辑的前端控制点、后端控制点都应选择同一个权限码。

前端访问控制点


首先前端的权限控制,并不是真正意义上有效的权限控制,因为恶意的用户完全可以绕过前端的界面,伪造请求直接提交给后端。前端的权限控制,更多是为了用户交互的友好性。即一个用户只能看到他有权限访问的界面,避免出现用户能看到一个页面,但是提交后端请求报错的情况。


前端的 UI 访问控制点,一般分为:菜单和按钮。菜单是对资源前端 UI 的访问路径,按钮是对资源的前端操作点。按钮包含在菜单对应的页面中,因此可以用树形结构组织菜单和按钮的权限控制点。很对系统在新建菜单时,可以在菜单下新建按钮。

后端访问控制点


系统权限控制的核心关键点在于后端。按照经典的 Web 应用后端分层设计,后端服务在逻辑上分为三层:


  1. 访问控制层(Controller Layer)

  2. 业务逻辑层(Service Layer):Service 层是核心的业务逻辑

  3. 数据库访问层(Dao Layer):数据库


这三层代码的权限控制逻辑如下:


  1. 访问控制层的权限控制点:因为访问控制层的作用是连接前端和业务逻辑服务层,因此每一个控制层的方法都应该加上权限控制点控制,业务上设定匿名访问的除外。其权限控制点和前端的按钮控制点应该一一对应。例如用户管理界面有一个添加用户的按钮,其在前端的权限控制点为 user:add,那么后端的 controller 方法的权限控制点也是 user:add。


伪代码如下:

前端:<button auth="user:add">添加用户</button>
后端:
public class UserAdminController { @PreAuthorize("hasAuthority('user:add')") public User addUser(User) { .... }}
复制代码


  1. 业务逻辑层的权限控制点:业务逻辑层是实现系统核心业务逻辑的地方,一般也叫业务领域层。这一层的公共方法(public,protected)都应该加上权限点控制。我见过很多系统,只在 controller 方法上加权限控制,后端 service 方式全部开放的,这很容易引入 bug,例如一个程序员不小心在另外一个权限很低的 controller 方法中,调用了需要很高权限才能访问的 service 方法。以上面的例子举例,后端 service 方法应该这样加上权限控制点:

public class UserService {
@PreAuthorize("hasAuthority('user:add')") public User addUser(User user) { .... }
}
复制代码


  1. 数据库访问层的权限控制:因为数据库表的访问不进行控制,因此这一层的代码不需要加权限控制点,依赖于业务逻辑层的控制。

hasRole 和 hasAuthority


在 Spring 的 security 框架中,提供了 hasRole(...) 和 hasAuthority(...) 两种编码方式用于控制对一个方法的访问。但我们不推荐使用 hasRole(...)的方式,为什么?


按照定义,角色是权限的集合,最小粒度的控制点,应该是权限,而不是角色。在自然语言的表达中,我们经常这样说:“如果用户是系统管理员,那么可以创建用户”,如果我们按照这样的表达来写代码,那么将是非常不灵活的:

@PreAuthorize("hasRole('ROLE_admin')")public User addUser(User user) { .....}
复制代码

这段代码可以工作,但一旦面临这样的需求:我想创建一个用户管理专员的角色,这个用户管理员也能创建用户。那就只能修改代码了。


在系统建模时应该把上面的话翻译成:如果用户有系统管理员的角色,那么他有创建用户的权限,因此他可以创建用户。


上述代码的正确写法应该是:

@PreAuthorize("hasAthority('user:add')")public User addUser(User user) { .....}
复制代码

记住, 角色只是为了授权方便,方便一次性授予用户一个权限的集合,在编码进行控制权限时不应该使用角色,应该直接使用权限编码。

数据权限控制


数据权限其实是在功能权限满足的基础上,对于用户能看到的数据子集访问的控制。例如:一个用户具有查看用户列表的功能权限,但他只能看到自己所在部门的用户(一个子集)。


因此数据权限的控制都是在取数据的过程中加上过滤条件,和具体的实现方式有关。如果依赖于 sql 取数据,那么就在 sql 中加入过滤条件,或者从数据库取出数据后在后端对结果进行过滤。


数据如何进行过滤是和具体的业务关联的,对于数据权限的授权没有统一的方式,针对系统的每一类的数据,都应该开发特定的数据授权页面。有时候以资源为中心授权方便,有时候以角色为中心授权方便,看具体的业务场景。

其它

给个人授权


经常听到这样的需求:我们要开发一个授权界面,给某个用户授权,不通过角色。


理论上这样做是可以的,但却没有必要。因为完全可以建立一个专有的角色,先把权限赋予到角色上,然后把角色赋予人。试想要给 10000 个人赋予权限,对于管理员不仅工作量大,而且容易出错。使用角色会让事情变得简单一些。给个人授权,就是赋予角色


另外系统业务的运行不应该绑定具体的用户,否则一旦这个人由于各种原因不处理业务了,系统中的业务处理就中止了,没有其它人可以替代,只能找开发人员直接修改数据库了。因此为了保障系统业务的连续性,权限的控制必须基于角色,任何人有这个角色,就可以处理业务。


但有一种例外情况,就是这个业务产生的数据本身就是私有的,别人不能干涉。例如某个用户在一个博客上写了有一篇文章的草稿,那任何人都不能替他进行发布。


一句话:公共的数据及其操作应该绑定角色,只有私有的数据及其操作才绑定个人。

岗位和角色


很多企业即的业务系统中都存在组织相关的数据权限控制。因为组织结构是每个企业都具有的,自然而然很多权限的设置就以组织结构的划分来设定。举个例子:某企业有 A、B 两个部门,张三和李四分别是两个部门的总经理。张三只可以审批 A 部门的订单数据,李四只可以查看 B 部门的订单数据。


那该怎么进行系统建模呢?前面说过,组织结构是业务领域的概念,一个系统可以没有组织结构,系统的权限体系包含的概念是:用户、角色和权限。因此最好的做法是把岗位映射成角色。以上例子上的部门总经理岗,可以映射成很多个系统的角色。岗位和角色的映射关系是 1:N。


映射关系:


张三 -- A 部门 -- 部门总经理 (岗位)-- A 部门流程审批(角色)、A 部门订单管理(角色)...

李四 -- B 部门 -- 部门总经理(岗位)-- B 部门流程审批(角色)、B 部门订单管理(角色)...


把岗位映射成系统角色之后,就可以实现很多灵活的功能,例如:让张三也可以看到 B 部门的订单数据,只需要把 B 部门订单管理(角色)赋予张三就够了,张三不需要调换部门。


所见即所得授权?


授权的所见即所得,指的是管理员在授权界面给用户分配了哪些菜单和按钮,那么在用户访问系统时,用户在前端界面就只能看到授予权限的菜单和按钮。但有部分的后端接口是没有前端界面对应的,例如一些领域层的接口,就无法使用“所见即所得”的方式,在配置菜单、按钮权限的时候进行分配。


可见所见即所得的授权方式,不是一种合理的方式。因为前端的控制只是一个控制点,无法覆盖后端的控制点。最好的方式,是直接针对资源进行授权。例如:赋予 A 角色创建创建用户的权限。表示为:(A 角色)拥有 (user:create 权限),那么所有的菜单、按钮、后端的接口方法,都使用 user:create 这个权限码进行控制。这样设计的话,也就不用开发专门的菜单授权界面了,因为菜单和按钮会自动根据权限码决定是否隐藏和展示。

授权代理链路

首先任何一个系统用户都无法修改自己的权限,包括超级管理员也不能给自己增加或者减少权限。


另外对于大型的系统,超级管理员可以把自己的权限授予其它管理员用户,以便其他人能代理自己进行授权。例如,超级管理员可以创建一个角色专门管理系统菜单,另一个角色专门管理用户。这些新创建的角色可以继续把权限赋予更多的角色。这样就形成了一条授权链:

(超级管理员) --->(A)---> (B)--> (C)


当然上级管理员可以对下级管理员的数据权限做限定,例如 A 可以制定 B 只管理某个部门的用户。


为了保证系统的安全,上级管理员可以修改和回收下级管理员的所有权限,下级无法修改平级和上级的权限。而且上级管理员只能授予下级管理员自己拥有的权限。

结束


本文对企业级 Web 应用系统的权限设计提出了一个完善的理论框架,以及对于某些关键部分的实现思路。希望对有缘看到的读者们有用。

发布于: 刚刚阅读数: 4
用户头像

得大自在

关注

还未添加个人签名 2008.10.08 加入

还未添加个人简介

评论

发布
暂无评论
企业级Web应用系统权限设计_权限_得大自在_InfoQ写作社区