Shiro授权

作者: 李白不喜欢杜甫 | 来源:发表于2020-05-27 17:23 被阅读0次

    授权组成

    权限

    Apache Shiro中的权限代表安全策略中最基本的元素。从根本上讲,它们是有关行为的声明,并明确表示可以在应用程序中完成的操作。格式正确的权限声明本质上描述了资源以及Subject与之交互时可以采取的措施。
    一些许可声明的例子:
    -打开文件
    -查看某个请求的视图
    -打印文档
    -删除某个用户
    大多数资源将支持典型的CRUD(创建、读取、更新、删除)操作,但是对于特定的资源类型,任何有意义的操作都可以。基本思想是,权限语句至少是基于资源和操作的。
    在查看权限时,可能要认识到的最重要的事情是权限语句没有表示谁可以执行所表示的行为。它们只是应用程序中可以执行的操作的语句。
    定义允许谁(用户)做什么(权限)是一种以某种方式向用户分配权限的练习。这通常是由应用程序的数据模型完成的,并且在不同的应用程序之间会有很大的差异。
    例如,权限可以分组在一个角色中,并且该角色可以与一个或多个用户对象相关联。或者,某些应用程序可以有一组用户,并且可以为一组分配一个角色,通过传递关联,这意味着该组中的所有用户都隐式地授予了角色中的权限。
    对于如何向用户授予权限,有许多变体——应用程序决定如何根据应用程序需求对此进行建模。

    权限粒度

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

    角色

    角色是一个命名实体,通常表示一组行为或职责。这些行为转化为您可以或不能用软件应用程序做的事情。角色通常分配给用户帐户,因此通过关联,用户可以“做”归于不同角色的事情。
    实际上有两种类型的角色,Shiro支持这两种概念:
    -隐式角色:大多数人将角色用作隐式构造:应用程序仅根据角色名暗示一组行为(即权限)。对于隐式角色,在软件级别上没有什么规定“允许角色X执行行为A、B和C”。行为仅仅通过名字就能暗示出来。
    注意:虽然隐式角色是最简单和最常见的方法,但是它可能会带来很多软件维护和管理问题。
    例如,如果您只是想添加或删除一个角色,或者稍后重新定义一个角色的行为,该怎么办?您将不得不返回到源代码并更改所有角色检查,以反映安全模型中的更改,每次都需要这样的更改!更不用说由此产生的操作成本(重新测试、通过QA、关闭应用程序、用新角色检查升级软件、重新启动应用程序,等等)。
    对于非常简单的情况,这可能是可以的
    -显式角色:显式角色实际上是实际权限语句的命名集合。在这个表单中,应用程序(以及Shiro)确切地知道拥有某个特定角色是什么意思。因为它知道可以执行或不可以执行的确切行为,所以不需要猜测或暗示某个特定角色可以或不可以执行什么。
    深入了解:[The New RBAC: Resource-Based Access Control]

    用户

    用户本质上是应用程序的“谁”。然而,正如我们之前所讨论的,主题实际上是Shiro的“用户”概念。
    用户(主题)可以通过与角色或直接权限的关联在应用程序中执行某些操作。您的应用程序的数据模型准确地定义了允许或不允许主题执行某些操作的方式。
    例如,在您的数据模型中,可能有一个实际的User类,并将权限直接分配给User实例。或者您可能只将权限直接分配给角色,然后通过associati将角色分配给用户
    笔记:最终,您的数据源实现是与数据源(RDBMS、LDAP等)进行通信的实现。因此,您的领域将告诉Shiro角色或权限是否存在。您可以完全控制授权模型的结构和定义方式。

    授权主体

    在Shiro中执行授权有三种方式:

    • 编程方式——可以在java代码中使用if和else块这样的结构执行授权检查。
    • JDK注释——可以将授权注释附加到Java方法
    • JSP/GSP标记库——您可以根据角色和权限控制JSP或GSP页面输出

    编程式授权

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

    基于角色的授权

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

    角色检查

    如果只想检查当前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 Method Description
    hasRole(String roleName) 如果指定了指定的角色,则返回true,否则返回false。
    hasRoles(List<String> roleNames) 返回与方法参数中的索引对应的hasRole结果数组。如果需要执行许多角色检查(例如,在定制复杂视图时),可以作为性能增强的有用工具。
    hasAllRoles(Collection<String> roleNames) R如果给主题分配了所有指定的角色,则返回true,否则返回false。

    角色认定

    除了检查布尔值以查看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方法相比,这种方法的一个优点是代码可以更简洁一些,因为如果当前主题不满足预期的条件(如果不希望这样),您就不必构造自己的authorizationexception。
    与hasRole
    方法相比,这种方法的一个优点是代码可以更简洁一些,因为如果当前主题不满足预期的条件(如果不希望这样),您就不必构造自己的authorizationexception。

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

    基于许可的授权

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

    权限检查

    如果您想检查主题是否被允许执行某些操作,您可以调用各种isPermitted方法的变体。有两种主要的检查权限的方法——使用基于对象的权限实例或使用表示权限的字符串
    基于对象的权限检查:
    执行权限检查的一种可能方法是实例化Shiro的org.apache.shiro.authz的一个实例。权限接口,并将其传递给接受权限实例的
    isPermitted方法。
    例如,考虑以下场景:办公室中有一台打印机,其惟一标识符为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?
    }
    

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

    • 您需要编译时类型安全
    • 您希望确保权限得到正确表示和使用
    • 您需要显式地控制权限解析逻辑(称为权限隐含逻辑,基于权限接口的implies方 法)的执行方式。
    • 您希望确保权限准确地反映应用程序资源(例如,可能可以在项目的构建过程中基于项目的域模型自动生成权限类)。
      有几个面向对象许可的主题方法,你可以调用,取决于你的需要:
      | Subject Method | Description |
      | --- | --- |
      | isPermitted(Permission p) | 如果允许主题执行操作或访问由指定权限实例汇总的资源,则返回true,否则返回false。 |
      | isPermitted(List<Permission> perms) | 返回与方法参数中的索引对应的isallowed结果数组。如果需要执行许多权限检查(例如,在定制复杂视图时),可作为性能增强的有用工具。|
      | isPermittedAll(Collection<Permission> perms) | 如果主题允许所有指定的权限,则返回true,否则返回false。|
      基于字符串的权限检查
      虽然基于对象的权限可能很有用(编译时类型安全、有保证的行为、定制的隐含逻辑等),但对于许多应用程序来说,它们有时会让人感觉有些“笨拙”。另一种方法是使用普通字符串来表示权限实例。
      例如,基于上面的打印权限示例,我们可以重新制定相同的检查作为基于字符串的权限检查:
    Subject currentUser = SecurityUtils.getSubject();
    
    if (currentUser.isPermitted("printer:print:laserjet4400n")) {
      //show the Print button
    } else {
      //don't show the button?  Grey it out? 
    }
    

    这个示例仍然显示了相同的实例级权限检查,但是权限的重要部分—打印机(资源类型)、打印(操作)和laserjet4400n(实例id)—都在一个字符串中表示。
    这个特殊的例子显示了由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?
    }
    
    Subject Method Description
    isPermitted(String perm) 如果允许主题执行操作或访问按指定字符串权限汇总的资源,则返回true,否则返回false。
    isPermitted(String... perms) 返回与方法参数中的索引对应的isallowed结果数组。如果需要执行许多字符串权限检查(例如,在定制复杂视图时),可以作为性能增强工具使用。
    isPermittedAll(String... perms) 如果主题允许所有指定的字符串权限,则返回true,否则返回false。

    权限认定

    作为检查布尔值以查看是否允许主题执行某些操作的替代方法,您可以简单地断言它们在执行逻辑之前具有预期的权限。如果不允许该主题,将抛出AuthorizationException。如果它们如预期的那样被允许,则断言将静静地执行,逻辑将如预期的那样继续。
    例如:

    Subject currentUser = SecurityUtils.getSubject();
    
    //guarantee that the current user is permitted 
    //to open a bank account: 
    Permission p = new AccountPermission("open");
    currentUser.checkPermission(p);
    openBankAccount();
    

    或者,同样的检查,使用一个字符串权限:

     Subject currentUser = SecurityUtils.getSubject();
    
    //guarantee that the current user is permitted 
    //to open a bank account: 
    currentUser.checkPermission("account:open");
    openBankAccount();
    

    与isPermitted*方法相比,这种方法的一个好处是代码可以更简洁一些,因为如果当前主题不满足预期的条件(如果您不希望),您就不必构造自己的authorizationexception。
    根据您的需要,很少有面向许可的主题断言方法可以调用:

    Subject Method Description
    checkPermission(Permission p) 如果允许主题执行操作或访问由指定的权限实例汇总的资源,则静静地返回;如果不允许,则抛出AuthorizationException。
    checkPermission(String perm) 如果允许主题执行操作或访问由指定的字符串权限汇总的资源,则静静地返回;如果不允许,则抛出AuthorizationException。
    checkPermissions(Collection<Permission> perms) 如果主题允许所有指定的权限,则静静地返回;如果不允许,则抛出AuthorizationException。
    checkPermissions(String... perms) 与上面的checkPermissions方法相同,但是使用基于字符串的权限。

    基于注解的授权

    除了主题API调用之外,如果您喜欢基于元数据的授权控制,Shiro还提供了一组Java 5+注释。

    配置

    在使用Java注释之前,需要在应用程序中启用AOP支持。有许多不同的AOP框架,因此,不幸的是,没有在应用程序中启用AOP的标准方法。
    对于AspectJ,您可以查看我们的AspectJ示例应用程序。
    对于Spring应用程序,您可以查看我们的Spring Integration文档。
    对于Guice应用程序,您可以查看我们的Guice集成文档

    RequiresAuthentication注释

    RequiresAuthentication注释要求当前主体在其当前会话期间对要访问或调用的带注释的类/实例/方法进行身份验证。
    例如:

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

    相当于下面的代码:

    public void updateAccount(Account userAccount) {
      if (!SecurityUtils.getSubject().isAuthenticated()) {
          throw new AuthorizationException(...);
      }
    
      //Subject is guaranteed authenticated here
      ...
    }
    

    RequiresGuest注释

    RequiresGuest注释要求当前主题是一个“guest”,也就是说,对于要访问或调用的带注释的类/实例/方法,它们没有经过身份验证或从上一个会话中被记住。
    例如:

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

    相当于下面的代码:

    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
     ...
    }
    

    RequiresPermissions注释

    requirespermission注释要求当前主题被允许有一个或多个权限,以执行带注释的方法。
    例如:

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

    相当于下面的代码:

    public void createAccount(Account account) {
      Subject currentUser = SecurityUtils.getSubject();
      if (!subject.isPermitted("account:create")) {
          throw new AuthorizationException(...);
      }
    
      //Subject is guaranteed to be permitted here
      ...
    }
    

    RequiresRoles许可

    RequiresRoles注释要求当前主题拥有所有指定的角色。如果它们不具有角色,则将不执行该方法并引发AuthorizationException。
    例如:

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

    相当于下面的代码:

    public void deleteUser(User user) {
      Subject currentUser = SecurityUtils.getSubject();
      if (!subject.hasRole("administrator")) {
          throw new AuthorizationException(...);
      }
    
      //Subject is guaranteed to be an 'administrator' here
      ...
    }
    

    RequiresUser注释

    RequiresUser*注释要求当前主题是要访问或调用的带注释的类/实例/方法的应用程序用户。“应用程序用户”被定义为具有已知标识的主题,由于在当前会话中进行了身份验证,或者从上一个会话的“memorberme”服务中记住了该标识。

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

    相当于下面的代码:

    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
      ...
    }
    

    JSP(其他模板)的标签授权

    hiro提供了一个标记库,用于根据主题状态控制JSP/GSP页面输出。

    授权序列

    现在我们已经了解了如何基于当前主题执行授权,接下来让我们看看在进行授权调用时Shiro内部会发生什么。
    我们从体系结构一章中提取了前面的体系结构图,只突出显示了与授权相关的组件。每个数字代表授权操作中的一个步骤:

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

    ModularRealmAuthorizer

    如前所述,Shiro SecurityManager实现默认使用ModularRealmAuthorizer实例。ModularRealmAuthorizer同样支持具有单个领域的应用程序和具有多个领域的应用程序。
    对于任何授权操作,ModularRealmAuthorizer都将遍历其内部领域集合,并按迭代顺序与每个领域交互。各领域交互功能如下:

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

    数据源授权命令

    需要指出的是,与身份验证完全一样,ModularRealmAuthorizer将按迭代顺序与领域实例进行交互。
    ModularRealmAuthorizer可以访问在SecurityManager上配置的领域实例。当执行授权操作时,它将迭代该集合,对于实现Authorizer接口本身的每个域,调用该域各自的Authorizer方法(例如hasRole、checkRole、ispermitted 或checkPermission)。

    配置一个全局PermissionResolver

    在执行基于字符串的权限检查时,Shiro的大多数默认域实现在执行权限隐含逻辑之前首先将该字符串转换为实际的权限实例。
    这是因为权限是基于隐含逻辑进行评估的,而不是直接的相等性检查(有关隐含与相等的更多信息,请参阅权限文档)。隐含逻辑在代码中比通过字符串比较更好地表示。因此,大多数领域需要将提交的权限字符串转换或解析为对应的代表权限实例。
    为了帮助进行这种转换,Shiro支持PermissionResolver的概念。大多数Shiro领域实现使用一个PermissionResolver来支持他们的授权人接口实现的方法:基于字符串的许可当调用这些方法的一个领域,它将使用PermissionResolver将字符串转换成一个许可实例,并执行的检查方法。
    所有Shiro Realm实现都默认使用内部WildcardPermissionResolver,该解析器采用Shiro的WildcardPermission字符串格式。
    如果您希望创建自己的PermissionResolver实现,可能是为了支持您自己的权限字符串语法,并且您希望所有配置的领域实例都支持该语法,那么您可以为所有可以配置一个领域的领域全局地设置您的PermissionResolver。
    例如,在shiro.ini中:
    shiro.ini

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

    注意:如果您想要配置一个全局的PermissionResolver,则要接收所配置的PermissionResolver的每个域必须实现PermisionResolverAware接口。这保证了所配置的实例可以转发到支持这种配置的每个域。
    如果你不想使用全局的PermissionResolver,或者你不想被PermissionResolver接口打扰,你可以用一个PermissionResolver实例显式地配置一个域(假设有一个与javabean兼容的setPermissionResolver方法):

    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的领域全局设置您的RolePermissionResolver。
    shiro.ini

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

    注意:如果您想要配置一个全局RolePermissionResolver,则要接收所配置的RolePermissionResolver的每个域必须实现RolePermisionResolverAware接口。这保证了所配置的全局RolePermissionResolver
    如果你不想使用全球RolePermissionResolver或你不想被打扰RolePermissionResolverAware接口,您可以配置一个领域与RolePermissionResolver实例明确(假设有一个JavaBeans-compatible setRolePermissionResolver方法):

    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
    

    相关文章

      网友评论

        本文标题:Shiro授权

        本文链接:https://www.haomeiwen.com/subject/nvtgahtx.html