当前位置: 首页 > 图灵资讯 > 技术篇> Apache Shiro 授权

Apache Shiro 授权

来源:图灵教育
时间:2023-05-19 09:07:30

Apache Shiro 授权 授权,即访问控制,是管理资源访问的过程,换句话说,“谁”有权在一个程序中访问“什么”。 授权的例子是:用户是否允许查看页面、编辑数据、查看按钮或从打印机打印?这些决定是用户可以访问的决定。 授权组件 我们经常在Shiro中使用授权三个核心组件:权限(permissions)、角色(roles)和用户(users)。 权限(Permissions) Apache是权限 Shiro中最基本的安全策略组成部分,它们是一组关于行为的基本指令,以清楚地表示在一个程序中可以做什么。当一个Subject与这些资源互动时,必须描述资源和什么动作才能执行一个很好的定义权限指令。 以下是一些权限指令的例子: 打开文件; 查看“/user/list”页面; 打印文档; 删除“Jsmith”用户。 大多数资源支持基本CRUD(create,read,update,delete)操作,但对于特定的资源类型,任何动作都可以。权限设置最基本的思想是在资源和行动的基础上设置最小的权限指令。 当你看到权限时,最重要的是,如果你意识到一个权限指令不是描述“谁”可以执行这个动作,而是描述“什么”可以做的指令。 权限只描述行为 权限指令只描述行为(与资源相关的行为),并不关心“谁”能够执行这一行为。 “谁”(用户)被允许做“什么”(权限)的定义通常取决于程序的数据模型,并且在程序中经常发生变化。 例如,一组权限可以归因于一个角色,角色与一个或多个用户对象相关,或者一些程序可以有一组用户,一组可以指定一个角色,这里的关系将被传递,也就是说,组中的用户隐含被赋予角色的权限。 将权限赋予用户的方法有很多——程序根据需要决定如何设计。 后来我们讨论了shiro如何判断subject是否被允许。 权限粒度 上述权限示例均为资源(门、文件、客户等)指定的动作(打开、阅读、删除等)。在某些场景中,他们还将指定非常细粒度的“实例级别”行为,如“删除”(delete)名为“Jsmith“用户”(资源类型)(实例标识),在Shiro中,您可以准确地将指令定义到您可以细化的程度。 在Shiro的Permissions文档中,我们详细讨论了权限粒度和权限指令的“等级”。 角色 角色是代表一组行为或职责的实体名称,这些行为在程序中转化为你能做或不能做的事情。角色通常被赋予用户帐户。关联后,用户可以“做”属于不同角色的事情。 Shiro支持两种有效的角色指定方法。 权限隐含在角色中:大多数用户将权限隐含在创建的角色中:程序只在角色名称中隐含一组行为(即权限)。使用时,软件级别不会说“角色允许执行A”、B和C而是将行为隐含在一个单独的角色名称中。 潜在的安全隐患 虽然这是一种非常简单和常用的方法,但隐含的角色可能会增加软件的维护成本和管理问题。 例如,如果你想添加或删除一个角色,或重新定义一个角色怎么办?您必须重新打开代码,修改更改后的所有角色检测,并且每次都需要这样做,这还没有提到执行成本(重新测试、质量验证、关闭程序、升级软件、重启程序等)。 这种方法可能适用于简单程序(例如只有一个)admin'角色和'everyone else但在复杂的程序中,这将成为您程序生命周期中的一个主要问题,并将为您的软件带来巨大的维护成本。 明确的角色指定权限:明确的角色指定权限本质上是一组权限指令的名称集,在这种形式下,程序(和shiro)准确地知道特定的角色是什么意思,因为它确切知道行为是否可以执行,而不是猜测特定的角色可以或不能做什么。 shiro团队提倡使用权限和明确替代角色指定权限的原始方法,以便您能够更强地控制程序安全。 用户 一个用户本质上是程序中的“谁”,正如我们前面提到的,Subject实际上是shiro的“用户”。 用户(Subjects)程序数据模型确定了Subject是否允许通过与角色或权限关联来执行程序中的特定动作。 例如,在你的数据模型中,你定义了一个普通的用户类别,并直接为它设置权限,或者你只是直接为角色设置权限,然后将用户与角色联系起来。通过这种联系,用户“拥有”角色的权限,或者你也可以通过“组”的概念来完成它,这取决于你的程序设计。 数据模型定义了如何授权。Shiro依靠Realm将您的数据模型关联转换为Shiro可以理解的内容。我们稍后将讨论Realms。 最后,Realm和你的数据源(RDBMS,LDAP等。)做交流,Realm用来告诉Shiro是否有角色或权限,你可以完全控制如何创建和定义你的授权模型。 授权对象 在Shiro执行授权有三种方式: 程序代码-您可以使用类似于if和else的结构在您的JAVA代码中执行权限检查。 JDK 注释-您可以在您的JAVA方法上添加权限注释 JSP/GSP标签-您可以根据角色和权限控制JSP或GSP页面输出内容。 在程序中检查授权 在程序中直接授权当前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? Grey it out? }

以下是您可以根据需要调用的函数: hasRole(String roleName) 若Subject指定特定角色返回真实,则返回假; hasRoles(List<String> roleNames) 返回与参数顺序对应的hasrole结果数组,多个角色需要一次检测时非常有用(比如定制复杂的视图)。 hasAllRoles(Collection<String> roleNames) 假如Subject有所有角色返回真实,否则返回假。 角色判断 另一种方法是检测Subjet是否指定为某个角色,如果Subject不是所需的角色,您可以在代码执行前简单地判断它们是否是所需的角色, Authorizationexception异常将被抛出。如果是要求的角色,判断将安静地执行以下逻辑,并按预期顺序执行以下逻辑。 角色判断 另一种方法是检测Subjet是否指定为某个角色,如果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不符合要求,您不需要建立自己的authorizationexceptions异常(如果您不想这样做)。 以下是您可以根据需要调用的函数: checkRole(String roleName) 若Subject被指定为特定角色,则安静返回,否则抛出Authorizationexception异常; checkRoles(Collection<String> roleNames) 如果Subject被指定为所有特定的角色,则静静地返回,否则抛出Authorizationexception异常; checkRoles(String... roleNames) 与上面的checkRoles效果相同,但允许Java5变参形式。 授权基于权限 就像我们上面在角色概述中提到的,通过授权基于权限执行访问控制是更好的方法。 授权基于权限 就像我们上面在角色概述中提到的,通过授权基于权限执行访问控制是更好的方法。授权基于权限,因为其与程序功能(以及程序核心资源上的行为)紧密联系,基于权限授权的源代码在程序功能改变时才需要改变,而与安全策略无关。这意味着对代码的影响不如同一个角色的授权。 权限检查 如果你想检查Subject是否允许做某事,你可以调用ispermitted*方法的变形,主要有两种方法来检查授权——基于对象的权限实例和基于字符串的权限表示。 检查基于对象的权限 执行权限检查的一种方法是实例化Shirooorg.apache.shiro.authz.*ispermitted方法Permision接口并将其传递给接收权限实例。 例如,假设以下情况:办公室里只有一台标有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? Grey it out? }

在这个例子中,我们还看到了一个非常强大的实例级访问控制检查——限制单独数据实例行为的能力。 基于对象的权限对以下情况非常有用: 希望编译期安全; 希望正确引用和使用权限; 希望明确控制权限判断逻辑(称为权限隐含逻辑,基于权限界面的implies方法)的执行方法; 希望确保权限能够正确反映程序资源(例如,当一个对象域模型创建一个对象时,权限类可能会自动产生)。 以下是您可以根据需要调用的函数: isPermitted(Permission p) 如果Subject允许执行特定权限实例的综合指定动作或资源访问权返回真实性,否则返假; isPermitted(List<Permission> perms) 按参数顺序返回ispermitted的结果数组,若许多权限需要检查时非常有用(如定制复杂视图); isPermittedAll(Collection<Permission> perms) 假如Subject有指定的所有权返回真实,否则返回假。 检查基于字符串的权限 虽然基于对象的权限检查非常有用(编译期安全、行为保证、定制隐含逻辑等),但有时在许多程序中感觉有点笨重一种选择是用普通字符串代表权限。 例如,对于上述打印权限的例子,我们可以使用字符串权限进行检查,以达到相同的结果:

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

本例还实现了实例级别的权限检查,但所有主要权限部件--printer(资源类型)、print(动作)、laserjet4400n(实例ID)都表现为字符串。 上面的例子显示了一个特殊形式的字符串,定义为Shiro org.apache.shiro.authz.permission.在WildcardPermision中,它适合大多数用户。 上面的代码块基本上是下面代码的缩写:

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

在Shiro的Permission文档中,将深入讨论WildcardPermision令牌的形式和构成选项。 上面的字符串使用默认的WildcardPermision格式。事实上,您可以创建和使用您自己的字符串格式。我们将讨论如何在下面的Realm授权章节中这样做。 基于字符串的权限有利的一面是你不需要实现一个接口,简单的字符串也很容易读取,不利的一面是不确保类型的安全,当你需要定义更复杂的行为超出字符串的性能,你仍然需要使用权限接口来实现你自己的权限对象。事实上,大多数Shiro终端用户选择基于字符串的方式,因为它很简单,但最终你的程序需求决定了哪种方法会更好。 与基于对象的权限检查方法一样,字符串权限检查的函数如下: isPermitted(String perm) 若允许Subject执行字符串表达的动作或资源访问权限,则返回真实性,否则返假; isPermitted(String... perms) 按参数顺序返回ispermitted的结果数组,当需要检查许多字符串权限时非常有用(如定制复杂的视图); isPermittedAll(String... perms) 当Subject拥有所有字符串定义的权限时,返回真实,否则,返回假期。 权限判断 另一种检查Subject是否允许做某事的方法是在逻辑执行之前简单判断它们是否有所需的权限。如果不允许,Authorizationexception异常被抛出。如果允许,判断将安静地执行以下逻辑,并按预期顺序执行以下逻辑。 权限判断 另一种检查Subject是否允许做某事的方法是在逻辑执行之前简单判断它们是否有所需的权限。如果不允许,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();

ispermited* 与该方法相比,该方法的优点是代码更清晰。如果当前的Subject不符合条件,您不必创建自己的Authorizationexceptions异常(如果您不想这样做)。 以下是您可以根据需要调用的函数: checkPermission(Permission p) 如果允许Subject执行特定权限实例指定的动作或资源访问,则安静返回,否则抛出Authorizationexception异常。 checkPermission(String perm) 如果允许执行权限字符串指定的动作或资源访问Subject,则安静返回,否则抛出Authorizationexception异常。 checkPermissions(Collection<Permission> perms) 如果允许Subject执行所有权限实例指定的动作或资源访问,则安静返回,否则抛出Authorizationexception异常。 checkPermissions(String... perms) 就像上面的checkPermissions效果一样,只使用字符串权限类型。 授权基于注释 如果你更喜欢授权基于注释控制,除了Subject的API之外,Shiro提供了一个Java5的注解集。 配置 在使用JAVA注释之前,您需要在程序中启动AOP支持,因为有很多AOP框架,所以不幸的是,在程序中没有标准的AOP方法。 关于AspectJ,您可以查看我们的AspectJ sample application 关于Spring,您可以查看Spring Integration文档; 关于Guice,您可以查看我们的 Guice Integration文档; Requiresauthentication Requiresauthentication表示在访问或调用被注解的类/实例/方法时,要求Subject在当前的session中已经被验证。 例如:

@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注释表示要求当前Subject是一个“访客”,也就是,在访问或调用被注解的类/实例/方法时,他们没有被认证或者在被前一个Session记住。 例如: @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 注解 RequiresPermissions 注释要求Subject在执行注释方法时具有一个或多个相应的权限。 例如: @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 createAcc /this method will only be invoked by an administrator  ... } RequiresRoles 注解 RequiresPermissions 注释表示,Subject在执行注释方法时需要具备所有角色,否则将抛出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*在访问或调用被注解的类/实例/方法时,注解表示需要注解目前,Subject是一个程序用户,“程序用户”是一个已知身份的Subject,或在当前的Session中被验证或记住在以前的Session中。 例如: @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标签库 Shiro提供了一个控制JSP/GSP页面输出的标签库,这将在Web文档中的JSP/GSP标签库中讨论。 授权序列 现在我们已经看到了如何授权当前的Subject,让我们知道Shiro在一个授权命令中发生了什么。 我们仍然使用前面Architecture章节中的架构图,左边只有与授权相关的组件是亮点,每个数字代表授权操作中的一个步骤: 第一步:程序或框架代码调用Subjecthasrole*、checkRole*、 isPermitted*或者 checkPermission*方法,传递所需的权限或角色。 第二步:Subject的例子通常是一个DelegatingSubject(或子类),通过调用securityManager和每个hasrole*、checkRole*、 isPermitted*或者checkPermission*基本相同的方法将权限或角色传递给程序的securitymanager。 第3步:SecurityManager,通过调用authorizer的各hasrole,基本的安全组件*、checkRole*、 isPermitted*或者checkPermission*方法将权限转让/委托给其内部org.apache.shiro.authz.Authorizer实例,它负责协调授权操作中的一个或多个Realm实例。 第四步是检查每个配置的Realm是否实现相同的Authorizer接口。如果是这样,Realm自己的Reale*、checkRole*、 isPermitted*或调用checkPermission*方法。 ModularRealmAuthorizer 前面提到的,Shiro 默认情况下,Securitymanager使用Modularrealmauthorizer实例,Modularrealmauthorizer实例支持使用一个realm程序和多个realm程序。 对于任何授权操作,Modularrealmauthorizer将在其内部Realm集中迭代(iterator),按迭代(iteration)顺序与每个Realm交互,以下方法与每个Realm交互: 1.如果Realm实现了Authorizer接口,调用各自的授权方法(hasRole*、 checkRole*、isPermitted*或 checkPermission*)。 1.如果Realm函数的结果是exception,那么从Subject调用器Authorizationexception衍生的exception将切断授权过程,剩余的授权Realm将不执行。 2.如果Realm的方法是hasrole*或ispermited*并返回真实值,则真实值将立即返回,剩余Realm将短路。这种做法被用作性能增强。在Realm判断允许后,它隐含了Subject是允许的。 2.如果Realm的方法是hasrole*或ispermited*并返回到真实值,则真实值将立即返回,剩余的Realm将短路。这种做法作为一种性能增强,在Realm判断允许后,意味着Subject是允许的。它支持最安全的安全策略:默认情况下禁止一切,并明确规定允许的事项。 2.如果Realm没有实现Authorizer接口,将被忽略。 Realm授权顺序 就像验证一样,需要指出一个非常重要的点(authentication)同样,Modularrealmauthorizer按迭代(iteration)顺序与Realm交互。 Modularrealmauthorizer拥有Securitymanager配置的Realm实例的入口。当执行授权操作时,它将在整个集合中迭代(iteration),实现Authorizer接口的Realm,调用Realm各自的Authorizer方法(如 hasRole*、 checkRole*、 isPermitted*或 checkPermission*)。 Permisionresolver配置全局 当执行基于字符串的权限检查时,大多数Shiro默认的Realm将在执行权限隐含逻辑之前将字符串转换为常用的权限实例。 Permisionresolver配置全局 当执行基于字符串的权限检查时,大多数Shiro默认的Realm将在执行权限隐含逻辑之前将字符串转换为常用的权限实例。 这是因为权限被认为是基于隐含逻辑而不是相等检查(查看Permission章节,了解更多隐含和相等对比)。用代码表示隐含逻辑比用字符串对比好。因此,大多数Realm需要将提交的权限字符串转换为相应的权限实例。 为此转换目的,Shiro支持Permissionresolver,大多数Shiro Realm使用PermissionResolver来支持它们在Authorizer接口中基于字符串权限的实现:当这些方法在Realm上被调用时,使用Permissionresolver将字符串转换为权限实例并进行检查。 所有的Shiro Realm默认使用内部WildcardPermisionResolver,使用ShiroWildcardPermision字符串格式。 如果你想创建自己的Permisionresolver, 实现,比如你想创建自己的权限字符串语法,希望所有配置的Realm实例都支持这种语法。您可以将您的PermisionResolver设置为所有Realm。 如,在shiro.ini中: shiro.ini globalPermissionResolver = com.foo.bar.authz.MyPermissionResolver ... securityManager.authorizer.permissionResolver = $globalPermissionResolver ... PermissionResolverAware 若要配置全局Permissionresolver,每一个能读取Permissionresolver配置的Realm都必须实现Permissionresolveraware接口,这确保配置Permissionresolver的实例能够传递给每一个支持这种配置的Realm。 如果你不想使用全局Permisionresolver,或者你不想被Permisionresolveraware接口所困扰,您可以清楚地为单个Realm配置PermisionResolver接口(可以看作是Javabean的SetpermisionResolver方法): permissionResolver = com.foo.bar.authz.MyPermissionResolver realm = com.foo.bar.realm.MyCustomRealm realm.permissionResolver = $permissionResolver ... RolePermisionresolver配置全局 类似于Permissionresolver,Realm所需的权限实例,RolePermisionResolver有能力表示执行权限检查。 最重要的区别是接收的字符串是角色名,而不是权限字符串。 最重要的区别是接收的字符串是角色名,而不是权限字符串。 RolePermisionResolver在需要时被Realm用来将角色名称转换为一组明确的权限实例。 这是非常有用的,它支持没有权限概念的遗留或不灵活的数据源。 例如,由于没有权限的概念,许多LDAP目录存储角色名称(或组名),但不支持角色名称和权限的结合。使用shiro的程序可以使用存储在LDAP中的角色名,但需要实现RolePermisionresolver将LDAP名转换为一组确切的权限,以实现明确的访问控制。权限的联合将存储在其他数据存储中,如本地数据库。 Shiro默认的Realm没有使用它们,因为这种将角色名转换为权限的概念是特定的。 但是,如果你想创造自己 RolePermissionResolver 并且希望用它配置多个Realm 实现,你可以做你的 RolePermisionResolver设置为全局。 shiro.ini globalRolePermissionResolver = com.foo.bar.authz.MyPermissionResolver ... securityManager.authorizer.rolePermissionResolver = $globalRolePermissionResolver ... RolePermissionResolverAware 若要配置全局RolePermisionResolver,每一个能读取RolePermisionResolver配置的Realm都必须实现RolePermisionResolveraware接口,这样才能保证配置RolePermisionResolver的Realm实例能够传递给每一个支持这种配置的Realm。 如果你不想使用RolePermisionresolver 或者你不想麻烦实现RolepermisionResolveraware接口,你可以单独为Realm配置RolePermisionResolver(可以看作是Javabean的setrolepermisionResolver方法)。 rolePermissionResolver = com.foo.bar.authz.MyRolePermissionResolver realm = com.foo.bar.realm.MyCustomRealm realm.rolePermissionResolver = $rolePermissionResolver ... 定制Authorizer 如果您的程序使用超过一个Realm执行授权,而Modularrrealmauthorizer默认的简单迭代(iteration)、短路授权行为不能满足您的需要,您可以创建自己的Authorizer,并将其分配给相应的SecurityManager。 例如,在shiro.ini中: [main] ... authorizer = com.foo.bar.authz.CustomAuthorizer securityManager.authorizer = $authorizer