15.5. 错误消息
安全API内嵌了针对各种安全相关操作的faces messages。下面这个表里面列出了在message.properties文件里面修改这些消息的message keys。如果想要禁用这些消息,只要在资源文件里面将这些错误消息的值置空就ok了。
Table 15.6. 安全消息
Message Key | 描述 |
org.jboss.seam.loginSuccessful | 不用多说了,看字面就知道什么意思了 |
org.jboss.seam.loginFailed | |
org.jboss.seam.NotLoggedIn | |
org.jboss.seam.AlreadyLoggedIn |
15.6. 验证(Authorization)
Seam Security API里面提供了很多权限验证的方法,无论是component、component method、或者页面上,你随时都可以进行权限验证相关的处理。这一节介绍了所有权限验证方面的内容。如果你需要使用任何高级特性(例如基于规则的权限验证),你一定要记住在components.xml文件里面指明(配置方法见前面的章节)。
15.6.1. 核心概念
Seam安全模块就是用来让那些被授予了角色和(或)权限的人能够进行某些操作,并且阻止那些没有被授权的人进行这些相应操作的。Seam安全接口提供了验证用户权限的各种方法,这些方法都基于角色/权限概念。通过扩展框架功能,你还能同时使用多种方式来对程序的安全进行控制。
15.6.1.1. 什么是角色?
一个角色就是一个组或者说是一种类型。这个组包含了用户可能会被赋予的一组权限(一组被许可在程序中进行的操作集合)。角色的结构很简单,仅仅由一个名称组成,例如“admin”、“user”、“customer”等。角色可以被赋予某个用户(有时也可能将角色赋予某个角色),并且可以用来对拥有相似操作权限的用户分组。
15.6.1.2. 什么是权限?
权限就是进行某个操作(有时是一次性操作)的权利。在编写一个程序的时候,你完全可以只用权限来控制所有操作(这里指不适用角色的概念),但是有了角色,你就能进行一些更加“高级”的操作,例如将一组很多个权限同时赋予用户。权限比角色的结构稍微复杂点,主要包含三个属性:一个目标(target)、一个操作(action)、一个接受者(recipient)。权限的目标指接受者(或者说用户)被授权进行某一特殊操作的对象(可以是一个字符串的名称或者一个class)。例如,一个名叫Bob的用户拥有删除客户的权限,那么,删除用户这个权限的对象(target)为“客户”,权限的操作(action)为“删除”,权限的接受者(recipient)为“Bob”这个人。
在这个文档里面,我们通常将一个权限通过这种方式来表现:target:action(我们在这里忽略了接受者,但是在实际应用当中,接受者是必须的) 。
15.6.2. components验证
让我们从最简单的验证方式开始——components验证。首先是@Restrict注解。
注意:@Restrict vs Typesafe security annotations
@Restrict能够使用EL表达式,所以它能够提供功能完善且灵活的验证方法。我们建议使用到的EL表达式应该是类型安全的,起码在编译的时候要保证它的安全。
15.6.2.1. @Restrict注解
Seam组件可以通过使用添加@Restrict注解的方式来实现方法级别或者类级别的安全控制。如果一个方法和这个方法所在类同时使用了@Restrict注解,那么方法级别的安全限制优先级更高(意味着类级别的限制不起作用)。如果在方法上验证失败,那么与执行Identity.checkRestriction()方法一样抛出一个异常(参考相关的文档)。把@Restrict加在一个组件类上,相当于将这个约束加在了这个类的所有方法上。
一个空的@Restrict约束默认使用componentName:methodName作为权限的名称。例如以下这段代码:
@Name("account")
public class AccountAction {
@Restrict public void delete() { ... }
}
在这个例子中,默认调用delete()方法需要account:delete权限。效果与
@Restrict("#{s:hasPermission('account','delete')}")
相等。
再来看下面这段代码:
@Restrict @Name("account")
public class AccountAction {
public void insert() {
...
}
@Restrict("#{s:hasRole('admin')}")
public void delete() {
...
}
}
这种情况下,类上已经加了@Restrict,这就意味着所有没有加@Restrict的方法都需要进行默认的权限验证。在上面这个例子中,insert()方法需要权限account:insert,而delete()方法需要用户拥有admin角色。
当我们继续进行下一步之前,让我们在上面的代码中找到这个表达式:#{s:hasRole()}。s:hasRole和s:hasPermission都是EL表达式,分别对应Identity类里面的方法。这个方法能够用在所有EL表达式中,并且会用在所有安全验证相关的部分。
作为一个EL表达式,@Restrict注解的值可以关联到任何一个Seam上下文中存在的对象上。这在对某个具体的实例进行权限验证的时候非常有用:
面这个例子中,有一段代码比较有趣。就是在hasPermission()方法内部使用到了selectedAccount对象。此时,Seam会到上下文中寻找这个对象实例,并且将它传递给Identity对象的hasPermission()方法。这样就能判断出当前用户是否有权限来修改当前被注入的这个Account对象。
@Name("account")
public class AccountAction {
@In Account selectedAccount;
@Restrict("#{s:hasPermission(selectedAccount,'modify')}")
public void modify() {
selectedAccount.modify();
}
}
15.6.2.2. 内部约束(Inline restrictions)
有的时候你可能会需要在某个方法的代码内部检查权限,这样就无法使用@Restrict注解。这种情况下,你就需要用到Identity.checkRestriction()方法来执行一个与安全有关的表达式,例如:
public void deleteCustomer() {
Identity.instance().checkRestriction("#{s:hasPermission(selectedCustomer,'delete')}");
}
如果表达式未返回true则:
用户没有登录——抛出NotLoggedInException异常,
用户已经登录——抛出AuthorizationException异常。
同样的,你也可以在任何Java代码中使用hasRole()和hasPersmission()方法。
if (!Identity.instance().hasRole("admin"))
throw new AuthorizationException("Must be admin to perform this action");
if (!Identity.instance().hasPermission("customer", "create"))
throw new AuthorizationException("You may not create new customers");
15.6.3. 程序界面中的安全控制(Security in the user interface)
一个良好的用户界面设计需要符合一个特征,用户应该只看见他所能操作的部分,而用户没有权限看见或者没有权限进行操作的按钮链接等,不应该显示出来。Seam提供了两种根据权限控制界面显示内容的方法:
1)根据权限判断页面是否显示,
2)根据权限判断界面中的某个控件是否显示。这两种控制都使用EL表达式来判断。
我们来看几个权限控制的例子。
首先,一个登录表单需要只有当用户没有登录的时候才能看见。如果用户已经登录,那么就不需要显示登录表单。这个功能可以使用identity.isLoggedIn()属性来判断,在页面上,我们可以写成这样:
<h:form class="loginForm" rendered="#{not identity.loggedIn}">
这种控制方式非常直观,看见代码的字面意思就应该能明白。下面,我们假设页面上有个显示所有报表的链接,这个链接只有当用户拥有manager角色的时候才能看见。我们在页面上可以这么写:
<h:outputLink action="#{reports.listManagerReports}" rendered="#{s:hasRole('manager')}"> Manager Reports </h:outputLink>
这段代码也很直观。如果用户没有manager角色,那么这个链接就不会显示。通常rendered属性可以直接写在具体的标签内部,但是根据实际情况,也可以将它写在某个父标签上,例如<s:div>或<s:span>。
现在,我们来看一个复杂点的例子。假设页面上有一个h:dataTable,用于显示一个记录列表。表格的最后一列用来显示对当前记录的操作链接,这个链接需要根据用户拥有的权限来判断是否显示。s:hasPermission表达式允许我们将当前行的记录作为参数传入权限判断函数,并根据传入的对象以及当前登录的用户来判断是否显示。下面就是这个表格的代码:
<h:dataTable value="#{clients}" var="cl"> <h:column> <f:facet name="header">Name</f:facet> #{cl.name} </h:column> <h:column> <f:facet name="header">City</f:facet> #{cl.city} </h:column> <h:column> <f:facet name="header">Action</f:facet> <s:link value="Modify Client" action="#{clientAction.modify}" rendered="#{s:hasPermission(cl,'modify')"/> <s:link value="Delete Client" action="#{clientAction.delete}" rendered="#{s:hasPermission(cl,'delete')"/> </h:column> </h:dataTable>
15.6.4. 页面安全控制(Securing pages)
页面安全控制需要程序里面有pages.xml文件,不过配置代码非常简单。只需要在page标签内部添加一个对象就能够实现安全控制。如果restrict对象没有任何属性或者子对象,那么默认情况下,来自non-faces (GET)的请求会需要权限/viewId.xhtml:render, JSF postback(来自表单提交方式)的请求会需要权限/viewId.xhtml:restore。此外,如果想要指定约束规则,那么只要写出标准的安全验证表达式即可。下面是几个例子:
<page view-id="/settings.xhtml"> <restrict/> </page>
<page view-id="/reports.xhtml"> <restrict>#{s:hasRole('admin')}</restrict> </page>
15.6.5. 实体安全控制(Securing Entities)
Seam安全模块允许给实体添加read,insert,update和delete操作。如果想要控制所有操作,那么只要在实体类上添加一个@Restrict注解:
@Entity
@Name("customer")
@Restrict
public class Customer {
...
}
如果@Restrict注解内部没有写任何权限判断表达式,那么默认的权限为entity:action,这里的entity为实体的实例。冒号后的操作有四种,分别是:read、insert、update、delete。
也可以单独指定某个生命周期操作,在@Restrict注解之外添加相应的注解到对应生命周期的方法上即可。共有以下四种注解:
- @PostLoad - 当一个实体的实例从数据库中读取出来以后调用。使用这个方法来配置read权限。
- @PrePersist - 插入数据库之前,insert
- @PreUpdate - 更新之前,update
- @PreRemove - 删除之前,delete
下面这个例子介绍了如果给所有insert操作添加安全检查。请注意,这个方法并不需要做任何事情,唯一需要注意的就是,这个方法是如何被声明的:
@PrePersist @Restrict
public void prePersist() {}
你也可以在/META-INF/orm.xml文件中定义:
<?xml version="1.0" encoding="UTF-8"?> <entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd" version="1.0"> <entity class="Customer"> <pre-persist method-name="prePersist" /> </entity> </entity-mappings>
当然,你仍然需要使用@Restrict来注解Customer对象中的prePersist()方法。
下面这个例子介绍了使用规则判断当前用户是否有权限添加一篇新的MemberBlog记录(来自seamspace示例)。
在下面这个规则中,需要被检查权限的实体会被自动插入到working memory中(这里就是指MemberBlog对象的实例):
rule InsertMemberBlog no-loop activation-group "permissions" when principal: Principal() memberBlog: MemberBlog(member : member -> (member.getUsername().equals(principal.getName()))) check: PermissionCheck(target == memberBlog, action == "insert", granted == false) then check.grant(); end;
这条规则的含义是:如果当前被验证用户(在规则中是用Principal来表示的)的姓名与当前将要被创建的blog的用户名相同,则检查当前用户是否拥有memberBlog:insert权限(还是当姓名相同的时候授予当前用户插入权限,译者并不是很清楚)。
类似“principal: Principal()”的结构代表一个变量绑定,它将Principal对象的实例从working memory中取出(在authentication的时候装载进入working memory的),并且给他取个变量名叫principal。变量一旦被绑定以后,其他地方就可以通过变量名来获取对象,例如在接下来的几行中,将member的用户名与Principal的name比较。如果想要了解更多有关规则的内容,请参考Drools文档。
最后,我们需要添加一个监听器来将seam security与你的JPA provider集成起来。
15.6.5.1. 使用JPA实现实体安全控制(Entity security with JPA)
EJB3实体的安全控制通过一个EntityListener来实现。你可以在META-INF/orm.xml写以下代码来安装这个监听器:
<?xml version="1.0" encoding="UTF-8"?> <entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd" version="1.0"> <persistence-unit-metadata> <persistence-unit-defaults> <entity-listeners> <entity-listener class="org.jboss.seam.security.EntitySecurityListener"/> </entity-listeners> </persistence-unit-defaults> </persistence-unit-metadata> </entity-mappings>
15.6.5.2. 使用一个受管的Hibernate Session来实现实体安全控制(Entity security with a Managed Hibernate Session)
如果你在Seam中使用Hibernate的SessionFactory,并且也使用了注解或者orm.xml文件进行映射配置,那么你不需要做任何事情就可以使用实体安全控制。
15.6.6. 类型安全的权限注解(Typesafe Permission Annotations)
seam提供了一组用来替代@Restrict的安全注解。由于这些注解不能想@Restrict这样灵活,所以这些注解提供了更高级别的编译时安全(compile-time safety)控制。
seam提供了一组标准的CRUD注解,当然,你可以扩展并添加属于你自己的注解。下面几个注解你能够在org.jboss.seam.annotations.security包内找到:
- @Insert
- @Read
- @Update
- @Delete
只需要将它们写在你想要检查权限的方法或者参数前面即可。如果放在方法前面,那么你需要指定一下需要检查权限的目标的class:
@Insert(Customer.class)
public void createCustomer() {
...
}
在这个例子里面,当用户尝试创建一个新的Customer对象的时候,会去检查用户是否有相应权限。需要检查权限的对象为Customer.class(实际上是其本身的java.lang.Class实例),而需要判断权限的操作为注解的小写方式,这里为insert。
这种注解同样也可以用在方法的参数上面。如果加在参数上,那么你不需要指定权限验证的目标(因为参数值本身就将作为权限验证的目标):
public void updateCustomer(@Update Customer customer) {
...
}
如果你想要编写自己的权限验证注解接口,只需要在注解接口上添加@PermissionCheck即可,例如:
@Target({METHOD, PARAMETER})
@Documented
@Retention(RUNTIME)
@Inherited
@PermissionCheck
public @interface Promote {
Class value() default void.class;
}
如果你想覆盖默认的权限判断操作,那么你可以在@PermissionCheck中指定你想覆盖的操作名称(小写):
@PermissionCheck("upgrade")
15.6.7. 类型安全的角色控制注解(Typesafe Role Annotations)
除了权限注解之外,Seam安全模块同样也提供了类型安全的角色注解。这些注解允许你根据用户所拥有的角色来判断是否能够进行相关操作。
Seam单独提供了这么一个注解:org.jboss.seam.annotations.security.Admin。这个注解用来判断用户是否拥有admin角色(只要你的程序里面支持这个角色)。想要创建你自己的角色判断注解,只需要在你编写的注解接口上额外添加一个org.jboss.seam.annotations.security.RoleCheck注解,例如:
@Target({METHOD})
@Documented
@Retention(RUNTIME)
@Inherited
@RoleCheck
public @interface User {
}
当编写了上面这个注解以后,只要添加了这个注解的地方,就会自动去检查用户是否拥有user角色(注解名称的小写形式)。
15.6.8. 权限验证的模型(The Permission Authorization Model)
seam security提供了一个可扩展的应用程序权限解析框架。下面的类图展现了权限验证框架的主要组件:
上图中涉及到的类,将在后面的章节中详细介绍。
15.6.8.1. PermissionResolver组件
这个组件实际上是一个接口。提供了解析某个对象权限的方法。Seam提供了下面两个内置的PermissionResolver实现。后面的章节中会详细介绍它们。
- RuleBasedPermissionResolver - 这个权限解析器使用Drools来解析基于规则的权限验证。
- PersistentPermissionResolver - 这个权限解析器用来解析持久化的对象权限,例如将权限存储在数据库中。
15.6.8.1.1. 编写你自己的PermissionResolver
自定义权限解析器非常简单。PermissionResolver接口只定义了两个必须实现的方法,这两个方法列举在下面的表格中。
表 15.7. PermissionResolver接口
- boolean hasPermission(Object target, String action):这个方法必须要判断出当前被验证的用户(通过Identity.getPrincipal()获得)是否拥有权限来对target进行某个action。如果返回true,那么当前用户拥有权限,反之返回false
- void filterSetByAction(Set<Object> targets, String action):这个方法应该实现将任意对象从指定的set中删除,以便当传入相同的action参数值的时候能够让hasPermission()方法返回true。
当你将自定义的权限解析器部署到你的项目中去的时候,它就会自动检查并注册到默认的解析器组中去。
15.6.8.2. 权限解析链ResolverChain
一个权限解析链(ResolverChain)包含了一组有序的权限解析器(PermissionResolver),因此它能够解析某一特定class或者target的权限。
下面的程序表(sequence diagram)展现了在权限验证过程(流程说明)中,各权限组件是如何互相配合工作的。一个权限验证的时间有可能来自各种不同的情况,例如,通过安全拦截器、EL表达式s:hasPermission、或是显式调用Identity.checkPermission接口:
- 1. 在某个地方触发了权限验证事件(在代码中或者EL表达式中),调用到了Idengity.hasPermission()方法。
- 1.1. Identity引用
PermissionMapper.resolvePermission()方法,传入需要被解析的权限。
- 1.1.1.
PermissionMapper提供一个
ResolverChain实例的Map,
key为class。通过这个map为权限的target对象寻找一个正确的ResolverChain。
一旦找到正确的
ResolverChain,
就会调用ResolverChain.getResolvers()将所有的PermissionResolver重新检索出来
- 1.1.2. PermissionMapper会为ResolverChain中的每一个PermissinResolver调用一次hasPermisson()方法,传入需要被检验的权限实例。一旦任何一个PermissionResolver返回了true,权限验证就成功并且PermissionMapper也返回true给Identity。如果没有一个PermissionResolver返回true,权限验证就失败。
15.6.9. RuleBasedPermissionResolver
RuleBasedPermissionResolver作为seam内置的一个权限解析器,支持基于Drools(JBoss Rules)规则的权限判断机制。基于规则的权限验证解析器有两个重要的原因
1)、将权限验证的业务逻辑与其他部分分离,全部集中到规则文件中,便于维护与修改
2)、速度——Drools的规则判断与执行内核拥有非常高效的算法,能够让非常复杂的规则判断更加快速。
15.6.9.1. 需求(Requirement)
如果使用基于规则的权限判断机制,那么以下几个包必须发布到你的项目中去:
drools-compiler.jar
drools-core.jar
janino.jar
antlr-runtime.jar
mvel14.jar
15.6.9.2. 配置
首先,Drools规则系统需要在components.xml文件中配置一下。默认情况下,会去找叫做securityRules的规则。例如下面这段配置:
<components xmlns="http://jboss.com/products/seam/components" xmlns:core="http://jboss.com/products/seam/core" xmlns:security="http://jboss.com/products/seam/security" xmlns:drools="http://jboss.com/products/seam/drools" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation= "http://jboss.com/products/seam/core http://jboss.com/products/seam/core-2.1.xsd http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.1.xsd http://jboss.com/products/seam/drools http://jboss.com/products/seam/drools-2.1.xsd" http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.1.xsd"> <drools:rule-base name="securityRules"> <drools:rule-files> <value>/META-INF/security.drl</value> </drools:rule-files> </drools:rule-base> </components>
这个默认的规则名称可通过覆盖RuleBasedPermissionResolver的security-rules属性来指定。
<security:rule-based-permission-resolver security-rules="#{prodSecurityRules}"/>
一个RuleBase的组件已经配置好,现在是时候来写一些安全规则了。
15.6.9.3. 编写权限规则
编写权限规则的第一件事就是要在你程序jar包所在路径/META-INF下面创建一个规则文件。
通常这个文件被取名为类似security.drl。不过你可以随意给他取名,只要这个名字在
components.xml
文件中能对的上号。
那么,安全规则文件到底需要包含哪些内容呢?首先我们应该快速浏览一下Drools的文档。我们先从一个及其简单的例子来分析:
package MyApplicationPermissions; import org.jboss.seam.security.permission.PermissionCheck; import org.jboss.seam.security.Role; rule CanUserDeleteCustomers when c: PermissionCheck(target == "customer", action == "delete") Role(name == "admin") then c.grant(); end
让我们一行一行来分析。首先我们看见的是包声明。Drools里面的package实际上是一组规则的集合。包名你可以任意取,因为这里的包名仅仅针对这些规则文件有效。
接下来需要注意的就是import PermissinCheck与Role对象。这两句告诉规则引擎后面需要用到这两个对象。
最后就轮到规则检查语句了。通常一个包里面的每一条规则都会被起一个名字,这个名字经常用规则的目的来取名。这里我们的规则名为CanUserDeleteCustomers。这个规则是用来判断一个用户能否删除一条客户记录。