【IT168 技术文章】
关注点分离(separation of concerns)是面向服务的架构(Service-Oriented Architectures,SOA)的核心原则。令人遗憾的是,该原则在实现SOA服务时常常起不到作用。我们通常会看到带有多个关注点(如安全、事务管理)的巨大的实现类,使用业务逻辑记录所有混合在一起的关注点。使用Spring Framework和Aspect Oriented Programming (AOP)原则,我们可以将关注点分离,以用于服务实现。
本文中我们将演示如何使用Apache Axis和Spring来开发Web service,并使用Acegi Security对其进行保护——同时保持关注点很好地分离。
动机和设计
本文中我们将使用的示例是名为FundsTransferService的服务,银行使用该服务将资金从一个账户转移到另一个账户。可以在本文的参考资料部分找到该服务的WSDL及所有源代码、配置文件和构建文件。我们有意让该服务保持非常简单,以便集中讨论本文更有意义的方面。在本服务的实现中,我们将涉及三个关注点:
1.Web service管道,用来公开作为服务的功能
2.用于转移资金的业务逻辑
3.安全性,用于保证只有经授权的用户才能执行资金转移
而真正的系统很可能是必须处理其他的关注点,如事务管理、日志等。
我们想设计这样一种实现,处理每个关注点的代码与其他的代码完全分离。对于Web service管道,我们将使用Axis来公开作为服务的功能。用于将资金从一个账户转移到另一个账户的业务逻辑将封装在一组POJO(Plain Old Java Object)中。将通过Acegi Security框架来提供安全性。我们将使用Spring Framework及其AOP工具把各方面联系起来,使构成该Web service实现的所有代码之间的依赖性减到最小。
该实现的设计如图1所示。以黄色表示的对象就是我们需要实现的Web service。以蓝色表示的对象来自Axis;以粉红色表示的对象来自Acegi;以绿色表示的对象来自Spring。FundsTransferService是WSDL中所定义的服务接口。为了简化图表,我们将所有Axis类显示为名为Axis Engine的组件。BasicHandler也是Axis类,但由于它对于设计比较重要(稍后详述),所以单独显示出来。FundsTransferServiceSoapBindingImpl是Axis的生成类,需要实现它来提供服务功能。将直接通过Spring委派业务逻辑POJO AccountMgrImpl(稍后也会对此进行详细解释)。AccountMgrImpl与AccountMgr接口捆绑在一起是不错的做法,因为这样就允许我们插入Spring以发挥其作用(尽管有其他方法可以不带接口使用Spring)。
现在回到BasicHandler。需要返回的原因涉及到如何选择以提供安全。在本例中,将在两个不同级别上处理安全性:Web service的安全性,及应用程序代码的安全性,如POJO。按照关注点分离的理念,我们提出允许进行分离的设计。Axis允许插入定制的处理程序,侦听请求和响应消息,以提供其他功能(如安全性)。因此,我们将创建名为AcegiBridgeAuthenticationHandler的定制处理程序,负责处理Web service安全。将扩展Axis类BasicHandler,以便可以将其插入Axis处理程序框架。Acegi将用于提供应用程序级的安全,如提供对POJO的访问控制。
为了使这些方面无缝工作,需要将Web service安全上下文连接到Acegi安全上下文——因此定制的Axis处理程序类的名称为AcegiBridgeAuthenticationHandler。它不仅将处理Web service安全性,还负责将从该过程获取的安全上下文连接到Acegi环境,这样Acegi就可以决定是否授权访问POJO。方法是从Web service请求消息中提取安全声明,进行验证,然后创建认证令牌,因为我们已经为本例选择了用户名/密码认证,所以为UsernamePasswordAuthenticationToken。然后将该认证令牌设置到Acegi SecurityContext中,这样稍后在控制访问业务逻辑POJO时,Acegi可以使用请求方的证书和权限。
下面将解释如何使用Spring把各方面联系起来。法宝就是图1中名为AOP proxy的小绿色对象,由Spring生成。该对象实现了AccountMgr接口,并担当业务逻辑POJO AccountMgrImpl的代理。这允许Spring插入Acegi的MethodSecurityInterceptor,当有人尝试在POJO上调用方法时,会执行访问控制检查。当FundsTransferServiceSoapBindingImpl将Web service请求委派给POJO时,实际上是委派给AOP代理的实例,而不是直接委派给AccountMgrImpl。由于FundsTransferServiceSoapBindingImpl扩展了ServletEndpointSupport,因此它可以访问Spring应用程序上下文,以获得配置了正确接口、侦听器和目标类的AOP代理的引用AccountMgrImpl。
图2中的序列图演示了客户端调用FundsTransferService时,将发生的处理流程。请求消息由Axis Engine接收,然后调用AcegiBridgeAuthenticationHandler。AcegiBridgeAuthenticationHandler将验证认证信息,然后创建UsernamePasswordAuthenticationToken。接着,将该令牌设置到Acegi的SecurityContext中,用于稍后使用。AcegiBridgeAuthenticationHandler成功返回后,Axis Engine将在FundsTransferServiceSoapBindingImpl上调用transferFunds()方法。FundsTransferServiceSoapBindingImpl将其委派给AOP代理,在初始化过程中,可以较早从Spring web应用程序上下文获取。AOP proxy将调用Acegi MethodSecurityInterceptor,这样就可以进行它的安全检查。MethodSecurityInterceptor从SecurityContext获取认证令牌,然后检查是否已经对其进行了认证。接下来,将使用认证令牌中的信息来确定是否应该授权客户端的访问,在AccountMgrImpl上调用transferFunds()方法。如果允许客户端进行访问,那么MethodSecurityInterceptor将允许调用该方法,然后进入AccountMgrImpl。最后AccountMgrImpl处理该请求,并返回结果,最终传送回客户端程序。
业务逻辑实现和配置
我们的讨论将从解释业务逻辑类的实现和配置开始,因为他们是最简单的。AccountMgr接口和AccountMgrImpl类的源代码参见参考资料。从源代码中可以发现,事实上实现并没有做任何事情,所以可以保持其简单,因为本文并不是关于如何编写转移资金代码的文章。
下面是Spring配置文件的部分代码(可在参考资料部分得到整个配置文件),这些代码表明了如何配置业务逻辑的Spring beans,以便使用Spring的AOP工具。第一个bean条目是为AccountMgrImpl类创建bean。第二个bean条目是如何施展前面讨论的全部AOP代理魔法。使用从ProxyFactoryBean获取的id accountMgr来创建bean。当FundsTransferServiceSoapBindingImpl类向Spring请求具有该id的bean时,ProxyFactoryBean将返回AOP代理对象的实例。对它进行配置来实现AccountMgr接口,这样客户端程序会认为他们只是在使用业务逻辑对象。使用名为interceptorNames的第二个属性,创建名为securityInterceptor的bean(稍后将进行解释),可以侦听方法调用以执行安全检查。这允许我们在业务逻辑代码中插入不带任何依赖性的Acegi安全机制。最后,将目标设置到accountMgrTarget bean,这样方法调用将最终传送到实际的业务逻辑类AccountMgrImpl。
<bean id="accountMgrTarget"
class="com.mybank.bizlogic.AccountMgrImpl"/>
. . .
<bean id="accountMgr"
class="org.springframework.aop.framework.
ProxyFactoryBean">
<property name="proxyInterfaces">
<list>
<value>
com.mybank.bizlogic.AccountMgr
</value>
</list>
</property>
<property name="interceptorNames">
<list>
<value>
securityInterceptor
</value>
</list>
</property>
<property name="target">
<ref bean="accountMgrTarget"/>
</property>
</bean>
. . .
</beans>
Web service实现和配置
FundsTransferServiceSoapBindingImpl类是Web service实现。其源代码请参见参考资料部分。该类的框架由Axis生成,我们仅仅填写方法来提供实现。请注意该类扩展了ServletEndpointSupport。这是Spring提供的便利类,可以用于JAX-RPC Web service实现,来获取Spring应用程序上下文的引用。通过扩展该类,FundsTransferServiceSoapBindingImpl类可以访问Spring上下文,来获取先前所描述的accountMgr bean的引用。由于FundsTransferServiceSoapBindingImpl类由Axis管理,我们无法使用Spring的依赖性注入工具来自动获取该bean的引用。因此,必须在onInit()方法中明确执行。不幸的是,这会将该类中的一些依赖性添加到特定于Spring的类中。好,这就是使用Spring和Acegi在得到受益的同时需要付出的小小代价。请注意在实际的方法transferFunds()中,代码仅委派给accountMgr bean。
在Axis配置文件(deploy.wsdd和server-config.wsdd)中,需要确保将服务的实现类设置为该类FundsTransferServiceSoapBindingImpl,而不是由Axis生成的其他框架类(FundsTransferServiceSoapBindingSkeleton)。若要使Spring在与Axis相同的web应用程序中正确地工作,我们需要将下面的条目添加到web.xml文件中。context-param条目指定了放置Spring配置文件的位置。listener条目的设置用于在启动时载入Spring配置和上下文。
<context-param>
<param-name>
contextConfigLocation
</param-name>
<param-value>
/WEB-INF/spring-config.xml
</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.
ContextLoaderListener
</listener-class>
</listener>
. . .
</web-app>
Acegi安全配置
现在我们来讨论如何在Spring配置文件中配置Acegi安全。如前所述,我们配置了业务逻辑bean,因此由securityInterceptor bean来侦听方法调用,以执行安全检查。下面我们来看看如何配置该bean。下面所示是securityInterceptor bean的部分Spring配置文件代码。securityInterceptor bean是由名为MethodSecurityInterceptor的Acegi类提供的。正如其名称所示,该类用于加强方法调用的安全,通过侦听调用并检查调用方是否经过认证和授权。
. . .
<bean id="securityInterceptor"
class="org.acegisecurity.intercept.method.
aopalliance.MethodSecurityInterceptor">
<property name="authenticationManager">
<bean class="org.acegisecurity.
providers.ProviderManager">
<property name="providers">
<list>
<bean class="org.acegisecurity.providers.anonymous.
AnonymousAuthenticationProvider">
<property name="key" value="changeThis"/>
</bean>
</list>
</property>
</bean>
</property>
<property name="accessDecisionManager">
<bean class="org.acegisecurity.vote.UnanimousBased">
<property name="decisionVoters">
<list>
<bean class="org.acegisecurity. vote.RoleVoter"/>
</list>
</property>
</bean>
</property>
<property name="objectDefinitionSource">
<value>
com.mybank.bizlogic.AccountMgr.transferFunds=ROLE_MANAGER
</value>
</property>
</bean>
. . .
</beans>
我们需要配置带有authenticationManager属性的securityInterceptor bean,以指定使用哪种类型的认证。由于我们的设计依靠Axis 处理程序来执行认证,而这里不需要认证,因此仅使用AnonymousAuthenticationProvider进行配置。另外,我们在Axis处理程序中创建认证令牌的方式会使Acegi获知它已经进行了认证,因此不会尝试再次认证。稍后讨论Axis处理程序时,我们将对此进行更加详细的解释。
接下来,我们需要配置带有accessDecisionManager属性的bean,以指定如何决定是否授权某用户进行访问以调用方法。Acegi具有访问决策管理器(access decision manager)的三个具体实现:AffirmativeBased、ConsensusBased和UnanimousBased。Acegi根据投票人对是否授权某用户进行访问以执行特定操作的投票,来实施访问策略。计算投票数来决定是否应该授权访问。我们选择了UnanimousBased访问决策管理器,为了客户端能够执行操作,需要所有的投票都授权访问。用户应该阅读Acegi文档,以获取对该访问决策的更深入说明。下面,我们必须配置带有投票人列表的accessDecisionManager。这里我们仅使用一个名为RoleVoter投票人。这是来自Acegi的类,根据基于角色访问控制对是否授权访问进行投票。用户还应参考Acegi文档,以获取对RoleVoter工作方式的更深入说明。
需要配置的最后一个属性是objectDefinitionSource。这就是如何指定访问受保护的对象上不同方法所需要的认证。在这里,我们只想保护transferFunds()方法,并且只允许访问manager。通过列出所有符合条件的类名、方法名和进行访问所需的角色来实现:
com.mybank.bizlogic.AccountMgr.transferFunds=ROLE_MANAGER
定制的Axis处理程序
如先前所描述的,我们需要某个事物来连接Web service安全上下文和Acegi安全上下文。这里即用到定制的Axis处理程序AcegiBridgeAuthenticationHandler。我们保持了非常简单的实现,这样易于解释。可能引起注意的第一件事情是它实际上没有进行任何认证。我们只是从MessageContext中得到用户名和密码,然后照旧使用它们。当然,事实上真正的实现将尝试验证该信息。需要考虑的事情是真正的实现应该从SOAP消息中提取WS-Security头,然后进行处理以得到认证信息,而不是像我们这样仅仅从Axis MessageContext对象中提取他们。
得到认证信息后,接下来就是使其可用于Acegi。通过创建认证令牌,并在Acegi安全上下文中进行设置来实现。由于在进行用户名和密码认证,因此将创建UsernamePasswordAuthenticationToken的实例。进行创建前,需要指定为该主体授予了哪些权限。通过使用GrantedAuthority接口和称为GrantedAuthorityImpl的简单实现来实现。由于使用RoleVoter来进行访问决策,因此我们将进行授权的机构是角色。这里我们再次简化了实现,对其进行硬编码,授权主体manager角色,因为该角色是在POJO上调用transferFunds()方法所需要的。真正的实现可能将获取用户名和密码,然后在数据库或目录服务器中进行查找,以找到与该主体实际相关的角色。或者,在某些情况下,可在WS-Security头中以SAML声明形式获得该信息。
在任何情况下,一旦完成,都将创建UsernamePasswordAuthenticationToken实例,传递用户名、密码和授权的角色。通过使用这种形式的构造方法(采用GrantedAuthority数组),实际上在告知Acegi此令牌已经过了认证,因此不必再次进行认证。接下来,通过在SecurityContextHolder上调用静态方法来得到SecurityContext,并将认证令牌设置到SecurityContext中。现在认证和角色信息对于Acegi是下游可用的,用于执行其安全检查。因此,我们已有效地将Web service安全上下文连接到Acegi安全上下文。
有几件需要考虑的其他事情。首先,Acegi还提供了非常可靠的认证能力,因此不是让Axis处理程序来关注认证,可以让Acegi做这些事情。若要这样做,使用未采用GrantedAuthority数组的构造方法来创建未经认证的认证令牌。用户还需要确保配置了适当的认证提供程序,而不是使用AnonymousAuthenticationProvider。第二,Acegi不仅支持用户名/密码认证,还支持其他认证。例如,如果在进行基于PKI的认证,用户可以使用X509AuthenticationToken,而不是UsernamePasswordAuthenticationToken。
最后,需要配置Axis,在服务的请求处理路径上包含该处理程序。通过向Axis配置文件deploy.wsdd和server-config.wsdd添加以下条目来实现:
wsdd/providers/java">
<service name="FundsTransferService" provider="java:RPC" style="document"
use="literal">
. . .
<requestFlow>
<handler type="java:com.mybank.security.AcegiBridgeAuthenticationHandler"/>
</requestFlow>
. . .
</service>
</deployment>
结束语
关注点分离是开发面向服务的架构的关键原则。但是,它不仅需要应用到架构级,还需要应用到实现级。在本文中,我们演示了如何使用Axis、Spring和Acegi来实现符合SOA原则的受保护Web service。如示例代码所示,使用该方法使处理每个服务关注点的代码中的交叉依赖性减至最小。我们所给出的示例是刻意保持简单的,但应将其作为基础,用于开发具有可靠安全机制(结合了Web service安全性和Acegi提供的应用程序级安全性)的Web service。如前所述,真正的系统很可能需要开发处理程序,用来处理WS-Security头并将它们连接到Acegi安全上下文。一种方法是采用Apache工具包WSS4J,然后扩展其Axis处理程序以填充Acegi安全上下文,如本文所述。可能需要进行一些其他工作来创建捕获Acegi安全异常的Axis出站处理程序,并创建返回到客户端的更有意义的SOAP错误。