授权,也可以理解为 访问控制,是管理访问资源的过程。换句话说,就是控制 谁 在应用程序中访问了 什么。
检查权限的示例包括:是否允许用户查看此网页,编辑此数据,查看此按钮或打印到此打印机?这些都决定了用户可以访问的内容。
授权要素
授权有三个核心元素,我们在Shiro中引用了很多次:权限,角色和用户。
权限
Apache Shiro中的权限代表安全策略中最原子的元素(最小且不可分割)。它们可以说是关于用户行为的描述。一个具有良好格式的权限声明,实质上描述了资源以及当Subject
与这些资源交互时可能采取的操作。
下面许可声明的一些示例:
- 打开一个文件
- 查看“/ user / list”网页
- 打印文件
- 删除’jsmith’用户
大多数资源都支持典型的CRUD(创建,读取,更新,删除)操作,以及任何对特定资源类型有意义的操作都可以。基本思想是,权限声明至少要基于资源和操作两部分组成。
在查看权限时,最可能发生的事情可能是权限语句本身并没有指明 谁 可以执行要表示的行为,它们只是对应用程序中可以执行的操作的一个说明。
定义允许 谁(用户)去做 什么(权限)其实是通过某种方式为用户分配权限的操作。这始终由应用程序自己的数据模型完成,并且在不同应用程序之间可能有很大差异。
例如,权限可以在角色中分组,并且该角色可以与一个或多个用户对象相关联。或者某些应用程序可以拥有一组用户,并且可以为一个组分配一个角色,通过传递彼此之间的关联关系,也意味着该组中的所有用户都被隐式授予角色中的权限。
如何为用户授予权限有很多种方式 - 应用程序可以根据具体的需求对其进行建模。
我们关心的是Shiro如何确定Subject
是否被允许做某事。
权限粒度
上面的权限示例指定了对资源类型(文件,客户等)的操作(打开,读取,删除等)。在某些情况下,它们甚至可以指定非常精细的实例级行为 - 例如,使用用户名“jsmith”(实例标识符)‘删除’(操作)‘user’(资源类型)。在Shiro中,您可以准确定义这些语句的精确程度。
我们在Shiro的权限文档中更详细地介绍了权限声明的权限粒度和“级别”。
角色
角色是一个命名实体,通常代表一组行为或责任。这些行为转化为您在软件应用程序中是否被允许执行的操作。角色通常分配给用户帐户,因此通过关联关系,用户是否可以“执行”某个操作其实是归因于各种角色融合在一起的事物。
实际上Shiro支持两种类型的角色(概念级别):
- 隐式角色:大多数人将角色用作隐式构造:即直接通过角色来验证用户有没有操作权限。对于隐式角色,代码级别没有任何内容明确表示“允许角色X执行行为A,B和C”。行为仅由名称暗示。
虽然隐式角色是更简单和最常见的方法,但其可能会带来许多软件维护和管理上的问题。 例如,如果您只想添加或删除角色,或稍后重新定义角色的行为,该怎么办?每次需要进行此类更改时,您都必须返回源代码并更改所有角色对应安全模型的变化!更不用说这会产生的运营成本(重新测试,通过QA,关闭应用程序,使用新的角色检查升级软件,重新启动应用程序等)。 对于非常简单的应用程序来说这可能是好的(例如,可能存在’管理员’角色和’其他人’)。但对于更复杂或可配置的应用程序,这可能是贯穿整个应用程序生命周期中的重要问题,也为您的软件开发带来巨大的维护成本。 |
---|
- 显式角色:显式角色本质上是权限的集合。在这种形式下,应用程序(和Shiro)确切地知道特定角色的具体含义(即能对资源做哪些操作)。因为已知是否可以执行的确切行为,所以不需要猜测或暗示特定角色可以做什么或不能做什么。
Shiro团队主张使用权限和显式角色的组合而不是旧的隐式方法。通过这种方式您可以更好地控制应用程序的安全体验。
用户
用户本质上是指应用程序的“谁”。正如我们之前所述,Subject
实际上是Shiro的“用户”概念。
用户(Subjects
)通过角色或直接权限的关联关系,来决定在应用程序中是否允许执行某些操作。您的应用程序的数据模型确切地定义了Subject
是否被允许执行某些操作。
例如,在您的数据模型中,您可能拥有实际的User
类,并且您可以直接将权限分配给User
实例。或者,您可以将权限直接分配给角色,然后将角色分配给User
,因此通过关联,User
可以传递“拥有”分配给其角色的权限。当然你也可以用“group”(组)来表达权限集合的概念。
您的数据模型确切地定义了授权的功能。 Shiro依靠Realm实现将您的数据模型关联细节转换为Shiro理解的格式。我们将在后文中介绍Realms如何做到这一点。
授权对象
在Shiro执行授权可以通过3种方式完成:
- 以编程方式 - 您可以使用if和else块等结构在java代码中执行授权检查。
- JDK注解 - 您可以将授权附加到Java注解方法
- JSP / GSP TagLibs - 您可以根据角色和权限控制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
方法:
Subject 方法 | 描述 |
---|---|
hasRole(String roleName) | 如果Subject 拥有指定的角色返回true ,否则返回false |
hasRoles(List<String> roleNames) | 返回与传参中的给定的一组角色对应的角色集合,如果需要执行许多角色检查(例如,在自定义复杂视图时),可用作性能增强 |
hasAllRoles(Collection<String> roleNames) | 如果Subject 拥有全部指定的角色时返回true ,否则返回false |
角色断言
检查布尔值
以查看Subject
是否具有角色的替代方法,您可以在执行逻辑之前断言它们是否具有预期的角色。如果Subject
没有预期的角色,则抛出AuthorizationException
。如果它们确实具有预期的角色,则断言将静默执行,逻辑将按预期继续。
Subject currentUser = SecurityUtils.getSubject();
//判断当前用户是否是bankTeller
//如果是的话则打开账户
currentUser.checkRole("bankTeller");
openBankAccount();
这种方法优于hasRole *
方法的一个好处是代码可以更清晰,因为如果当前Subject
不符合预期条件(如果您不想这样做),则不必构建自己的AuthorizationExceptions
。
根据您的需要,您可以调用下面几种面向角色的主题断言方法:
Subject 方法 | 描述 |
---|---|
checkRole(String roleName) | 如果Subject 拥有指定的角色则继续执行`,否则抛出AuthorizationExceptions |
checkRoles(Collection<String> roleNames) | 如果Subject 拥有指定的所有角色则继续执行`,否则抛出AuthorizationExceptions |
checkRoles(String... roleNames) | 与上面的checkRoles方法效果相同,但允许使用Java 5可变长参数。 |
基于权限的授权
如上面我们对角色的概述所述,执行访问控制的更好方法通常是通过基于权限的授权。因为它与应用程序的自身功能(以及应用程序的核心资源上的行为)密切相关,基于权限授权的相关代码会在您的业务功能更改时更改,而不是在安全策略更改时更改。这意味着代码受到的影响远远低于类似的基于角色的授权代码。
权限检查
如果要检查Subject
是否被允许执行某些操作,可以调用各种isPermitted *
方法变体中的任何一种。检查权限主要有两种方法 - 使用基于对象的权限检查或使用基于字符串的权限检查。
基于对象的权限检查
执行权限检查的一种可能方法是实例化继承Shiro的org.apache.shiro.authz.Permission
接口的实例,并将其传递给接受权限实例的* 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?
}
我们看到了一个非常强大的实例级检查权限访问控制的示例,它拥有基于单个数据实例限制行为的能力。
在以下情况下,基于对象的权限会很有用:
- 你想要编译时类型安全
- 您希望保证正确的表示和使用权限
- 您希望显式的控制权限解析逻辑的执行方式。
- 您希望保证权限准确反映应用程序资源(例如,可能会在项目的构建期间根据项目的域模型自动生成权限类)。
根据您的需要,您可以调用下面几种面向对象的权限判断方法:
Subject 方法 | 描述 |
---|---|
isPermitted(Permission p) | 如果Subject 在执行某个操作或访问受保护的资源前通过了指定的Permission 实例检查,则返回true ,否则返回false |
isPermitted(List<Permission> perms) | 返回传参中的给定的一组权限对应的isPermitted 集合,如果需要执行许多权限检查(例如,在自定义复杂视图时),可用作性能增强 |
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?
}
此示例仍显示相同的实例级别的权限检查,但权限的重要部分 - 打印机
(资源类型),print
(操作)和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?
}
虽然上面的String字符串默认使用的是WildcardPermission
格式,但实际上您可以创建自己的字符串格式并根据需要使用它。我们将在下面的Realm Authorization部分介绍如何执行此操作。
基于字符串的权限使用起来更方便,因为您不必强制实现接口,并且简单的字符串通常易于阅读。缺点是您没有类型安全性,如果您需要更复杂的行为,这些行为超出了Strings所代表的范围,您需要基于权限接口实现您自己的权限对象。在实践中,最终大多数Shiro的用户选择基于字符串的方法是为了简化开发。
跟面向对象的权限检查方法一样,您可以调用下面几种字符串类型的权限判断方法:
Subject 方法 | 描述 |
---|---|
isPermitted(String perm) | 如果Subject 在执行某个操作或访问受保护的资源前通过了指定的字符串权限检查,则返回true ,否则返回false |
isPermitted(String... perms) | 返回传参中的给定的一组权限字符串对应的isPermitted 集合,如果需要执行许多权限检查(例如,在自定义复杂视图时),可用作性能增强 |
isPermittedAll(String... perms) | 如果满足所有给定的权限字符串集合则返回true,否则返回false |
权限断言
作为检查布尔值
以查看Subject
是否拥有某种权限的替代方法,您可以在执行逻辑之前断言它们具有预期的权限。如果Subject
不允许,则抛出AuthorizationException,允许的话程序会正常执行。
例如:
Subject currentUser = SecurityUtils.getSubject();
//确认当前用户是否有查看银行账户的权限
Permission p = new AccountPermission("open");
currentUser.checkPermission(p);
openBankAccount();
同样的,使用字符串权限断言:
Subject currentUser = SecurityUtils.getSubject();
//确认当前用户是否有查看银行账户的权限
currentUser.checkPermission("account:open");
openBankAccount();
这种方法优于isPermitted *
方法的一个好处是使代码可以更清晰,因为如果当前Subject
不符合预期条件,则直接抛出原生异常而不必构建自己的AuthorizationExceptions
。
根据您的需要,您可以调用下面几种面向权限的Subject
断言方法:
Subject 方法 | 描述 |
---|---|
checkPermission(Permission p) | 如果Subject 拥有执行某个操作或访问受保护的资源的基于对象的权限实例则继续执行,否则抛出AuthorizationExceptions |
checkPermission(String perm) | 如果Subject 拥有执行某个操作或访问受保护的资源的基于字符串的权限则继续执行,否则抛出AuthorizationExceptions |
checkPermissions(Collection<Permission> perms) | 如果Subject 拥有所有指定的权限则继续执行,否则抛出``AuthorizationExceptions` |
checkPermissions(String... perms) | 与上面的checkPermissions方法效果相同,但入参是基于字符串形式 |
基于注解的授权
除了Subject
的API调用之外,如果您更喜欢基于元数据的授权控制,Shiro还提供了Java 5+注释的集合。
配置
在使用Java注释之前,您需要在应用程序中启用AOP支持。有许多不同的AOP框架对应不同的应用程序(包括AspectJ,Spring应用,Guice应用)。
RequiresAuthentication注释
RequiresAuthentication注解要求当前的Subject
在其当前会话期间经过身份验证之后才能访问或调用被注解标记的类/实例/方法。
例如:
@RequiresAuthentication
public void updateAccount(Account userAccount) {
//这个方法只有在``Subject``经过认证之后才能被调用
...
}
它和下面代码的逻辑相同:
public void updateAccount(Account userAccount) {
if (!SecurityUtils.getSubject().isAuthenticated()) {
throw new AuthorizationException(...);
}
//Subject is guaranteed authenticated here
...
}
RequiresGuest注释
RequiresGuest注解要求当前Subject
为访客用户,即,不需要从先前会话中对其进行身份验证或记住也可以访问或调用带注释的类/实例/方法。 例如:
@RequiresGuest
public void signUp(User newUser) {
//这个方法只有在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
...
}
RequiresPermissions注释
RequiresPermissions注解要求当前Subject
有一个或多个权限才能执行被注解标记的方法。
例如:
@RequiresPermissions("account:create")
public void createAccount(Account 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
...
}
RequiresRoles权限
RequiresRoles注解要求当前Subject
具有所指定的角色。如果没有则不会执行该方法并抛出AuthorizationException。
例如:
@RequiresRoles("administrator")
public void deleteUser(User user) {
//这个方法只有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
,其通常是在当前会话期间进行身份验证或者从先前会话的“RememberMe”服务中记住。
例如:
@RequiresUser
public void updateAccount(Account account) {
//这个方法只有被已确认为“user”(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
...
}
JSP Taglib授权
Shiro提供了一个标记库,用于根据Subject
状态控制JSP / GSP页面输出。 我们将在Web章节的JSP / GSP标记库部分对此进行介绍。
授权顺序
现在我们已经看到了如何根据当前Subject
执行授权,让我们看看每当进行授权调用时Shiro内部会发生什么。
我们从Apache Shiro的体系架构章节中获取了我们之前的架构图,下面只留下了与授权相关的组件。每个数字代表授权操作中的一个步骤:
步骤1:应用程序或框架代码调用例如Subject
hasRole *
,checkRole *
,isPermitted *
或checkPermission *
之类的方法变体并传入需要的权限或角色表示。
步骤2:Subject
实例,通常是由DelegatingSubject
(或其子类)实现,通过委托给应用程序的SecurityManager
对象来调用相同的hasRole *
,checkRole *
,isPermitted *
或checkPermission *
方法变体(securityManager
实现org.apache.shiro.authz.Authorizer
接口,定义所有特定于Subject
的授权方法)。
步骤3:SecurityManager
作为一个基本的“伞形”组件,通过调用授权者各自的hasRole *
,checkRole *
,isPermitted *
或checkPermission *
方法,将其转发/委托给其内部org.apache.shiro.authz.Authorizer
实例。默认情况下,授权方实例是ModularRealmAuthorizer
实例,它支持在授权操作期间协调一个或多个Realm
实例。
步骤4:检查每个配置的Realm
以查看它是否实现了相同的Authorizer
接口。如果是这样,则调用Realm
自己的hasRole *
,checkRole *
,isPermitted *
或checkPermission *
方法。
ModularRealmAuthorizer
如上面所述,Shiro SecurityManager
实现默认使用ModularRealmAuthorizer
实例。 ModularRealmAuthorizer
同样支持具有单个Realm
的应用程序以及具有多个Realm
的应用程序。
对于任何授权操作,ModularRealmAuthorizer
将迭代其内部Realms
集合,并以迭代顺序与每个集合进行交互。每个Realm
交互功能如下:
- 如果
Realm
本身实现了Authorizer
接口,则会调用其各自的Authorizer
方法(hasRole *
,checkRole *
,isPermitted *
或checkPermission *
)。- 如果
Realm
的方法导致异常,则将AuthorizationException
异常抛给Subject调用者。这会使授权过程发生短路,并且不会继续查询剩余的Realm
。 - 如果
Realm
的方法是hasRole *
或isPermitted *
,它会返回一个布尔值,并当返回值为true时立即返回,并且剩余的Realms
都会被短路。此行为作为性能增强而存在,通常如果一个Realm
允许,则暗示Subject
被允许使用。这有利于安全策略的实现:默认情况下禁止一切,并且明确规定使用最安全的安全策略。
- 如果
- 如果
Realm
没有实现Authorizer
接口,则忽略它。
Realm授权顺序
需要指出的是,与身份验证相同,ModularRealmAuthorizer
将以迭代的顺序与Realm
实例进行交互。 ModularRealmAuthorizer
可以访问SecurityManager
上配置的Realm
实例。在执行授权操作时,它将遍历该集合,并且对于每个实现Authorizer
接口的Realm
,调用其Authorizer
方法(例如hasRole *
,checkRole *
,isPermitted *
或checkPermission *
)。
全局PermissionResolver
执行基于字符串
的权限检查时,默认情况下大部分Shiro的Realm
实现会在执行判断权限逻辑之前首先将此String转换为实际的Permission
实例。
这是因为权限是基于隐含逻辑而不是直接利用相等性检查来评估的(有关隐含与相等性的更多信息,请参阅后面的权限文档)。在代码中隐含逻辑会比通过字符串更好地表示。因此,大多数Realms需要将提交的权限字符串转换或解析为相应的Permission
实例。
为了帮助实现这种转换,Shiro提供了PermissionResolver
的概念。Shiro中实现了Authorizer
接口的Realm
会使用PermissionResolver
来解析String
格式的权限:当在Realm
上调用一个方法时,它将使用PermissionResolver
将字符串转换为Permission
实例,然后执行检查。
所有Shiro中Realm
的实现默认为内部WildcardPermissionResolver
,它采用Shiro的WildcardPermission
字符串格式。
如果为了支持您自己的Permission
字符串语法而选择创建自己的PermissionResolver
实现,并且您希望所有已配置的Realm
实例都支持该语法,您可以为所有可配置的Realms
设置全局PermissionResolver
。
例如,在shiro.ini中:、
globalPermissionResolver = com.foo.bar.authz.MyPermissionResolver
...
securityManager.authorizer.permissionResolver = $globalPermissionResolver
...
如果要配置全局PermissionResolver,建议要使用PermissionResolver的每个Realm都必须实现PermisionResolverAware接口。这可以保证每个Realm都可以中继到全局PermissionResolver配置。
如果您不想使用全局PermissionResolver
或者您不想使用PermissionResolverAware
接口,则可以显式的配置具有PermissionResolver
实例的指定Realm
(假设存在与JavaBeans兼容的setPermissionResolver方法):
permissionResolver = com.foo.bar.authz.MyPermissionResolver
realm = com.foo.bar.realm.MyCustomRealm
realm.permissionResolver = $permissionResolver
...
配置全局RolePermissionResolver
RolePermissionResolver
在概念上与PermissionResolver
类似,能够表示Realm
执行权限检查所需的Permission
实例。
但是,与PermissionResolver
的主要区别在于输入String
是角色名称,而不是权限字符串。
当需要将角色名称转换为一组具体的Permission
实例时,Realm
可以在内部使用RolePermissionResolver
。
这是一个特别有用的功能,用于支持可能没有权限概念的遗留或不灵活的数据源。
例如,许多LDAP目录存储角色名称(或组名称),其不支持将角色名称与具体权限关联,因为它们没有“权限”概念。基于Shiro的应用程序可以使用存储在LDAP中的角色名称,但实现RolePermissionResolver
接口可将LDAP名称转换为一组显式权限,以执行首选显式访问控制。权限关联将存储在另一个数据存储中,可以是本地数据库。
因为将角色名转换为权限这一概念非常特定于应用程序,所以Shiro的默认Realm
实现不使用它们。
globalRolePermissionResolver = com.foo.bar.authz.MyPermissionResolver
...
securityManager.authorizer.rolePermissionResolver = $globalRolePermissionResolver
...
如果您不想使用全局的RolePermissionResolver
或者您不想使用RolePermissionResolverAware
接口,则可以显式配置具有RolePermissionResolver
实例的Realm
(假设存在与JavaBeans兼容的``setRolePermissionResolver`方法):
自定义授权
如果您的应用程序使用多个Realm
来执行授权,而ModularRealmAuthorizer
的默认基于简单迭代的短路授权实现不能满足您的需要,您可以根据实际需求创建自定义的Authorizer
并在SecurityManager
属性中申明。
例如,在shiro.ini中:
[main]
...
authorizer = com.foo.bar.authz.CustomAuthorizer
securityManager.authorizer = $authorizer