了解 Apache Shiro 中的权限

Shiro 将“权限”定义为定义显式行为或操作的语句。这是对应用程序中原始功能的声明,仅此而已。权限是安全策略中最底层的结构,它们明确定义仅应用程序可以执行的“操作”。

他们根本不描述“谁”能够执行这些动作。

权限的一些示例:

  • 开启 Files

  • 查看“ /用户/列表”网页

  • Print documents

  • 删除“ jsmith”用户

定义允许“谁”(用户)执行“什么”(权限)是一种以某种方式向用户分配权限的练习。这始终由应用程序的数据模型完成,并且在不同应用程序之间可能会有很大差异。

例如,权限可以分组在一个角色中,并且该角色可以与一个或多个用户对象相关联。或者某些应用程序可以具有一个用户组,并且可以为一个组分配一个角色,这通过传递关联将意味着该组中的所有用户都隐式地获得了该角色中的权限。

如何授予用户权限有许多变体-应用程序根据应用程序需求确定如何对此建模。

Wildcard Permissions

上面的权限示例,“打开文件”,“查看'用户/列表'网页”等都是有效的权限声明。但是,在计算上很难解释那些自然语言字符串并确定是否允许用户执行该行为。

因此,为了启用易于处理但仍可读的权限声明,Shiro 提供了强大而直观的权限语法,我们称之为 WildcardPermission。

Simple Usage

假设您要保护对公司打印机的访问权限,以便某些人可以打印到特定打印机,而其他人可以查询队列中当前有哪些作业。

一种非常简单的方法是向用户授予“ queryPrinter”权限。然后,您可以通过调用以下命令来检查用户是否具有 queryPrinter 权限:

subject.isPermitted("queryPrinter")

这(大部分)相当于

subject.isPermitted( new WildcardPermission("queryPrinter") )

但以后会更多。

简单权限字符串可能适用于简单应用程序,但是它要求您具有“ printPrinter”,“ queryPrinter”,“ managePrinter”等权限。您还可以使用通配符向用户授予“ ”权限(赋予此权限构造其名称),这意味着它们在整个整个应用程序*中具有 *所有 *权限。

但是,使用这种方法无法仅说用户具有“所有打印机权限”。因此,通配符权限支持多个级别的权限。

Multiple Parts

通配权限支持多个* level parts *的概念。例如,您可以通过向用户授予权限来重组前面的简单示例

printer:query

在此示例中,冒号是一个特殊字符,用于分隔权限字符串中的下一部分。

在此示例中,第一部分是正在操作的域(printer),第二部分是正在执行的动作(query)。上面的其他示例将更改为:

printer:print
printer:manage

可以使用的部件数量没有限制,因此可以在应用程序中使用的方式取决于您的想象。

Multiple Values

每个部分可以包含多个值。因此,除了向用户授予“ printer:print”和“ printer:query”权限之外,您还可以向他们授予一个权限:

printer:print,query

这使他们能够使用printquery打印机。由于这两个操作都被授予,因此您可以通过调用以下命令来检查用户是否具有查询打印机的能力:

subject.isPermitted("printer:query")

会返回true

All Values

如果您想授予用户特定部分的“所有”值,该怎么办?这样做比手动列出每个值更方便。同样,基于通配符,我们可以执行此操作。如果printer域具有 3 个可能的操作(queryprintmanage),则此操作:

printer:query,print,manage

变成这样:

printer:*

然后,对“ printer:XXX”的* any *权限检查将返回true。以这种方式使用通配符比显式列出操作具有更好的伸缩性,因为如果以后在应用程序中添加了新操作,则无需更新在该部分中使用通配符的权限。

最后,也可以在通配符许可字符串的任何部分中使用通配符。例如,如果您想授予用户跨所有域(而不仅仅是打印机)的“查看”操作,则可以授予此权限:

*:view

然后,对“ foo:view”的任何权限检查都将返回true

实例级访问控制

通配符权限的另一种常见用法是为实例级访问控制列表建模。在这种情况下,您使用三个部分-第一部分是* domain ,第二部分是 action *,第三部分是要执行的实例。

例如,您可能有

printer:query:lp7200
printer:print:epsoncolor

第一个将行为定义为 ID 为lp7200query printer。第二个权限将行为print定义为 ID 为epsoncolorprinter。如果您将这些权限授予用户,则他们可以在特定实例上执行特定行为。然后,您可以执行签入代码:

if ( SecurityUtils.getSubject().isPermitted("printer:query:lp7200") {
    // Return the current jobs on printer lp7200 }
}

这是表达权限的一种非常强大的方法。但是同样,必须为所有打印机定义多个实例 ID 并不能很好地扩展,特别是在将新打印机添加到系统中时。您可以改用通配符:

printer:print:*

这确实可以扩展,因为它也涵盖了所有新打印机。您甚至可以允许访问所有打印机上的所有操作:

printer:*:*

或单个打印机上的所有操作:

printer:*:lp7200

甚至特定的动作:

printer:query,print:lp7200

许可的任何部分都可以使用“ *”通配符和“,”子部分分隔符。

Missing Parts

关于权限分配要注意的最后一件事:缺少部分意味着用户可以访问与该部分相对应的所有值。换一种说法,

printer:print

相当于

printer:print:*

and

printer

相当于

printer:*:*

但是,您只能删除字符串* end *的部分,因此:

printer:lp7200

***不等于

printer:*:lp7200

Checking Permissions

为了方便和可扩展性,权限分配大量使用通配符构造(“ printer:print:” =可打印到任何打印机),运行时权限 checks 应该始终*始终基于最具体的权限字符串。

例如,如果用户具有 UI,并且他们想将文档打印到lp7200打印机,则您应该**通过执行以下代码来检查是否允许用户这样做:

if ( SecurityUtils.getSubject().isPermitted("printer:print:lp7200") ) {
    //print the document to the lp7200 printer }
}

该检查非常具体,并且明确反映了用户当时正在尝试执行的操作。

但是,以下内容对于运行时检查不太理想:

if ( SecurityUtils.getSubject().isPermitted("printer:print") ) {
    //print the document }
}

为什么?因为第二个示例显示“您必须能够打印到任何打印机才能执行以下代码块”。但是请记住,“ printer:print”等效于“ printer:print:*”!

因此,这是不正确的检查。如果当前用户没有能力使用任何打印机进行打印,但是他们确实具有打印能力,例如lp7200epsoncolor打印机,该怎么办?那么上面的第二个示例将永远不允许他们使用lp7200打印机进行打印,即使他们已被授予该功能!

因此,经验法则是在执行权限检查时尽可能使用最具体的权限字符串。当然,如果您确实只想在允许用户使用任何打印机进行打印的情况下仅执行代码块,则上面的第二个块可能是应用程序中其他位置的有效检查(可疑,但可能)。您的应用程序将确定哪些检查有意义,但总的来说,检查越具体越好。

含意,而非平等

为什么运行时权限检查应尽可能具体,但权限分配却可能更通用?这是因为权限检查是通过“蕴含”逻辑进行评估的,而不是相等性检查。

也就是说,如果为用户分配了user:*权限,则暗示该用户可以执行user:view操作。字符串“ user:”显然不等于“ user:view”,但是前者暗含后者。 “用户:”描述了由“用户:视图”定义的功能的超集。

为了支持隐含规则,所有权限都转换为实现org.apache.shiro.authz.Permission接口的对象实例。这样,隐含逻辑可以在运行时执行,并且隐含逻辑通常比简单的字符串相等性检查更复杂。 org.apache.shiro.authz.permission.WildcardPermission类实现实际上使本文档中描述的所有通配符行为成为可能。以下是一些其他通配符权限字符串,这些字符串通过含义显示访问权限:

user:*

意味着也可以删除用户:

user:delete

Similarly,

user:*:12345

意味着还可以更新 ID 为 12345 的用户帐户:

user:update:12345

and

printer

暗示能够打印到任何打印机

printer:print

Performance Considerations

权限检查比简单的 equals 比较更为复杂,因此必须为每个分配的 Permission 执行运行时蕴含逻辑。当使用上面显示的权限字符串时,您暗中使用了 Shiro 的默认WildcardPermission,它执行必要的隐含逻辑。

Shiro 对于 Realm 实现的默认行为是,对于每个权限检查(例如,对subject.isPermitted的调用),都需要检查分配给该用户(在其组,角色中或直接分配给他们)的全部权限个别地暗示。 Shiro 通过在第一次成功检查后立即返回以提高性能来“短路”此过程,但这不是灵丹妙药。

当使用正确的CacheManager时,将用户,角色和权限缓存在内存中时,这通常非常快,而 Shiro 确实支持 Realm 实现。只需知道,使用这种默认行为,随着分配给用户或其角色或组的权限数量增加,执行检查的时间必然会增加。

如果 Realm 实现者具有更有效的方式来检查权限并执行此隐含逻辑,尤其是基于应用程序的数据模型,则他们应将其实现为 Realm isPermitted *方法实现的一部分。存在默认的 Realm/WildcardPermission 支持以覆盖大多数用例的 80-90%,但对于具有大量在运行时存储和/或检查的权限的应用程序,它可能不是最佳解决方案。

lendAHandDoc()