星期二, 三月 13, 2007

JBoss Seam 安全一点通

介绍
Seam框架内嵌了安全控制功能,能达到对业务方法和页面层面的控制。Seam安全机制是建立在JAAS的基础之上的,能够完美支持验证和授权。
Seam的安全机制在应用时分为两种模式:
--简单模式,支持验证服务和简单的基于角色的安全检测功能。
--高级模式,除了具备简单模式的功能外,还增加了使用JBoss Rules进行的基于规则的安全检测功能。
其中简单模式可以应用于大部分对安全没有特殊要求的场合,例如,需要通过角色来限定登陆用户访问的页面和执行的动作。
而高级模式主要应用于需要根据上下文状态或复杂的业务规则来进行安全检测的场合。
当使用高级模式的时候,Seam框架需要以下工具的支持(简单模式不需要):
drools-compiler-3.0.5.jar
drools-core-3.0.5.jar
commons-jci-core-1.0-406301.jar
commons-jci-janino-2.4.3.jar
commons-lang-2.1.jar
janino-2.4.3.jar
stringtemplate-2.3b6.jar
antlr-2.7.6.jar
antlr-3.0ea8.jar

为了使用基于Web的安全检测,jboss-seam-ui.jar必须被包含到WAR包中,并且要如下配置faces-config.xml:
《application》
《view-handler》org.jboss.seam.ui.facelet.SeamFaceletViewHandler《/view-handler》
《/application》

实现登陆验证
× 在components.xml中配置验证方法:
《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:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://jboss.com/products/seam/core http://jboss.com/products/seam/core-1.2.xsd
http://jboss.com/products/seam/components http://jboss.com/products/seam/components-1.2.xsd
http://jboss.com/products/seam/drools http://jboss.com/products/seam/drools-1.2.xsd"
http://jboss.com/products/seam/security http://jboss.com/products/seam/security-1.2.xsd"》

《security:identity authenticate-method="#{authenticator.authenticate}"/》

《/components》
表达式#{authenticator.authenticate} 指定了authenticator验证器中的验证方法,当调用authenticator验证器的login时是就会执行验证方法。

× 实现验证逻辑
@Name("authenticator")
public class Authenticator {
@In EntityManager entityManager;

public boolean authenticate() {
try
{
User user = (User) entityManager.createQuery(
"from User where username = :username and password = :password")
.setParameter("username", Identity.instance().getUsername())
.setParameter("password", Identity.instance().getPassword())
.getSingleResult();

if (user.getRoles() != null)
{
for (UserRole mr : user.getRoles())
Identity.instance().addRole(mr.getName());
}

return true;
}
catch (NoResultException ex)
{
FacesMessages.instance().add("Invalid username/password");
return false;
}

}

}
注解@Name("authenticator")把本类注册为Seam框架中的验证器,Identity.instance().getUsername()用来获取用户名称,Identity.instance().getPassword()用来获取用户输入的密码,Identity.instance().addRole()用于为经过验证的用户添加角色。

× 实现登陆页面
《div》
《h:outputLabel for="name" value="Username"/》
《h:inputText id="name" value="#{identity.username}"/》
《/div》

《div》
《h:outputLabel for="password" value="Password"/》
《h:inputSecret id="password" value="#{identity.password}"/》
《/div》

《div》
《h:commandButton value="Login" action="#{identity.login}"/》
《/div》
identity.login()方法用于验证用户,如果需要退出系统则调用 #{identity.logout}。

× 异常处理
当登陆验证失败或者用户权限不足时系统会抛出异常,为了不向用户暴露错误信息需要对pages.xml做以下异常重定向配置:
《pages》

...

《exception class="org.jboss.seam.security.NotLoggedInException"》
《redirect view-id="/login.xhtml"》
《message》You must be logged in to perform this action《/message》
《/redirect》
《/exception》

《exception class="org.jboss.seam.security.AuthorizationException"》
《end-conversation/》
《redirect view-id="/security_error.xhtml"》
《message》You do not have the necessary security privileges to perform this action.《/message》
《/redirect》
《/exception》

《/pages》

× 登陆后重定向
考虑以下场景,当用户访问受保护的资源时,系统会跳转到登陆页面要求用户登陆,当用户通过验证后,需要返回到登陆页面以前的受保护页面。
配置pages.xml实现未登录用户访问受保护页面时跳转到登陆页面:
《pages login-view-id="/login.xhtml"》

《page view-id="/members/*" login-required="true"/》

...

《/pages》

配置components.xml实现登陆后跳转:
《event type="org.jboss.seam.notLoggedIn"》
《action expression="#{redirect.captureCurrentView}"/》
《/event》

《event type="org.jboss.seam.postAuthenticate"》
《action expression="#{redirect.returnToCapturedView}"/》
《/event》

使用容器JAAS配置
如果需要使用容器提供的JAAS控制而不是Seam内置的SeamLoginModule,那么需要在components.xml配置文件中指定jaasConfigName属性,例如:
jaas-config-name="other"/>

授权
Seam为实现对Components(组件)、component method(组件方法) 和 pages(页面)的访问控制内置了很多的安全特性。提示:如果要使用高级模式请按照上面的配置启用高级模式。

核心概念
Seam框架提供的各种授权机制都是建立于一个前提概念之上的,那就是“系统中的每一个用户都会被授予角色和(或者)权限”,正是通过判断用户所具有的角色和(或者)权限能否满足被使用资源的声明,授权才准确地得以实行。其中,角色代表了一个组或者是类别的概念,被授予角色的用户可能会拥有访问多个资源的多个权限,角色代表一个权限集。而权限是单一的,是作用于单个资源上的一个动作。虽然仅仅使用权限也可以实现授权,但角色的引入大大的方便了授权的实现和管理。在表示方法上,角色仅仅通过一个名词来表示,例如,”管理员“,”编辑“,”拟稿人“,”仓库管理员“等等,而权限需要一个名词动词对来表达,例如,“客户:新增”,“客户:删除”,“产品:查询”等等。

组件的安全控制
在Seam中,注解@Restrict被用于声明安全检测。
Seam可以提供在类或者方法级别的安全检测,如果同时声明了类和方法级别的安全检测,那么方法级别优先而类级别被忽略。如果在一个组件类上应用了@Restrict,相当于对类中的每个方法都应用@Restrict。如果方法执行时发生了安全检测失败,作为约定,Identity.checkRestriction()会抛出一个异常。

一个空@Restrict注解代表了对一个由组件类名称和方法名称组合而成的权限进行隐式检测,例如:
@Name("account")
public class AccountAction {
@Restrict public void delete() {
...
}
}
即是代表了对权限“account:delete”进行检测相当于@Restrict("#{s:hasPermission('account','delete',null)}")显式检测。
再看另一个例子:
@Restrict @Name("account")
public class AccountAction {
public void insert() {
...
}
@Restrict("#{s:hasRole('admin')}")
public void delete() {
...
}
}
组件”account“在类级别启用了隐式安全检测,因此如果”account“内部的方法没有覆盖@Restrict声明的话都必须执行隐式权限检测,比如方法”insert“需要执行相当于@Restrict("#{s:hasPermission('account','insert',null)}")的权限检测,而方法”delete“因为声明了方法级别的@Restrict所以它将执行显式的@Restrict("#{s:hasRole('admin')}")角色检测。
注意s:hasPermission()和s:hasRole()都是表达式函数,对它们的调用会委派到Identity的对应方法。这些表达式函数可以在任意的表达式中使用,非常方便。
因为是表达式,@Restrict注解可以引用Seam上下文中任意的对象,这在需要对特定的对象实例进行安全检测时是非常方便的,例如:
@Name("account")
public class AccountAction {
@In Account selectedAccount;
@Restrict("#{s:hasPermission('account','modify',selectedAccount)}")
public void modify() {
selectedAccount.modify();
}
}
在执行modify方法时会去Seam的上下文中查找”selectedAccount“对象,并传递其引用到Identity的hasPermission(),这样就可以判断用户是否拥有对”selectAccount“修改(modify)的权限。

代码安全检测
有时候需要在代码中直接执行权限的限制而不是使用@Restrict注解,这时候直接调用Identity.checkRestriction()方法对安全表达式进行评估即可,例如:
public void deleteCustomer() {
Identity.instance().checkRestriction("#{s:hasPermission('customer','delete',selectedCustomer)}");
}
如果安全表达式评估结果为假,那么会执行以下任意一种动作:
--如果用户没有登陆则抛出NotLoggedInException异常。
--如果用户已经登陆则抛出AuthorizationException异常。
同理,也可以在代码中直接使用Identity.hasPermission()和Identity.hasRole()方法:
if (!Identity.instance().hasRole("admin"))
throw new AuthorizationException("Must be admin to perform this action");

if (!Identity.instance().hasPermission("customer", "create", null))
throw new AuthorizationException("You may not create new customers");

参考文档

星期一, 三月 12, 2007

JBoss Seam Develop tips

1. 分离测试和业务代码,新建一个源代码目录“test”,修改build.xml:
--在Property声明部分增加“test.java.dir" Property:
《!--src.java.dir was generated by seam gen--》
《property name="src.java.dir" value="src" /》
《!--test.java.dir added by developer manually--》
《property name="test.java.dir" value="test" /》
--修改target ”compile“的”javac“,添加test.java.dir到源代码目录:
《!--javac was generated by seam gen--》
《javac classpathref="build.classpath" destdir="${jar.dir}"
debug="${javac.debug}" deprecation="${javac.deprecation}" nowarn="on"》
《src path="${src.java.dir}" /》
《!--test.java.dir added by developer manually--》
《src path="${test.java.dir}" /》
《/javac》
--修改target “test“,把test.java.dir中的testng测试用例定义文件copy到测试classpath,增加以下语句:
《copy todir="${test.dir}" flatten="true"》
《fileset dir="${test.java.dir}"》
《include name="**/*Test.xml" /》
《/fileset》
《/copy》
--添加target “test_clean",并修改target ”test“,把”test_clean“加入”depends“属性:
《target name="test_clean"》
《delete》
《fileset dir="${test.dir}"》
《include name="**/*Test.xml" /》
《/fileset》
《/delete》
《/target》
《target name="test" depends="test_clean,jar,war,ear" description="Run the tests"》

2.测试驱动开发,Jboss Seam采用TestNG作为测试框架,同时支持Unit Testing(单元测试)和Intergration Testing(集成测试),其中前者主要用于分离地测试各层中特定的逻辑组件,
而后者负责测试除了页面以外的整个应用系统覆盖了各层的功能实现。
--随Jboss Seam 1.1.0.GA分发的Hibernate的JPA实现包hibernate-all.jar无法正常工作,需要更换最新的实现。
--Jboss Seam 1.1.0.GA文档中的”19.1. Unit testing Seam components“ 示例代码无法正常工作,建议参考example中的booking示例的BookingUnitTest.java,单元测试类直接继承于SeamTest。
--为了便于测试,简化测试代码的复杂度和提高测试执行效率,把有关EnityManager的管理代码抽取到一个共同的测试基类中,并且使用TestNG框架的方法级别和类级别的前置和后置代码功能,为每个测试类的每个线程保持一个EntityManager,每个方法绑定到一个Transaction,在执行方法的测试代码前自动打开事务,在方法执行完毕之后回滚事务,通过这种方法可以隔离各个测试方法不受外界影响也避免了频繁创建EntityManager带来的系统消耗。代码如下:
package com.psims.test;

import javax.ejb.CreateException;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import org.jboss.seam.mock.SeamTest;
import org.testng.annotations.Configuration;

import com.psims.base.BaseConstants;

public class BaseTest extends SeamTest {
private static EntityManagerFactory emf;

private ThreadLocal entityManagerLocal = new ThreadLocal();

public EntityManager getEntityManager() {
EntityManager em = this.entityManagerLocal.get();
if (em == null || !em.isOpen()) {
em = getEntityManagerFactory().createEntityManager();
this.entityManagerLocal.set(em);
}
return em;
}

public EntityManagerFactory getEntityManagerFactory() {
if (emf == null || !emf.isOpen()) {
emf = Persistence
.createEntityManagerFactory(BaseConstants.PERSISTENT_UNIT);
}
return emf;
}

@Configuration(beforeTestMethod = true)
public void beginTransaction() {

EntityManager em = getEntityManager();
em.getTransaction().begin();
}

@Configuration(afterTestMethod = true)
public void closeTransaction() {
EntityTransaction et = getEntityManager().getTransaction();
if (et != null && et.isActive()) {
et.rollback();
}
}

@Configuration(beforeTestClass = true)
public void createEntityManager() {
this.getEntityManager();
}

@Configuration(afterTestClass = true)
public void closeEntityManager() {
EntityManager em = getEntityManager();
EntityTransaction et = em.getTransaction();
if (et != null && et.isActive()) {
et.rollback();
}
em.close();
this.entityManagerLocal.remove();
if (emf != null && emf.isOpen()) {
emf.close();
}
}
}
3.在ManyToMany映射时,如果是双向关联需要在被控方设置”mappedBy“属性。和inverse雷同。
4.难道Session Context范围的Component可以自动初始化,在使用@In注入的时候,不需要(create = true)?
5.把Entity Bean直接使用@Name声明为Seam Component使用时,如果在使用集群的时候,效率会有问题,尽量
把Entity Bean作为Session Bean的一个属性来使用。
6.使用Jsf组件调用后台服务的时候,可以传递参数
action="#{hotelBooking.selectHotel(hot)}"
可以实现选择DataTable中的一行,这样就可以避免在后台服务组件中声明@DataModelSelection属性。
7.可以在不提交任何jsf Form的情况下调用Action并传递request parameter,而且他没有使用
任何的javascript,可以通过右键,“在新窗口中打开”的方式执行。
8.由于默认一个Conversation的生存范围仅仅持续一个Request,若需要跨多个Request,必须使用@Begin
和 @End 来限定。
9.在实现service层时,把实现查询功能的seesionbean的生存范围设置为Session而不是Conversation,
可以避免频繁的创建和销毁Component实例。
10.区分哪些状态信息适合于生存在哪个上下文中是合理正确使用Seam框架的基础。
11.EnityQuery 对象实现了对Enity的分页查询功能,继承者直接Override public String getEjbql() 方法,
并返回实际的ql语句即可,在使用Ejb继承EnityQuery时,还需要Override public setEntityManager(EntityManager em)方法,
以@PersistenceContext形式注入EntityManager。
12.Query是EntityQuery的抽象基类,它在内部封装了大部分通用的查询逻辑,其中getEjbql()方法负责返回ql查询语句的模板,如:
select e from Person e where lower(e.lastName) like lower( #{examplePerson.lastName} + '%' ) and
e.age = #{examplePerson.age} and e.gender = #{examplePerson.gender}
而parseEjbql()方法负责解析ql语句的格式,把需要设置的表达式查询参数和约束参数分解出来,其中queryParameters中
保存了形如:
#{examplePerson.age}
#{examplePerson.gender}
的表达式EL查询参数名称,
parsedEjbql保存使用符号参数替换EL查询参数后的ql语句了形如
select e from Person e where lower(e.lastName) like lower( :p1 + '%' ) and e.age = :p2 and e.gender = :p3
parsedRestrictions保存了约束语句,restrictionParameters保存了约束参数。
13.在实体Bean中不要直接使用HashCodeBuilder.reflectionHashCode and EqualsBuilder.reflectionEquals,
它们会引发[LazyInitializationException]异常,估计是由于动态代理的原因。
14.场景:Service由SFSB实现,DAO由SLSB实现,Service使用@In注入DAO实现,DAO使用@PersistenceContext由注入EntityManager.
现象:在页面上执行CUD操作时,无法完成操作,页面和后台均无无错误信息,或者提示session is null or closed
原因:使用@PersistenceContext注入的EntityManager的事务为标准类型,即边界为最外层SB的方法,而Seam中的Transaction
需要使用type=Extended类型的EntityManager
方案:使用@PersistenceContext(type = EXTENDED),需要使用SFSB 或使用@In(create = true) 注入在components.xml中定义好的 core:managed-persistence-context.
15.Seam Managed Transactions
Transaction management for extended persistence contexts. A transaction spans the restore view, apply request values, process validations,update model values and invoke application phases. It is committed when invoke application is complete, or renderResponse() or responseComplete() is called. A second transaction spans the render response phase.
场景:SB拥有声明性事务管理,默认容器会自动在SB中的个方法开始调用的时候开启新的事务,在方法结束的时候提交事务.
(或者如果是SB调用SB,延用已存在的事务)
现象:在使用ORM的lazy fecthing的时候会抛出LazyInitializationExceptions异常.
原因:当在View中访问惰性加载的属性时,已经超出了Transaction的边界,所以需要使用open session in view模式来将整个
request生命周期纳入Transaction中,在View 执行Render操作之后commit,这个会有一个问题,当事务失败的时候,view已经
被渲染完毕,使view表现了与persistence不一致的状态.
方案:首先使用Extended persistence context,并将scope拓展为conversation,而不是request.
其次采用为每个request启用两阶段Transaction,第一阶段始于JSF的"restore view" Phase,终于"invoke application" Phase,第二个阶段覆盖"render response" Phase.
16.扩展的持久上下文extended persistence context, The entity retrieved in the query remain in the managed state as long as the bean exists, so any subsequent method calls to the stateful bean can update them without needing to make any explicit call to the EntityManager.
17.测试的时候使用setField(hb, "facesMessages", new FacesMessages());
setField(hb, "log", new LogImpl(HotelBookingAction.class));
setField(hb, "events", new Events() { @Override public void raiseEvent(String type, Object... params) { assert "bookingConfirmed".equals(type); } } );

(译)在Rails的地盘耍横(基于Seam Eclipse Tools的5分钟快速开发演示)

原文:Beating Rails in its own game (5-minute movie on Seam Eclipse Tools)
By Michael Yuan

你能在5分钟写出一个完整的包含8个关联表格的CRUD Web应用吗?并且需要支持以下功能:
× 1对1,1对多,多对多关联(能够完成遍历和查询功能)
× 联合主键
× 查询任意列
× 分页查询
× 根据数据库定义自动获取必要的条件字段
× 基于AJAX的输入校验
× 提供完整的日期控件
× 具备即时可用的访问权限控制架构(仅仅需要提供java登陆验证代码即可工作)
× 完全使用基于CSS 和XHTML技术的页面布局

那么,你能使用java来完成这些吗?
使用最新发布的Red Hat Developer Studio开发环境和JBoss Seam框架,你可以仅仅点击几下鼠标就能完成上面所有的要求。
这儿有一个5分钟的flash来演示如何去做:
Seam开发演示

对于喜欢使用命令行工具而不习惯Elipse里的窗口工具的开发者来说可以使用具有相同功能的Ant脚本,具体请参考Seam Gen文档
这个演示仅仅是Ram Venkataraman, James Williams, Max Rydahl Andersen, and Max Katz 一起为 EclipseCon 2007准备的4小时的教程中的一小部分。完整的教程涉及了JBoss Eclipse tools for server management, Seam Gen, database browsing, database query analyzer, Facelets visual editing, business process editor, rules editor等等很多内容。等我们EclipseCon 2007归来之后,将来可能会把整个教程都发布到JBoss网站上,敬请期待。