授权,亦即访问控制,是管理资源访问的过程,换言之,也就是控制在一个程序中“谁”有权利访问“什么”。
授权的例子有:是否允许这个用户查看这个页面,编辑数据,看到按钮,或者从这台打印机打印?这些决定是一个用户可以访问什么的决断。
Elements of Authorization 元素
授权有三个核心元素,在 Shiro 中我们经常要用到它们:权限(permissions)、角色(roles)和用户(users)。
Permissions
权限是 Apache Shiro 中安全策略最基本的元素,只描述行为, 权限粒度(Permission Granularity)可以是针对资源(门、文件、客户等)指定的动作(打开、读、删除等),也可以指定非常细粒度的“实例级别”行为–例如,“删除”(delete)名为“Jsmith”(实例标识)的“用户”(资源类型),在 Shiro 中,你可以精确定义指令到你所能细化到的程度。后面会详细解释权限
Roles
角色是一个实体名,代表一组行为或职责,这些行为在程序中转换为你可以或者不能做的事情。角色通常赋给用户帐号,关联后,用户就可以“做”属于不同角色的事情。
有两种有效的角色指定方式,Shiro 都支持。
- 基于角色的访问控制(角色权限不明确,例如一个方法判断这个人是不是经理,是经理就有权限(经理有哪些权限呢不清楚))
- 基于资源的权限管理(角色被具体赋予了某些权限)
读一下这个文章:新的RBAC:基于资源的权限管理(Resource-Based Access Control),这篇文章深入讨论了使用权限和明确为角色指定权限代替旧的将权限隐含于角色中方法的好处(以及对源代码的影响)。
Users用户
一个用户本质上是程序中的“谁”,如同我们前面提到的,Subject 实际上是 shiro 的“用户”。(用户可以被赋予多个角色,例如一个用户既是公司员工也是部门经理)
用户(Subjects)通过与角色或权限关联确定是否被允许执行程序内特定的动作,程序数据模型确切定义了 Subject 是否允许做什么事情。
例如,在你的数据模型中,你定义了一个普通的用户类并且直接为其设置了权限,或者你只是直接给角色设置了权限,然后将用户与该角色关联,通过这种关联,用户就“有”了角色所具备的权限,或者你也可以通过“组”的概念完成这件事,这取决于你程序的设计。
数据模型定义了如何进行授权,Shiro 依赖一个 [Realm](https://www.ctolib.com/docs/sfile/apache-shiro-reference/II. Core 核心/7. Realms.md) 实现将你的数据模型关联转换成 Shiro 可以理解的内容,我们将稍后讨论 Realms。
最终,是 [Realm](https://www.ctolib.com/docs/sfile/apache-shiro-reference/II. Core 核心/7. Realms.md) 与你的数据源(RDBMS,LDAP等)做交流,Realm 用来告知Shiro 是否角色或权限是否存在,你可以完全控制你的授权模型如何创建和定义。
Authorizing Subjects 授权对象
在 Shiro 中执行授权可以有三种途径:
- 程序代码–你可以在你的 JAVA 代码中执行用类似于 if 和 else 的结构来执行权限检查。
- JDK 注解–你可以在你的 JAVA 方法上附加权限注解
- JSP/GSP 标签–你可以基于角色和权限控制 JSP 或 GSP 页面输出内容。
Programmatic Authorization 程序中检查授权
直接在程序中为当前 Subject 实例检查授权可能是最简单也最常用的方法。
Role-Based Authorization 基于角色的授权
如果你要基于简单/传统的角色名进行访问控制,你可以执行角色检查:
Role Checks 角色检查
如果你想简单地检查一下当前Subject是否拥有一个角色,你可以在一个实例上调用 hasRole* 方法。
例如,查看一个 Subject 是否有特定(单独)的角色,你可以调用subject.hasRole(roleName)方法,做出相应的反馈。
1 | Subject currentUser = SecurityUtils.getSubject(); |
下面是你可以根据需要调用的函数:
Subject 方法 | 描述 |
---|---|
hasRole(String roleName) |
如果Subject指定了特定的角色返回真,否则返回假; |
hasRoles(List roleNames) |
返回一个与参数顺序相对应的hasRole结果数组,当一次有多个角色需要检测时非常有用(如定制一个复杂的视图) |
hasAllRoles(Collection roleNames) |
如果Subject具备所有角色返回真,否则返回假。 |
Role Assertions 角色判断
还有另一个方法检测 Subjet 是否是指定为某个角色,你可以在的代码执行之前简单判断他们是否是所要求的角色,如果 Subject 不是所要求的角色, AuthorizationException 异常将被抛出,如果是所要求的角色,判断将安静地执行并按期望顺序执行下面的逻辑。
例如:
1 | Subject currentUser = SecurityUtils.getSubject(); |
与 hasRole* 方法相比,这种方法的好处在于代码更为清晰,如果当前Subject 不满足所需条件你不需要建立你自己的AuthorizationExceptions 异常(如果你不想那么做)。
下面是你可以根据需要调用的函数:
Subject 方法 | 描述 |
---|---|
checkRole(String roleName) |
如果Subject被指定为特定角色则安静地返回否则抛出AuthorizationException异常; |
checkRoles(Collection roleNames) |
如果Subject被指定了所有特定的角色则安静地返回否则抛出AuthorizationException异常; |
checkRoles(String... roleNames) |
和上面的checkRoles具有相同的效果,但允许Java5的变参形式。 |
Permission-Based Authorization 基于权限的授权
就像我们上面在角色概述中提到的,通过基于权限的授权执行访问控制是更好的方法。基于权限的授权,因为其与程序功能(以及程序核心资源上的行为)紧密联系,基于权限授权的源代码在程序功能改变时才需要改变,而与安全策略无关。这意味着与同样基于角色的授权相比,对代码的影响更少。
Permission Checks 权限检查
如果你希望检查一个 Subject 是否允许做某件事情,你可以调用isPermitted* 方法的变形,有两种主要方式检查授权–基于对象的权限实例和基于字符串的权限表示。
Object-based Permission Checks 基于对象的权限检查
执行权限检查的一种方法是实例化一个Shiro的org.apache.shiro.authz.Permission接口并且将它传递给接收权限实例的*isPermitted 方法。
例如,假设一下以下情景:办公室里有一台唯一标识为 laserjet4400n 的打印机,在我们向用户显示打印按钮之前,软件需要检查当前用户是否允许用这台打印机打印文档,检查权限的方式会是这样:
1 | Permission printPermission = new PrinterPermission("laserjet4400n", "print"); |
在这个例子中,我们同样看到了一个非常强大的实例级别的访问控制检查–在单独数据实例上限制行为的能力。
基于对象的权限对下列情况非常有用:
- 希望编译期类型安全;
- 希望确保正确地引用和使用的权限;
- 希望对权限判断逻辑(称作权限隐含逻辑,基于权限接口的 implies方法)执行方式进行明确控制;
- 希望确保权限正确地反映程序资源(例如,在一个对象域模型上创建一个对象时,权限类可能自动产生)。
下面是你可以根据需要调用的函数:
Subject 方法 | 描述 |
---|---|
isPermitted(Permission p) |
如果Subject允许执行特定权限实例综合指定的动作或资源访问权返回真,否则返回假; |
isPermitted(List perms) |
按参数顺序返回isPermitted的结果数组,如果许多权限需要检查时非常有用(如定制一个复杂的视图) |
isPermittedAll(Collection perms) |
如果Subject拥有指定的所有权限返回真,否则返回假。 |
String-based permission checks 基于字符串的权限检查
虽然基于对象的权限检查很有用(编译期类型安全,对行为担保,定制隐含逻辑等),但在许多程序里有时候感觉有点笨重,另一种选择是用普通的字符串来代表权限。
例如,对于上面打印权限的例子,我们可以使用字符串权限检查达到同样的结果
1 | Subject currentUser = SecurityUtils.getSubject(); |
这个例子同样实现了实例级别的权限检查,但是所有主要权限部件–printer(资源类型)、print(动作)、laserjet4400n(实例ID)都表现为一个字符串。
上面的例子展示了一种以冒号分割的特殊形式的字符串,定义于Shiro的 org.apache.shiro.authz.permission.WildcardPermission中,它适合大多数用户的需求。
上面的代码块基本上是下面这段代码的缩写:
1 | Subject currentUser = SecurityUtils.getSubject(); |
WildcardPermission 令牌形式和构成选项将在 Shiro 的 [Permission](https://www.ctolib.com/docs/sfile/apache-shiro-reference/II. Core 核心/6.1. Permissions 权限.md)文档中深入讨论
上面的字符串使用默认的 WildcardPermission 格式,实际上你可以创造并使用你自己的字符串格式,我们将在下面 Realm 授权章节讨论如何这样做。
基于字符串的权限有利的一面在于你不需要实现一个接口而且简单的字符串也非常易读,而不利的一面在于不保证类型安全,而且当你需要定义超出字符串表现能力之外的更复杂的行为时,你仍旧需要利用权限接口实现你自己的权限对象。实际上,大部分 Shiro 的终端用户因为其简单而选择基于字符串的方式,但最终你的程序需求决定了哪一种方法会更好。
和基于对象的权限检查方法一样,下面是字符串权限检查的函数:
Subject 方法 | 描述 |
---|---|
isPermitted(String perm) |
如果Subject被允许执行字符串表达的动作或资源访问权限,返回真,否则返回假; |
isPermitted(String... perms) |
按照参数顺序返回isPermitted的结果数组,当许多字符串权限需要检查时非常有用(如定制一个复杂的视图时); |
isPermittedAll(String... perms) |
当Subject具备所有字符串定义的权限时返回真,否则返回假。 |
Permission Assertions 权限判断
另一种检查 Subject 是否被允许做某件事的方法是,在逻辑执行之前简单判断他们是否具备所需的权限,如果不允许,AuthorizationException异常被抛出,如果是允许的,判断将安静地执行并按期望顺序执行下面的逻辑。
例如:
1 | Subject currentUser = SecurityUtils.getSubject(); |
或者,同样的判断,可以用字符串形式:
1 | Subject currentUser = SecurityUtils.getSubject(); |
与 isPermitted* 方法相比较,这种方法的优势是代码更为清晰,如果当前Subject 不符合条件,你不必创建你自己的 AuthorizationExceptions 异常(如果你不想那么做)。
下面是你可以根据需要调用的函数:
Subject 方法 | 描述 |
---|---|
checkPermission(Permission p) |
如果Subject被允许执行特定权限实例指定的动作或资源访问,安静地返回,否则抛出AuthorizationException异常。 |
checkPermission(String perm) |
如果Subject被允许执行权限字符串指定的动作或资源访问,安静地返回,否则抛出AuthorizationException异常。 |
checkPermissions(Collection perms) |
如果Subject被允许执行所有权限实例指定的动作或资源访问,安静地返回,否则抛出AuthorizationException异常。 |
checkPermissions(String... perms) |
和上面的checkPermissions效果一样,只是使用字符串权限类型。 |
Annotation-based Authorization 基于注解的授权
如果你更喜欢基于注解的授权控制,除了 Subject 的 API 之外,Shiro提供了一个 Java 5 的注解集。
Configuration 配置
在你使用 JAVA 的注解之前,你需要在程序中启动 AOP 支持,因为有许多AOP 框架,所以很不幸,在这里并没有标准的在程序中启用 AOP 的方法。
关于AspectJ,你可以查看我们的AspectJ sample application;
关于Spring,你可以查看 [Spring Integration](https://www.ctolib.com/docs/sfile/apache-shiro-reference/V. Integration 整合/15. Spring Framework.md)文档;
关于Guice,你可以查看我们的 [Guice Integration](https://www.ctolib.com/docs/sfile/apache-shiro-reference/V. Integration 整合/16. Guice.md)文档;
RequiresAuthentication 注解
RequiresAuthentication 注解表示在访问或调用被注解的类/实例/方法时,要求 Subject 在当前的 session中已经被验证。
举例:
1 |
|
这基本上与下面的基于对象的逻辑效果相同:
1 | public void updateAccount(Account userAccount) { |
RequiresGuest 注解
RequiresGuest 注解表示要求当前Subject是一个“guest(访客)”,也就是,在访问或调用被注解的类/实例/方法时,他们没有被认证或者在被前一个Session 记住。
例如:
1 |
|
这基本上与下面的基于对象的逻辑效果相同:
1 | public void signUp(User newUser) { |
RequiresPermissions 注解
RequiresPermissions 注解表示要求当前Subject在执行被注解的方法时具备一个或多个对应的权限。
例如:
1 |
|
这基本上与下面的基于对象的逻辑效果相同
1 | public void createAccount(Account account) { |
RequiresRoles 注解
RequiresRoles 注解表示要求当前Subject在执行被注解的方法时具备所有的角色,否则将抛出 AuthorizationException 异常。
例如:
1 |
|
这基本上与下面的基于对象的逻辑效果相同
1 | public void deleteUser(User user) { |
RequiresUser 注解
RequiresUser* 注解表示要求在访问或调用被注解的类/实例/方法时,当前 Subject 是一个程序用户,“程序用户”是一个已知身份的 Subject,或者在当前 Session 中被验证过或者在以前的 Session 中被记住过。
例如:
1 |
|
这基本上与下面的基于对象的逻辑效果相同
1 | public void updateAccount(Account account) { |
JSP TagLib Authorization 标签库授权
Shiro 提供了一个标签库来控制 JSP/GSP 页面输出,这将在 [Web](https://www.ctolib.com/docs/sfile/apache-shiro-reference/III. Web Applications/10. Web.md) 文档中的 JSP/GSP 标签库中讨论
Authorization Sequence 授权序列
现在我们已经看到如何对当前 Subject 执行授权,让我们了解一下当一个授权命令调用时 Shiro 内部发生了什么事情。
我们仍使用前面[Architecture](https://www.ctolib.com/docs/sfile/apache-shiro-reference/I. Overview 总览/3. Architecture 架构.md)章节里的架构图,在左侧仅仅与授权相关的组件是高亮的,每一个数字代表授权操作中的一个步骤:
第1步:程序或框架代码调用一个 Subject 的hasRole*
、checkRole*
、 isPermitted*
或者 checkPermission*
方法,传递所需的权限或角色。
第2步:Subject实例,通常是一个 DelegatingSubject(或子类),通过调用securityManager 与各 hasRole*
、checkRole*
、 isPermitted*
或 checkPermission*
基本一致的方法将权限或角色传递给程序的 SecurityManager(实现了 org.apache.shiro.authz.Authorizer 接口)
第3步:SecurityManager 作为一个基本的“保护伞”组件,接替/代表其内部 org.apache.shiro.authz.Authorizer 实例通过调用 authorizer 的各自的 hasRole*
, checkRole*
, isPermitted*
,或 checkPermission*
方法。 authorizer 默认情况下是一个实例 ModularRealmAuthorizer 支持协调一个或多个实例 Realm 在任何授权操作实例。
**第4步:**,检查每一个被配置的 Realm 是否实现相同的 Authorizer接口,如果是,Realm 自己的各 hasRole*
、checkRole*
、 isPermitted*
或 checkPermission*
方法被调用。
ModularRealmAuthorizer
前面提到过,Shiro SecurityManager 默认使用 ModularRealmAuthorizer 实例,ModularRealmAuthorizer 实例同等支持用一个 Realm 的程序和用多个 Realm 的程序。
对于任何授权操作,ModularRealmAuthorizer 将在其内部的 Realm 集中迭代(iterator),按迭代(iteration)顺序同每一个 Realm 交互,与每一个 Realm 交互的方法如下:
1.如果Realm实现了 Authorizer 接口,调用它各自的授权方法(hasRole*
、 checkRole*
、isPermitted*
或 checkPermission*
)。
1.1.如果 Realm 函数的结果是一个 exception,该 exception 衍生自一个 Subject 调用者的 AuthorizationException,就切断授权过程,剩余的授权 Realm 将不在执行。
1.2.如果 Realm 的方法是一个 hasRole*
或 isPermitted*
,并且返回真,则真值立即被返回而且剩余的 Realm 被短路,这种做法作为一种性能增强,在一个 Realm 判断允许后,隐含认为这个 Subject 被允许。它支持最安全的安全策略:默认情况下所有都被禁止,明确指定允许的事情。
2.如果 Realm 没有实现 Authorizer 接口,将被忽略。
Realm Authorization Order 授权顺序
需要指出非常重要的一点,就如同验证(authentication)一样,ModularRealmAuthorizer 按迭代(iteration)顺序与 Realm 交互。
ModularRealmAuthorizer 拥有 SecurityManager 配置的 Realm 实例的入口,当执行一个授权操作时,它将在整个集合中进行迭代(iteration),对于每一个实现 Authorizer 接口的 Realm,调用Realm 各自的 Authorizer 方法(如 hasRole、 checkRole、 isPermitted或 checkPermission)。
Configuring a global PermissionResolver 配置全局的 PermissionResolver
当执行一个基于字符串的权限检查时,大部分 Shiro 默认的 Realm 将会在执行权限隐含逻辑之前首先把这个字符串转换成一个常用的权限实例。
这是因为权限被认为是基于隐含逻辑而不是相等检查(查看[Permission](https://github.com/waylau/apache-shiro-1.2.x-reference/blob/master/II. Core 核心/6.1. Permissions 权限.md)章节了解更多隐含与相等的对比)。隐含逻辑用代码表示要比通过字符串对比好,因此,大部分 Realm需要转换一个提交的权限字符串为对应的权限实例。
为了这个转换目的,Shiro 支持 PermissionResolver,大部分 Shiro Realm 使用 PermissionResolver 来支持它们对Authorizer 接口中基于字符串权限方法的实现:当这些方法在Realm上被调用时,将使用PermissionResolver 将字符串转换为权限实例,并执行检查。
所有的 Shiro Realm 默认使用内部的 WildcardPermissionResolver,它使用 Shiro 的WildcardPermission字符串格式。
如果你想创建你自己的 PermissionResolver 实现,比如说你想创建你自己的权限字符串语法,希望所有配置的Realm实例都支持这个语法,你可以把自己的 PermissionResolver 设置成全局,供所有 realm 使用。
如,在shiro.ini中:
1 | globalPermissionResolver = com.foo.bar.authz.MyPermissionResolver |
PermissionResolverAware
如果你想配置一个全局的 PermissionResolver,每一个会读取这个PermissionResolver 配置的 Realm 必须实现PermissionResolverAware 接口,这确保被配置 PermissionResolver 的实例可以传递给支持这种配置的每一个 Realm。
如果你不想使用一个全局的 PermissionResolver 或者你不想被PermissionResolverAware 接口麻烦,你可以明确地为单个的 Realm 配置 PermissionResolver 接口(可看作是JavaBean的setPermissionResolver 方法):
1 | permissionResolver = com.foo.bar.authz.MyPermissionResolver |
Configuring a global RolePermissionResolver 配置全局的RolePermissionResolver
与 PermissionResolver 类似,RolePermissionResolver 有能力表示执行权限检查的 Realm 所需的权限实例。
最主要的不同在于接收的字符串是一个角色名,而不是一个权限字符串。
RolePermissionResolver 被 Realm 在需要时用来转换一个角色名到一组明确的权限实例。
这是非常有用的,它支持那些遗留的或者不灵活的没有权限概念的数据源。
例如,许多 LDAP 目录存储角色名称(或组名)但不支持角色名和权限的联合,因为它没有权限的概念。一个使用 shiro 的程序可以使用存储于 LDAP 的角色名,但需要实现一个 RolePermissionResolver 来转换 LDAP 名到一组确切的权限中以执行明确的访问控制,权限的联合将被存储于其它的数据存储中,比如说本地数据库。
因为这种将角色名转换为权限的概念是特定的,Shiro 默认的 Realm 没有使用它们。
然而,如果你想创建你自己的 RolePermissionResolver 并且希望用它配置多个 Realm 实现,你可以将你的 RolePermissionResolver设置成全局。
shiro.ini
1 | globalRolePermissionResolver = com.foo.bar.authz.MyPermissionResolver |
RolePermissionResolverAware
如果你想配置一个全局的 RolePermissionResolver, 每个 Realm 接收必须实现了 RolePermisionResolverAware 接口的配置了的 RolePermissionResolver 。这保证了配置全局 RolePermissionResolver 实例可以传递到各个支持这样配置的 Realm 。
如果你不想使用全局的 RolePermissionResolver 或者你不想麻烦实现 RolePermissionResolverAware 接口,你可以单独为一个 Realm 配置 RolePermissionResolver(可以看作 JavaBean 的 setRolePermissionResolver 方法)。
1 | rolePermissionResolver = com.foo.bar.authz.MyRolePermissionResolver |
Custom Authorizer 定制Authorizer
如果你的程序使用多于一个 Realm 来执行授权而 ModularRealmAuthorizer 默认的简单迭代(iteration)、短路授权的行为不能满足你的需求,你可以创建自己的 Authorizer 并配置给相应的 SecurityManager。
例如,在shiro.ini中:
1 | [main] |
来源
1 | https://www.ctolib.com/docs/sfile/apache-shiro-reference/II.%20Core%20%E6%A0%B8%E5%BF%83/6.%20Authorization%20%E6%8E%88%E6%9D%83.html |
- 本文作者: 初心
- 本文链接: http://funzzz.fun/2021/02/03/Apache Shiro --- 5 Authorization 授权/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!