前言

基于角色的权限控制(RBAC)是管理用户对某种资源或操作的权限的通用方法。权限可以明确指定可以访问的资源和操作。基本原理如下:权限将被分配给某个角色,并将该角色分配给某个用户或者是用户组,而不是直接分配给某个用户。

角色与权限捆绑

将权限与单个用户关联起来是一件很复杂的事情,随着更多的用户使用系统,维护用户的权限变得更加困难,且容易出错。权限的错误分配会阻止用户访问所需的系统,甚至是允许非授权用户访问限制区域或是执行危险操作。

在这篇文章中,我会介绍如何对应用开启权限控制。权限控制的模型有许多种,比如RBAC(基于角色的权限控制),DAC(自由访问控制)等。虽然文档中解释的原则可以应用于各种模型,但我选择RBAC作为参考,因为它被广泛接受并且非常直观。

查看用户的活动通常只会产生用户执行的有限数量的操作(如读取数据,提交表单)。深入观察这些用户的行为会发现,这些行为通常一起执行,即执行A操作的用户往往也会执行B操作。比如,读取并更新报告,或者是添加和删除用户。这些都可以与角色绑定,比如编辑或是账户管理员。注意这里的角色并不一定和职称或是组织结构绑定,而是以有意义的方式反映相关的用户操作。当恰当划分好角色并分配给用户时,就可以将权限分配给每个角色,而非用户。管理少量角色的权限是一件相对简单的事情。

如下,是没有角色作为中介的权限与用户图:

猫头鹰的深夜翻译:对于RestAPI简单的基于身份的权限控制-LMLPHP

而如下,则是完全相同的用户和权限集,由角色组织:

猫头鹰的深夜翻译:对于RestAPI简单的基于身份的权限控制-LMLPHP

显而易见,角色使得权限管理更容易了

用户与群组绑定

将用户与群组绑定是一种更好的实践。

在观察用户关于上述角色的行为模式时,我们经常发现用户之间有很多共同之处,比如某一组用户常常行为相似--在共同的资源上执行相同的操作。这允许我们将用户组织到组中,然后将角色分配给少数组,而不是许多用户。比如,会发现一组用户都需要系统管理员权限,因此我们新建一个名叫账户管理员的群组,将用户添加到该组并将该角色分配给该组,而不是每个用户。

实现角色时的注意事项

不要将行为和验证细节耦合

在许多系统中,开发人员通过直接在实现方法上指定权限来限制对特定操作的访问。没错,就在代码上!通常,角色的验证通过注解添加到需要检查的方法上,比如这里提供了一个spring-security的一个范例:

@PreAuthorize("hasRole('Editor')") public void update_order(Order order);

在不同语言和框架中,这种做法非常常见。虽然很容易实现,但遗憾的是,它在所需角色和动作的实现之间产生了不希望的耦合。想象一下有几十个方法都需要添加这样的注解。跟踪每一个角色的有效操作将会变得很艰难,几乎肯定会导致依赖于不准确或过时的文档,或者更糟糕的是 - 分散在您的应用程序中的未知,非托管权限。

从客户的角度来看,这种耦合使得无法修改开发人员事先定义的角色集或者他们的权限,因为更改它意味着每次都必须编译和打包代码!这种用户体验也许不是我们的目标。

如何避免耦合

更好的方式是,首先从要由外部授权机制处理的代码中提取可能的操作列表,然后,我们可以使代码不知道角色或任何其他授权细节,简单地询问当前用户(无论它是否被检索)是否具有执行特定方法所需的权限(无论在何处定义)。

这允许我们使用更加通用的注解,如下所示:

@Secured public void update_order(Order order);

角色和权限的映射(即执行特定操作的权限)现在可以在配置文件中完成,可以由客户轻松定制!
比如,假设有这样的一个roles_config.yml文件:

order_manager:
  - 'create_order'
  - 'view_order'
  - 'delete_order'
  - 'update_order'
order_inspector:
  - 'viewer_order'

由@secured注解的方法回去查询配置文件确定当前用户是否具有执行该操作的权限。这意味着当前用户必须具有order_manager的角色,而这一点也是很容易配置的。但是,授权机制必须知道如何将每个权限与代码中的特定方法相匹配,并且有人必须记录所有可用的方法(即create_order,view_order等)。

关注点分离--外部授权

既然方法实现代码不包含授权细节,整个授权逻辑可以移动到单独的独立模块。通过使用通用标题(例如注解@secure),我们允许修改整个授权机制而不影响应用程序的代码。例如,可以将@secure实现为基于角色的检查,但也可以使用访问控制列表(ACL)。比如,检查当前用户是否列在订单的ACL列表中。另一种解决方案可以是通过询问第三方是否允许用户执行该动作来使用oauth。

Rest是最佳选择

提取操作--举手之劳

REST接口肯定更好,或者至少是最容易匹配这个模型的。设计良好的Rest服务通过标准的基于HTTP的API暴露资源和方法,资源通过URI定义,方法通过HTTP动词(如GET,PUT)等定义。

比如,POST http://www.domain.com/bookings会创建一本新书,而GET http://www.domain.com/orders/12345会返回订单#12345的详情。这意味着可以轻而易举的获得资源的名称和对资源的操作。

请求网关

除了标准的建模操作之外,REST服务通常是请求流中评估身份验证和授权的好地方,因为这通常是系统的主要入口点。为了使访问控制机制有意义,建议阻止所有其他到系统的路由,例如直接访问数据存储或代码中的任何远程调用机制。该架构的另一个重要优点是响应过滤,以防某些不应当返回给用户的数据写在响应中。

请求也是访问控制工具

REST服务处理传入请求,这意味着请求中找到的信息可用于制定访问控制决策。一些有用的细节是:

  • 请求源:允许阻止来自不明IP或是网段的请求
  • 请求头:许多有意义的细节可以在请求头传递,比如用户凭证,从而支持全面的认证/授权过程。
  • 目标终端:如请求的URI所示。根据其他条件,访问可以仅限于应用程序端点的子集。例如,虽然version端点对所有人开放,但secret端点仅对经过身份验证的用户开放。
  • 目标方法:由HTTP动词(例如DELETE)表示,这意味着可以基于被调用的方法传递或阻止请求。

总而言之:用REST来实现权限控制

所有的资源将会通过REST的URI表示,操作通过HTTP动词表示,这能够覆盖所有能被执行且需要验证的操作。在下面的例子中,定义了三个角色:

  • order_manager:能够查看,创建,更新和删除订单
  • order_editor:能够查看,创建,更新订单,但不能删除他们
  • order_inspector:只能查看订单
order_manager:
  '/orders':
    - 'GET'
    - 'POST'
    - 'PUT'
    - 'DELETE'
order_editor:
  '/orders':
    - 'GET'
    - 'POST'
    - 'PUT'
order_inspector:
  '/orders':
    - 'GET'

由此可见,REST天然能够实现权限控制。通过处理传入请求,REST服务能够检索有价值的信息,这些信息可以移交给单独的模块以执行身份验证和授权。如果用户被授权在目标资源上执行所请求的方法,则可以继续请求处理。否则,在到达任何内部应用程序代码之前拒绝进一步访问。

01-29 13:11