Apache Shiro 授权
Apache Shiro 授权
授权,亦即访问控制,是管理资源访问的过程,换言之,也就是控制在一个程序中“谁”有权利访问“什么”。
授权的例子有:是否允许这个用户查看这个页面,编辑数据,看到按钮,或者从这台打印机打印?这些决定一个用户可以访问什么的决断。
授权组件
授权有三个核心组件,在Shiro中我们经常要用到它们:权限(permissions)、角色(roles)和用户(users)。
权限(Permissions)
权限是Apache Shiro中安全策略最基本的组件,它们是一组关于行为的基本指令,以明确表示在一个程序中什么可以做。一个很好定义的权限指令必须描述资源以及当一个Subject与这些资源交互时什么动作可以执行。
下面是一些权限指令的例子:
打开一个文件;
查看“/user/list”页面;
打印文档;
删除“Jsmith”用户。
大部分资源都支持基本的CRUD(create,read,update,delete)操作,而任何能识别特定资源类型的动作也是支持的的。最基础的思想是在资源和动作的基础上设置的最小量的权限指令。
当看到权限时,也许最重要的事情是意识到一个权限指令没有描述“谁”可以执行这个动作,它们只是一些“什么”可以做的指令。
权限只描述行为
权限指令只描述行为(和资源相关的动作),并不关心“谁”有能力执行这个动作。
定义“谁”(用户)被允许做“什么”(权限)需要用一些方法将权限赋给用户,这通常取决于程序的数据模型而且经常在程序中发生改变。
例如,一组权限可以归于一个角色而角色与一个或多个用户对象关联,或者一些程序可以有一组用户而一个组可以指定一个角色,在这里关系将被传递也就是说组内用户隐含被赋予角色的权限。
有很多方式可以将权限赋予用户--程序根据需求决定如何设计。
我们稍后讨论shiro如何判断一个Subject是否被允许。
权限粒度
上面的权限示例都是对资源(门、文件、客户等)指定的动作(打开、读、删除等),在一些场景中,他们也会指定非常细粒度的“实例级别”行为--例如,“删除”(delete)名为“Jsmith”(实例标识)的“用户”(resource type),在Shiro中,你可以精确定义指令到所能细化到的程度。
我们在Shiro的Permissions文档中详细讨论权限粒度和权限指令的“级别”。
角色
角色是一个实体名,代表一组行为或职责,这些行为在程序中转换为你可以或者不能做的事情。角色通常赋给用户帐号,关联后,用户就可以“做”属于不同角色的事情。
有两种有效的角色,Shiro都支持。
隐含角色:大部分用户隐含创建角色:程序只是在一个角色名称上隐含了一组行为(也就是权限),使用隐含角色时,在软件级别不会说“某角色允许执行行为A、B和C”,行为隐含于一个单独的名字中。
潜在的安全隐患
虽然这是一个非常简单和常用的方法,但隐含的角色可能会增加软件的维护成本和管理问题。
例如,如果你想增加或删除一个角色,或者重定义角色的行为怎么办?你不得不重新打开代码修改所有已更改角色的检测,每次都需要这样做,这还没提到其引起的执行代价(重测试,通过质量验证,关闭程序,升级软件,重启程序等)。
对于简单程序这种方法可能适用(比如只有一个'admin'角色和'everyone else'角色),但复杂的程序中,这会成为你程序生命周期中一个主要的问题,会给你的软件带来很大的维护代价。
明确角色:明确角色本质上是集合了实际权限指令的一个名字,在这种形式下,程序(以及shiro)准确知道是否拥有特定的角色意味着什么,因为它确切知道某行为是否可以执行,不用猜测或隐喻一个特定的角色可以或不可以做什么。
shiro团队提倡使用权限和明确角色替代原始的隐含方法,你可以对程序安全提供更强的控制。
基于资源的访问控制
读一下Les Hazlewood的文章:The New RBAC: Resource-Based Access Control,这篇文章深入讨论了使用权限和明确角色(以及对源代码的影响)代替旧的隐含角色方法的好处。
用户
一个用户本质上是程序中的“谁”,如同我们前面提到的,Subject实际上是shiro的“用户”概念。
用户(Subjects)通过与角色或权限关联被允许执行程序内特定的动作,程序数据模型确切定义了Subject是否允许做什么事情。
例如,在你的数据模型中,你定义了一个普通的用户类并且直接为其设置了权限,或者你只是直接给角色设置了权限,然后将用户与该角色关联,通过这种关联,用户就“有”了角色所具备的权限,或者你也可以通过“组”的概念完成这件事,这取决于你程序的设计。
数据模型定义了如何进行授权,Shiro依赖一个Realm实现将你的数据模型关联转换成Shiro可以理解的内容,我们将稍后讨论Realms。
最终,Realm实现是与你的数据源(RDBMS,LDAP等)所做的交流,Realm用来告知Shiro是否角色或权限存在,你可以完全控制你的授权模型如何创建和定义。
授权对象
在Shiro中执行授权可以有三种途径:
程序中授权--你可以在你的JAVA代码中执行用类似于if和else的结构来执行法令检查。
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?
}
下面是你可以根据需要调用的函数:
hasRole(String roleName)
如果Subject指定了特定的角色返回真,否则返回假;
hasRoles(List
返回一个与参数顺序相对应的hasRole结果数组,当一次有多个角色需要检测时非常有用(如定制一个复杂的视图)。
hasAllRoles(Collection
如果Subject指定了所有角色返回真,否则返回假。
角色判断
还有另一个方法检测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
与isPermitted* 方法相比较,这种方法的优势是代码更为清晰,如果当前Subject不符合条件,你不必创建你自己的AuthorizationExceptions异常(如果你不想那么做)。。
下面是你可以根据需要调用的函数:
checkRole(String roleName)
如果Subject被指定为特定角色则安静地返回否则抛出AuthorizationException异常;
checkRoles(Collection
如果Subject被指定了所胡特定的角色则安静地返回否则抛出AuthorizationException异常;
checkRoles(String... roleNames)
和上面的checkRoles具有相同的效果,但允许Java5的变参形式。
基于权限的授权
就像我们上面在角色概述中提到的,通过基于权限的授权执行访问控制是更好的方法。基于权限的授权,因为其与程序功能(以及程序核心资源上的行为)紧 密联系,基于权限授权的源代码在程序功能改变时才需要改变,而与安全策略无关。这意味着与同样基于角色的授权相比,对代码的影响更少。
权限检查
如果你希望检查一个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?
}
在这个例子中,我们同样看到了一个非常强大的实例级别的访问控制检查--在单独数据实例上限制行为的能力。
基于对象的权限对下列情况非常有用:
希望编译期类型安全;
希望确保正确地引用和使用的权限;
希望对权限判断逻辑(称作权限隐含逻辑,基于权限接口的implies方法)执行进行明确控制;
希望确保权限正确地反映程序资源(例如,在一个对象域模型上创建一个对象时,权限类可能自动产生)。
下面是你可以根据需要调用的函数:
isPermitted(Permission p)
如果Subject允许执行特定权限实例综合的动作或资源访问返回真,否则返回假;
isPermitted(List
按参数顺序返回isPermitted的结果数组,如果许多权限需要检查时非常有用(如定制一个复杂的视图);
isPermittedAll(Collection
如果Subject拥有指定的所有权限返回真,否则返回假。
基于字符串的权限检查
虽然基于对象的权限检查很有用(编译期类型安全,对行为担保,定制隐含逻辑等),但在许多程序里有时候感觉有点笨重,另一种选择是用普通的字符串来代表权限实例。
例如,对于上面打印权限的例子,我们可以使用字符串权限检查达到同样的结果:
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)都表现为一个字符串。
上面的例子展示了一种以冒号分割的特殊形式的字符串,定义于Shiro的 org.apache.shiro.authz.permission.WildcardPermission实现中,它适合大多数用户的需求。
上面的代码块基本上是下面这段代码的缩写:
Permission p = new WildcardPermission("printer:print:laserjet4400n");
if (currentUser.isPermitted(p) {
//show the Print button
} else {
//don't show the button? Grey it out?
}
WildcardPermission令牌形式和构成选项将在Shiro的Permission文档中深入讨论。
上面的字符串使用默认的WildcardPermission格式,实际上你可以创造并使用你自己的字符串格式,我们将在下面Realm授权章节讨论如何这样做。
基于字符串的权限有利的一面在于你不需要实现一个接口而且简单的字符串也非常易读,而不利的一面在于不保证类型安全,而且当你需要定义超出字符串表现能力之外的更复杂的行为时,你仍旧希望基于权限接口实现你自己的权限对象。实际上,大部分Shiro的终端用户回为其简单而选择基于字符串的方式,但最终你的程序需求决定了哪一种方法会更好。
和基于对象的权限检查方法一样,下面是字符串权限检查的函数:
isPermitted(String perm)
如果Subject被允许执行字符串表达的动作或资源访问权限,返回真,否则返回假;
isPermitted(String... perms)
按照参数顺序返回isPermitted的结果数组,当许多字符串权限需要检查时非常有用(如定制一个复杂的视图时);
isPermittedAll(String... perms)
当Subject具备所有字符串定义的权限时返回真,否则返回假。
权限判断
作为检查Subject是否被允许做某件事之后的一个选择,你可以在逻辑执行之前简单判断他们是否具备所需的权限,如果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();
与isPermitted* 方法相比较,这种方法的优势是代码更为清晰,如果当前Subject不符合条件,你不必创建你自己的AuthorizationExceptions异常(如果你不想那么做)。
下面是你可以根据需要调用的函数:
checkPermission(Permission p)
如果Subject被允许执行特定权限实例指定的动作或资源访问,安静地返回,否则抛出AuthorizationException异常。
checkPermission(String perm)
如果Subject被允许执行权限字符串指定的动作或资源访问,安静地返回,否则抛出AuthorizationException异常。
checkPermissions(Collection
如果Subject被允许执行所有权限实例指定的动作或资源访问,安静地返回,否则抛出AuthorizationException异常。
checkPermissions(String... perms) 和上面的checkPermissions效果一样,只是使用字符串权限类型。
基于注解的授权
如果你更喜欢基于注解的授权控制,除了Subject的API之外,Shiro提供了一个Java5的注解集。
配置
在你使用JAVA的注解之前,你需要在程序中启动AOP支持,因为有许多AOP框架,所以很不幸,在这里并没有标准的在程序中启用AOP的方法。
关于AspectJ,你可以查看我们的AspectJ sample application(http://svn.apache.org/repos/asf/shiro/trunk/samples/aspectj/);
关于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章节里的架构图,在左侧仅仅与授权相关的组件是高亮的,每一个数字代表授权操作中的一个步骤:
第1步:程序或框架代码调用一个Subject的hasRole*、checkRole*、 isPermitted*或者 checkPermission*方法,传递所需的权限或角色。
第2步:Subject实例,通常是一个DelegatingSubject(或子类),通过调用securityManager与各hasRole*、checkRole*、 isPermitted*或checkPermission*基本一致的方法传递给程序的SecurityManager。
第3步:SecurityManager,基本的安全组件,通过调用authorizer的各hasRole*、checkRole*、 isPermitted*或checkPermission*方法转交/委托给其内部的org.apache.shiro.authz.Authorizer实例,它负责在授权操作中协调一个或多个Realm实例。
第4步,每一个被配置的Realm被检查是否实现相同的Authorizer接口,如果是,Realm自己的各hasRole*、checkRole*、 isPermitted*或checkPermission*方法被调用。
ModularRealmAuthorizer
前面提到过,Shiro SecurityManager默认使用ModularRealmAuthorizer实例,ModularRealmAuthorizer实例对用一个Realm的程序和用多个Realm的程序同样支持。
对于任何授权操作,ModularRealmAuthorizer将与其内部的Realm集迭代,按迭代顺序同每一个Realm交互,与每一个Realm交互的方法如下:
1.如果Realm自己实现Authorizer接口,调用它各自的授权方法(hasRole*、 checkRole*、isPermitted*或 checkPermission*)。
1.如果Realm函数的结果是一个exception,该exception衍生自一个Subject调用者的AuthorizationException,这切断了授权过程,剩余的授权Realm将不在执行。
2.如果Realm的方法是一个hasRole*或isPermitted*,并且返回真,则真值立即被返回而且剩余的Realm被短路,这种做法作为一种性能增强,在一个Realm判断允许后,隐含认为这个Subject被允许。它支持最安全的安全策略:默认情况下所有都被禁止,明确指定允许的事情。
2.如果Realm没有实现Authorizer接口,将被忽略。
Realm授权顺序
需要指出非常重要的一点,就如同验证一样ModularRealmAuthorizer按迭代顺序与Realm交互。
ModularRealmAuthorizer拥有SecurityManager配置的Realm实例的入口,当执行一个授权操作时,它将在整个集合中进行迭代,对于每一个实现Authorizer接口的Realm,调用Realm各自的Authorizer方法(如 hasRole*、 checkRole*、 isPermitted*或 checkPermission*)。
配置全局的PermissionResolver
当执行一个基于字符串的权限检查时,大部分Shiro默认的Realm将会在执行权限隐含逻辑之前首先把这个字符串转换成一个常用的权限实例。
这是因为权限被认为是基于隐含逻辑而不是相等检查(查看Permission章节了解更多隐含与相等的对比)。隐含逻辑用代码表示要比通过字符串对比好,因此,大部分Realm需要转换一个提交的权限字符串为对应的权限实例。
为了这个转换目的,Shiro支持PermissionResolver,大部分Shiro Realm使用一个PermissionResolver来支持它们对Authorizer接口中基于字符串的权限方法的实现:当这些方法在Realm上被调用时,将使用PermissionResolver将字符串转换为权限实例,并执行检查。
所有的Shiro Realm默认使用内部的WildcardPermissionResolver,它采用Shiro的WildcardPermission字符串格式。
如果你想创建你自己的PermissionResolver 实现,比如说你想创建你自己的权限字符串语法,你希望所有配置的Realm实例都支持这个语法,你可以对所有的Realms把自己的PermissionResolver设置成全局。
如,在shiro.ini中:
shiro.ini
globalPermissionResolver = com.foo.bar.authz.MyPermissionResolver
...
securityManager.authorizer.permissionResolver = $globalPermissionResolver
...
PermissionResolverAware
如果你想配置一个全局的PermissionResolver,每一个会接收这个PermissionResolver配置的Realm必须实现PermissionResolverAware接口,这确保被配置PermissionResolver的实例可以传递给支持这种配置的每一个Realm。
如果你不想使用一个全局的PermissionResolver或者你不想被PermissionResolverAware接口麻烦,你可以明确地为单个的Realm配置PermissionResolver接口(可看作是JavaBean的setPermissionResolver方法):
permissionResolver = com.foo.bar.authz.MyPermissionResolver
realm = com.foo.bar.realm.MyCustomRealm
realm.permissionResolver = $permissionResolver
...
配置全局的RolePermissionResolver
与PermissionResolver类似,RolePermissionResolver有能力对执行检查的Realm所需的权限实例进行表示。
最主要的不同在于输入的字符串是一个角色名,而不是一个权限字符串。
RolePermissionResolver被Realm在需要时用来转换一个角色名到一组明确的权限实例。
这是非常有用的,它支持那些遗留的或者不灵活的没有权限概念的数据源。
例如,许多LDAP目录存储角色名称(或组名)但不支持角色名和权限的联合因为没有权限的概念。一个使用shiro的程序可以使用存储于LDAP的角色名,但需要实现一个RolePermissionResolver来转换LDAP名到一组确切的权限中以执行明确的访问控制,权限的联合将被存储于其它的数据存储中,比如说本地数据库。
因为这种将角色名转换为权限的理念是专用的,Shiro默认的Realm没有使用它们。
然而,如果你想创建你自己的 RolePermissionResolver 并且希望用它配置多个Realm 实现,你可以将你的 RolePermissionResolver设置成全局。
shiro.ini
globalRolePermissionResolver = com.foo.bar.authz.MyPermissionResolver
...
securityManager.authorizer.rolePermissionResolver = $globalRolePermissionResolver
...
RolePermissionResolverAware
如果你想配置一个全局的RolePermissionResolver,每一个会接收这个RolePermissionResolver配置的Realm必须实现RolePermissionResolverAware接口,这确保被配置了RolePermissionResolver的实例可以传递给支持这种配置的每一个Realm。
如果你不想使用全局的RolePermissionResolver 或者你不想麻烦实现RolePermissionResolverAware接口,你可以单独为一个Realm配置RolePermissionResolver(可以看作JavaBean的setRolePermissionResolver方法)。
rolePermissionResolver = com.foo.bar.authz.MyRolePermissionResolver
realm = com.foo.bar.realm.MyCustomRealm
realm.rolePermissionResolver = $rolePermissionResolver
...
定制Authorizer
如果你的程序使用多于一个Realm来执行授权而ModularRealmAuthorizer默认的简单迭代、短路授权的行为不能满足你的需求,你可以创建自己的Authorizer并配置给相应的SecurityManager。
例如,在shiro.ini中:
[main]
...
authorizer = com.foo.bar.authz.CustomAuthorizer
securityManager.authorizer = $authorizer