【Shiro】 授权

Metadata

title: 【Shiro】 授权
date: 2023-01-19 13:50
tags:
  - 行动阶段/完成
  - 主题场景/组件
  - 笔记空间/KnowladgeSpace/ProgramSpace/ModuleSpace
  - 细化主题/Module/Shiro/基础
categories:
  - Shiro
keywords:
  - Shiro
description: 【Shiro】 授权

【Shiro】 授权

授权本质上是访问控制——控制您的用户在您的应用程序中可以访问的内容,例如资源、网页等。大多数用户通过使用角色和权限等概念来执行访问控制。也就是说,通常允许用户根据分配给他们的角色和/或权限来做某事或不做某事。然后,您的应用程序可以根据对这些角色和权限的检查来控制公开哪些功能。如您所料,Subject API 允许您非常轻松地执行角色和权限检查。例如,清单 7 中的代码片段展示了如何检查一个 Subject 是否被分配了一个特定的角色。

清单7: 角色检查

if ( subject.hasRole("administrator") ) { 
    //显示“创建用户”按钮
} else { 
    //灰色按钮?
}

如您所见,您的应用程序可以根据访问控制检查来启用或禁用功能。

权限检查是执行授权的另一种方式。上述示例中的角色检查存在一个重大缺陷:您无法在运行时添加或删除角色。您的代码是用角色名称硬编码的,因此如果您更改了角色名称和/或配置,您的代码就会被破坏!如果您需要能够在运行时更改角色的含义,或者根据需要添加或删除角色,则必须依赖其他东西。

为此,Shiro 支持其权限概念。权限是功能的原始声明,例如“打开一扇门”、“创建博客条目”、“删除‘jsmith’用户”等。通过让权限反映应用程序的原始功能,您只需更改权限检查何时更改应用程序的功能。反过来,您可以根据需要在运行时将权限分配给角色或用户。

例如,如下面的清单 8 所示,我们可以重写之前的角色检查,改用权限检查。

清单8: 权限检查

if (subject.isPermitted("user:create") ) { 
    //显示“创建用户”按钮
} else { 
    //灰色按钮?
}

这样,任何分配了”user:create”权限的角色或用户都可以单击“创建用户”按钮,这些角色和分配甚至可以在运行时更改,为您提供了一个非常灵活的安全模型。

“user:create”字符串是遵守特定解析约定的权限字符串的示例。Shiro 通过其 WildcardPermission 开箱即用地支持此约定。虽然超出了本介绍文章的范围,但您会看到 WildcardPermission 在创建安全策略时可以非常灵活,甚至支持实例级访问控制等内容。

清单 9: 实例级权限检查

if (subject.isPermitted("user:delete:jsmith")) { 
    //删除 'jsmith' 用户
} else { 
    //不删除 'jsmith' 
}

此示例表明,如果需要,您可以控制对单个资源的访问,甚至可以控制到非常细粒度的实例级别。如果愿意,您甚至可以发明自己的权限语法。有关详细信息,请参阅Shiro 权限文档。最后,与身份验证一样,上述调用最终到达 SecurityManager,SecurityManager 将咨询一个或多个 Realms 以做出访问控制决策。这允许 Realm 在必要时响应身份验证和授权操作。

以上就是 Shiro 授权能力的简要概述。虽然大多数安全框架仅止于身份验证和授权,但 Shiro 提供的更多。接下来我们将讨论 Shiro 的高级会话管理功能。

授权要素

授权有三个核心元素,我们在Shiro中经常引用它们:权限、角色和用户。

Permissions

Apache Shiro中的权限代表安全策略中最原子的元素。它们基本上是关于行为的陈述,并显式地表示在应用程序中可以做什么。格式良好的权限语句基本上描述了资源以及当Subject与这些资源交互时可能进行的操作。

权限语句的一些例子:

  • 打开一个文件
  • 查看/user/list web页面
  • 打印文件
  • 删除jsmith用户

大多数资源将支持典型的CRUD(创建、读取、更新、删除)操作,但任何对特定资源类型有意义的操作都是可以的。基本思想是权限语句至少基于资源和操作。

在查看权限时,可能要认识到的最重要的事情是,权限语句没有表示谁可以执行所表示的行为。它们只是应用程序中可以做什么的声明。

权限只代表行为
权限语句只反映行为(与资源类型相关联的操作)。它们并不能反映谁有能力做出这样的行为。”)

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

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

在如何向用户授予权限方面有很多变化——应用程序根据应用程序需求决定如何对其建模。

稍后我们将介绍Shiro如何确定是否允许一个Subject做某事。

许可的粒度

上面的权限示例都指定了对资源类型(门、文件、客户等)的操作(打开、读取、删除等)。在某些情况下,它们甚至指定非常细粒度的实例级行为——例如,使用用户名’jsmith’(实例标识符)’删除’(操作)’用户’(资源类型)。在Shiro中,您可以精确地定义这些语句的粒度。

我们将在[[【Shiro】 权限|Shiro的权限文档]]中更详细地介绍权限粒度和权限语句的“级别”。

角色

角色是一个命名实体,通常表示一组行为或职责。这些行为转化为软件应用程序能做或不能做的事情。角色通常分配给用户帐户,因此通过关联,用户可以执行归属于各种角色的任务。

实际上有两种类型的角色,Shiro同时支持这两种概念:

  • 隐式角色:大多数人将角色用作隐式构造:应用程序仅基于角色名隐含一组行为(即权限)。对于隐式角色,在软件级别上不允许角色X执行行为A、B和C,行为仅由名称暗示。

这对于非常简单的应用程序来说可能是可行的(例如,可能有一个管理角色和其他所有人)。但对于更复杂或可配置的应用程序,这可能是应用程序整个生命周期的主要问题,并为软件带来巨大的维护成本。

  • 显式角色:显式角色本质上是实际权限语句的命名集合。在这种形式中,应用程序(和Shiro)确切地知道拥有某个特定角色意味着什么。因为知道可以执行或不能执行的确切行为,所以不需要猜测或暗示特定角色可以或不能执行什么。

Shiro团队提倡使用权限和显式角色,而不是旧的隐式方法。您将可以更好地控制应用程序的安全体验。

Users

用户本质上是应用程序的“谁”。然而,正如我们之前所讨论的,主体实际上是Shiro的“用户”概念。

允许用户(主题)通过与角色或直接权限的关联在应用程序中执行某些操作。应用程序的数据模型准确地定义了Subject如何允许做某事。

例如,在您的数据模型中,可能您有一个实际的User类,并且您直接将权限分配给User实例。或者您可能只直接将权限分配给角色,然后将角色分配给用户,因此通过关联,用户传递地“拥有”分配给他们的角色的权限。或者你可以用“Group”概念来表示这些东西。这取决于您——使用对您的应用程序有意义的东西。

您的数据模型准确地定义了授权的功能。Shiro依靠Realm实现将数据模型关联细节转换为Shiro能够理解的格式。稍后我们将介绍Realms是如何做到这一点的。

Authorizing Subjects

在Shiro中执行授权可以通过3种方式完成:

  • 以编程方式-您可以在java代码中使用if和else块等结构执行授权检查。
  • JDK注释——您可以将授权注释附加到Java方法
  • JSP/GSP TagLibs—您可以基于角色和权限控制JSP或GSP页面输出

Programmatic Authorization | 编程方式

执行授权最简单、最常见的方法可能是通过编程方式直接与当前Subject实例交互。

Role-Based Authorization | 基于角色的授权

如果你想基于更简单/传统的隐式角色名来控制访问,你可以执行角色检查:

Role Checks | 角色检查

如果您只想查看当前Subject是否有角色,可以在Subject实例上调用变体hasRole*方法。

例如,要查看Subject是否具有特定(单一)角色,可以调用该Subject。hasRole(roleName)方法,并做出相应的反应:

Subject currentUser = SecurityUtils.getSubject();

if (currentUser.hasRole("administrator")) {
    //show the admin button
} else {
    //don't show the button?  Grey it out?
}

根据你的需要,你可以调用一些面向角色的Subject方法:

Subject Method Description
hasRole(String roleName) 如果Subject被分配了指定的角色,则返回true,否则返回false。
hasRoles(List<String> roleNames) 返回与方法参数中的索引相对应的hasRole结果数组。如果需要执行许多角色检查(例如,在定制一个复杂的视图时),这对于提高性能非常有用。
hasAllRoles(Collection<String> roleNames) 如果Subject被分配了所有指定的角色,则返回true,否则返回false。
Role Assertions | 角色的断言

除了检查布尔值以查看Subject是否具有角色之外,您还可以在执行逻辑之前简单地断言它们具有预期的角色。如果Subject没有预期的角色,就会抛出AuthorizationException异常。如果它们确实具有预期的角色,则断言将安静地执行,逻辑将按照预期继续进行。

例如:

Subject currentUser = SecurityUtils.getSubject();

//guarantee that the current user is a bank teller and
//therefore allowed to open the account:
currentUser.checkRole("bankTeller");
openBankAccount();

hasRole*方法相比,这种方法的一个好处是代码可以更简洁,因为如果当前Subject不满足预期的条件(如果您不想这样做),您不必构造自己的authorizationexception。

根据你的需要,你可以调用一些面向角色的Subject断言方法:

Subject Method Description
checkRole(String roleName) 如果Subject被分配了指定的角色,则安静地返回;如果没有,则抛出AuthorizationException。
checkRoles(Collection<String> roleNames) 如果Subject被分配了所有指定的角色,则静静地返回;如果没有,则抛出AuthorizationException。
checkRoles(String…​ roleIdentifiers) 与上面的checkRoles方法效果相同,但是允许Java 5的var-args风格的参数。

Permission-Based Authorization | 基于许可的授权

如上所述,在角色概述中,执行访问控制的更好方法通常是通过基于权限的授权。基于权限的授权,因为它与应用程序的原始功能(以及应用程序核心资源上的行为)密切相关,所以基于权限的授权源代码会在功能发生变化时发生变化,而不是在安全策略发生变化时发生变化。这意味着代码受到的影响要比类似的基于角色的授权代码少得多。

Permission Checks | 权限检查

如果你想检查一个Subject是否被允许做一些事情,你可以调用任何不同的isallowed*方法变量。检查权限有两种主要方法——基于对象的权限实例或表示权限的字符串

Object-based Permission Checks | 基于对象的权限检查

执行权限检查的一种可能方法是实例化Shiro的org.apache.shiro.authz.Permission接口的实例,并将其传递给接受权限实例的* isallowed方法。

例如,考虑以下场景:办公室中有一台打印机,其唯一标识符为laserjet4400n。在允许当前用户按下“打印”按钮之前,我们的软件需要检查当前用户是否被允许在打印机上打印文档。查看是否可能的权限检查可以这样表述:

Permission printPermission = new PrinterPermission("laserjet4400n", "print");

Subject currentUser = SecurityUtils.getSubject();

if (currentUser.isPermitted(printPermission)) {
    //show the Print button
} else {
    //don't show the button?  Grey it out?
}

在这个例子中,我们还看到了一个非常强大的实例级访问控制检查的例子——基于单个数据实例限制行为的能力。

基于对象的权限在以下情况下是有用的:

  • 你需要编译时类型安全
  • 您希望保证权限被正确地表示和使用
  • 您希望显式控制权限解析逻辑(基于permission接口的implies方法,称为权限隐含逻辑)的执行方式。
  • 您希望保证Permissions能够准确地反映应用程序资源(例如,在基于项目域模型的项目构建过程中,可能会自动生成Permission类)。

根据你的需要,你可以调用一些面向对象权限的Subject方法:

Subject Method Description
isPermitted(Permission p) 如果Subject被允许执行某个操作或访问指定Permission实例汇总的资源,则返回true,否则返回false。
isPermitted(List<Permission> perms) 返回一个isallowed结果数组,对应于方法参数中的索引。如果需要执行许多权限检查(例如,当定制一个复杂的视图时),这对于提高性能非常有用。
isPermittedAll(Collection<Permission> perms) Returns true if the Subject is permitted all of the 如果Subject被允许所有指定的权限,则返回true,否则返回false。specified permissions, false otherwise.
String-based permission checks | 基于字符串的权限检查

虽然基于对象的权限可能很有用(编译时类型安全、有保证的行为、自定义蕴涵逻辑等),但对于许多应用程序来说,它们有时会感觉有点“笨重”。另一种方法是使用普通字符串来表示权限实例。

例如,基于上面的打印权限示例,我们可以重新制定相同的检查作为基于string的权限检查:

Subject currentUser = SecurityUtils.getSubject();

if (currentUser.isPermitted("printer:print:laserjet4400n")) {
    //show the Print button
} else {
    //don't show the button?  Grey it out?
}

这个例子仍然显示了相同的实例级权限检查,但是权限的重要部分——printer(资源类型)、print(动作)和laserjet4400n(实例id)——都在String中表示。
这个特殊的示例显示了Shiro默认的org.apache.shiro.authz.permission.WildcardPermission实现定义的一种特殊的以“:”分隔的格式,大多数人都认为这种格式合适。
也就是说,上面的代码块(主要)是以下代码的快捷方式:

Subject currentUser = SecurityUtils.getSubject();

Permission p = new WildcardPermission("printer:print:laserjet4400n");

if (currentUser.isPermitted(p) {
    //show the Print button
} else {
    //don't show the button?  Grey it out?
}

在Shiro的权限文档中深入介绍了WildcardPermission令牌格式和形成选项。

虽然上面的字符串默认为WildcardPermission格式,但如果您喜欢,实际上可以创建自己的字符串格式并使用它。我们将在下面的领域授权部分介绍如何做到这一点。

基于字符串的权限是有益的,因为您不必强制实现接口,而且简单的字符串通常易于阅读。缺点是您没有类型安全,如果您需要Strings所代表的范围之外的更复杂的行为,您将希望基于权限接口实现自己的权限对象。在实践中,大多数Shiro终端用户都选择基于字符串的方法,因为它们简单,但最终将由应用程序的需求决定哪种方法更好。

像基于对象的权限检查方法一样,有String变量来支持基于字符串的权限检查:

Subject Method Description
isPermitted(String perm) 如果Subject被允许执行某个操作或访问由指定String权限汇总的资源,则返回true,否则返回false。
isPermitted(List perms) 返回一个isallowed结果数组,对应于方法参数中的索引。如果需要执行许多String权限检查(例如,当自定义一个复杂的视图时),对于性能提高非常有用。
isPermittedAll(String…​ perms) 如果Subject被允许所有指定的String权限,则返回true,否则返回false。

Notation-based Authorization | 基于符号的授权

除了Subject API调用之外,如果您更喜欢基于元的授权控制,Shiro还提供了一系列Java 5+注释。

配置

在使用Java注释之前,需要在应用程序中启用AOP支持。有许多不同的AOP框架,因此,不幸的是,在应用程序中启用AOP没有标准的方法。

对于AspectJ,您可以回顾我们的AspectJ示例应用程序。
对于Spring应用程序,您可以查看我们的Spring Integration文档。
对于Guice应用程序,您可以查看我们的Guice集成文档。

The RequiresAuthentication annotation

RequiresAuthentication注释要求当前Subject在其当前会话期间已通过身份验证,以便访问或调用带注释的类/实例/方法。

For example:

@RequiresAuthentication
public void updateAccount(Account userAccount) {
    //this method will only be invoked by a
    //Subject that is guaranteed authenticated
    ...
}

这在很大程度上等同于以下基于subject的逻辑:

public void updateAccount(Account userAccount) {
    if (!SecurityUtils.getSubject().isAuthenticated()) {
        throw new AuthorizationException(...);
    }

    //Subject is guaranteed authenticated here
    ...
}

The RequiresGuest annotation

RequiresGuest注释要求当前Subject是一个“来宾”,也就是说,它们没有经过身份验证,也没有从要访问或调用的注释类/实例/方法的前一个会话中被记住。

For example:

@RequiresGuest
public void signUp(User newUser) {
    //this method will only be invoked by a
    //Subject that is unknown/anonymous
    ...
}

这在很大程度上等同于以下基于subject的逻辑:

public void signUp(User newUser) {
    Subject currentUser = SecurityUtils.getSubject();
    PrincipalCollection principals = currentUser.getPrincipals();
    if (principals != null && !principals.isEmpty()) {
        //known identity - not a guest:
        throw new AuthorizationException(...);
    }

    //Subject is guaranteed to be a 'guest' here
    ...
}

The RequiresPermissions annotation

RequiresPermissions注释要求允许当前Subject具有一个或多个权限,以便执行注释方法。

For example:

@RequiresPermissions("account:create")
public void createAccount(Account account) {
    //this method will only be invoked by a Subject
    //that is permitted to create an account
    ...
}

这在很大程度上等同于以下基于subject的逻辑:

public void createAccount(Account account) {
    Subject currentUser = SecurityUtils.getSubject();
    if (!subject.isPermitted("account:create")) {
        throw new AuthorizationException(...);
    }

    //Subject is guaranteed to be permitted here
    ...
}

The RequiresRoles permission

RequiresRoles注释要求当前Subject拥有所有指定的角色。如果它们没有角色,则不会执行该方法,并抛出AuthorizationException异常。

例如:

@RequiresRoles("administrator")
public void deleteUser(User user) {
    //this method will only be invoked by an administrator
    ...
}

这在很大程度上等同于以下基于subject的逻辑:

public void deleteUser(User user) {
    Subject currentUser = SecurityUtils.getSubject();
    if (!subject.hasRole("administrator")) {
        throw new AuthorizationException(...);
    }

    //Subject is guaranteed to be an 'administrator' here
    ...
}

The RequiresUser annotation

RequiresUser*注释要求当前Subject是要访问或调用的注释类/实例/方法的应用程序用户。“应用程序用户”被定义为具有已知身份的Subject,该身份要么是由于在当前会话期间进行身份验证而已知的,要么是从前一个会话的“RememberMe”服务中记住的。

@RequiresUser
public void updateAccount(Account account) {
    //this method will only be invoked by a 'user'
    //i.e. a Subject with a known identity
    ...
}

这在很大程度上等同于以下基于subject的逻辑:

public void updateAccount(Account account) {
    Subject currentUser = SecurityUtils.getSubject();
    PrincipalCollection principals = currentUser.getPrincipals();
    if (principals == null || principals.isEmpty()) {
        //no identity - they're anonymous, not allowed:
        throw new AuthorizationException(...);
    }

    //Subject is guaranteed to have a known identity here
    ...
}

授权序列

现在我们已经看到了如何基于当前主题执行授权,让我们看看每当进行授权调用时Shiro内部发生了什么。

我们已经参考了前一章中的架构图,只高亮显示了与授权相关的组件。每个数字表示授权操作中的一个步骤:

  • 步骤1:应用程序或框架代码调用主题hasRole*checkRole*isallowed *checkPermission*方法的任何变体,传递所需的权限或角色表示。
  • 步骤2:Subject实例,通常一个DelegatingSubject(或子类)通过调用SecurityManager几乎相同的hasRole*checkRole*ispermit*checkPermission*方法变体委托给应用程序的SecurityManager(SecurityManager实现org.apache.shiro.authz.Authorizer接口,它定义了所有特定于Subject的授权方法)。
  • 步骤3:SecurityManager,作为一个基本的“雨伞”组件,通过调用授权方各自的hasRole*checkRole*isallowed*checkPermission*方法,传递/委托给它内部的org.apache.shiro.authz.Authorizer实例。默认情况下,authorizer实例是一个ModularRealmAuthorizer实例,它支持在任何授权操作期间协调一个或多个领域实例。
  • 步骤4:检查每个配置的领域是否实现相同的授权器接口。如果是这样,就会调用领域各自的hasRole*checkRole*isallowed*checkPermission*方法。

模块化领域授权器

如前所述,Shiro SecurityManager的实现默认使用ModularRealmAuthorizer实例。ModularRealmAuthorizer同样支持具有单领域和多领域的应用程序。

对于任何授权操作,ModularRealmAuthorizer将迭代其内部领域集合,并按迭代顺序与每个领域进行交互。每个领域的交互功能如下:

  1. 如果领域本身实现了Authorizer接口,则调用其相应的Authorizer方法(hasRole*checkRole*isallowed*checkPermission*)。
    1. 如果领域的方法导致异常,则该异常将作为AuthorizationException传播到主题调用者。这将缩短授权过程,并且不会为该授权操作咨询任何剩余领域。
    2. 如果领域的方法是返回布尔值的hasRole*isallowed*变体,并且返回值为true,则立即返回true值,并且所有剩余的领域都被短路。这种行为作为一种性能增强而存在,因为通常情况下,如果一个领域允许,则意味着该主题是允许的。这有利于那些默认禁止一切而明确允许一切的安全策略,这是最安全的安全策略类型。
  2. 如果领域没有实现Authorizer接口,则忽略它。

Realm Authorization Order

需要指出的是,就像身份验证一样,ModularRealmAuthorizer将按迭代顺序与Realm实例进行交互。

ModularRealmAuthorizer可以访问SecurityManager上配置的Realm实例。当执行授权操作时,它将迭代该集合,并且对于每个实现Authorizer接口本身的领域,调用该领域各自的Authorizer方法(例如hasRole*checkRole*isallowed*checkPermission*)。

配置全局PermissionResolver

当执行基于字符串的权限检查时,大多数Shiro的默认域实现都会在执行权限蕴涵逻辑之前先将此字符串转换为实际的权限实例。

这是因为权限是基于蕴涵逻辑进行评估的,而不是直接的相等性检查(有关蕴涵与相等性的更多信息,请参阅Permission文档)。蕴涵逻辑在代码中表现得比字符串比较好。因此,大多数领域需要将提交的权限字符串转换或解析为相应的代表性权限实例。

为了帮助这种转换,Shiro支持PermissionResolver的概念。大多数Shiro领域实现使用PermissionResolver来支持Authorizer接口基于字符串的权限方法的实现:当在领域上调用这些方法之一时,它将使用PermissionResolver将字符串转换为权限实例,并以这种方式执行检查。

所有Shiro领域的实现默认都是一个内部的WildcardPermissionResolver,它假定Shiro的WildcardPermission字符串格式。

如果您想创建自己的PermissionResolver实现,可能要支持自己的权限字符串语法,并且希望所有配置的领域实例都支持该语法,那么可以为所有可以配置一个的领域全局设置PermissionResolver。

例如,在shiro.ini中:

globalPermissionResolver = com.foo.bar.authz.MyPermissionResolver
...
securityManager.authorizer.permissionResolver = $globalPermissionResolver
...

permissionResolver = com.foo.bar.authz.MyPermissionResolver

realm = com.foo.bar.realm.MyCustomRealm
realm.permissionResolver = $permissionResolver
...

配置全局RolePermissionResolver

在概念上类似于PermissionResolver, RolePermissionResolver具有表示领域执行权限检查所需的权限实例的能力。

不过,RolePermissionResolver的关键区别在于,输入字符串是角色名,而不是权限字符串。

当需要将角色名转换为一组具体的权限实例时,领域可以在内部使用RolePermissionResolver。

这是一个特别有用的特性,用于支持可能没有权限概念的遗留或不灵活的数据源。

例如,许多LDAP目录存储角色名(或组名),但不支持角色名与具体权限的关联,因为它们没有“权限”的概念。基于shiro的应用程序可以使用存储在LDAP中的角色名,但要实现RolePermissionResolver来将LDAP名转换为一组显式权限,以执行首选的显式访问控制。权限关联将存储在另一个数据存储中,可能是本地数据库。

因为这种将角色名转换为权限的概念是非常特定于应用程序的,Shiro的默认域实现不使用它们。

但是,如果您想创建自己的RolePermissionResolver,并且有多个需要配置的领域实现,则可以为可以配置一个的所有领域全局设置RolePermissionResolver。

globalRolePermissionResolver = com.foo.bar.authz.MyPermissionResolver
...
securityManager.authorizer.rolePermissionResolver = $globalRolePermissionResolver
...
rolePermissionResolver = com.foo.bar.authz.MyRolePermissionResolver

realm = com.foo.bar.realm.MyCustomRealm
realm.rolePermissionResolver = $rolePermissionResolver
...

自定义授权人

如果您的应用程序使用多个领域来执行授权,而ModularRealmAuthorizer默认是简单的基于迭代的短路授权行为,不能满足您的需求,您可能需要创建一个自定义授权器并相应地配置SecurityManager。

例如,在shiro.ini中:

[main]
...
authorizer = com.foo.bar.authz.CustomAuthorizer

securityManager.authorizer = $authorizer