原文作者: 陈亚强
原文链接:http://www.ibm.com/developerworks/cn/webservices/ws-handler/index.html
高级软件工程师, 北京华园天一科技有限公司 2003 年 8 月
一、Handler的基本概念
J2EE Web 服务中的Handler技术特点非常像Servlet技术中的Filter。我们知道,在Servlet中,当一个HTTP到达服务端时,往往要经过多个Filter对请求进行过滤,然后才到达提供服务的Servlet,这些Filter的功能往往是对请求进行统一编码,对用户进行认证,把用户的访问写入系统日志等。相应的,Web服务中的Handler通常也提供一下的功能:
对客户端进行认证、授权;
把用户的访问写入系统日志;
对请求的SOAP消息进行加密,解密;
为Web Services对象做缓存。
SOAP消息Handler能够访问代表RPC请求或者响应的SOAP消息。在JAX-RPC技术中,SOAP消息Handler可以部署在服务端,也可以在客户端使用。
下面我们来看一个典型的SOAP消息Handler处理顺序:
某个在线支付服务需要防止非授权的用户访问或者撰改服务端和客户端传输的信息,从而使用消息摘要(Message Digest)的方法对请求和响应的SOAP消息进行加密。当客户端发送SOAP消⑹保?突Ф说?andler把请求消息中的某些敏感的信息(如信用卡密码)进行加密,然后把加密后的SOAP消息传输到服务端;服务端的SOAP消息Handler截取客户端的请求,把请求的SOAP 消息进行解密,然后把解密后的SOAP消息派发到目标的Web服务端点。
Apache axis是我们当前开发Web服务的较好的选择,使用axisWeb服务开发工具,可以使用Handler来对服务端的请求和响应进行处理。典型的情况下,请求传递如图1所示。
图1 SOAP消息的传递顺序
在图中,轴心点(pivot point)是Apache与提供程序功能相当的部分,通过它来和目标的Web服务进行交互,它通常称为Provider。axis中常用的Provider有Java:RPC,java:MSG,java:EJB。一个Web服务可以部署一个或者多个Handler。
Apache axis中的Handler体系结构和JAX-RPC 1.0(JSR101)中的体系结构稍有不同,需要声明的是,本文的代码在axis中开发,故需要在axis环境下运行。
在axis环境下,SOAP消息Handler必须实现org.apache.axis.Handler接口(在JAX-RPC 1.0规范中,SOAP消息Handler必须实现javax.xml.rpc.handler.Handler接口),org.apache.axis.Handler接口的部分代码如下:
例程1 org.apache.axis.Handle的部分代码
为了提供开发的方便,在编写Handler时,只要继承org.apache.axis.handlers. BasicHandler即可,BasicHandler是Handler的一个模板,我们看它的部分代码:
例程2 BasicHandler的部分代码
public abstract class BasicHandler implements Handler {
protected static Log log =
LogFactory.getLog(BasicHandler. class .getName());
protected Hashtable options;
protected String name;
// 这个方法必须在Handler中实现。
public abstract void invoke(MessageContext msgContext) throws AxisFault;
public void setOption(String name, Object value) {
if ( options == null ) initHashtable();
options.put( name, value );
}
…
}
BasicHandler 中的 (MessageContext msgContext) 方法是 Handler 实现类必须实现的方法,它通过 MessageContext 来获得请求或者响应的 SOAPMessage 对象,然后对 SOAPMessage 进行处理。
在介绍 Handler 的开发之前,我们先来看一下目标 Web 服务的端点实现类的代码,如例程 3 所示。
例程 3 目标 Web 服务的端点实现类
public class HandleredService
{
// 一个简单的Web服务
public String publicMethod(String name)
{
return " Hello! " + name;
}
}
// 另一个Web服务端点:
package com.hellking.webservice;
public class OrderService
{
// web服务方法:获得客户端的订单信息,并且对订单信息进行对应的处理,
通常情况是把订单的信息写入数据库,然后可客户端返回确认信息。
public String orderProduct(String name,String address,String item, int quantity,Card card)
{
String cardId = card.getCardId();
String cardType = card.getCardType();
String password = card.getPassword();
String rderInfo = " name= " + name + " ,address= " + address + " ,item= " + item + " ,quantity= " + quantity + "
,cardId = " +cardId+ " ,cardType = " +cardType+ " ,password = " +password;
System.out.println( " 这里是客户端发送来的信息: " );
System.out.println(orderInfo);
return orderInfo;
}
}
二、下面我们分不同情况讨论 Handler 的使用实例。
使用Handler 为系统做日志
Handler 为系统做日志是一种比较常见而且简单的使用方式。和 Servlet 中的 Filter 一样,我们可以使用 Handler 来把用户的访问写入系统日志。下面我们来看日志 Handler 的具体代码,如例程 4 所示。
例程 4 LogHandler 的代码
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.util.Date;
import org.apache.axis.AxisFault;
import org.apache.axis.Handler;
import org.apache.axis.MessageContext;
import org.apache.axis.handlers.BasicHandler;
public class LogHandler extends BasicHandler {
/** invoke,每一个handler都必须实现的方法。
*/
public void invoke(MessageContext msgContext) throws AxisFault
{
// 每当web服务被调用,都记录到log中。
try {
Handler handler = msgContext.getService();
String filename = (String)getOption( " filename " );
if ((filename == null ) || (filename.equals( "" )))
throw new AxisFault( " Server.NoLogFile " ,
" No log file configured for the LogHandler! " ,
null , null );
FileOutputStream fos = new FileOutputStream(filename, true );
PrintWriter writer = new PrintWriter(fos);
Integer counter = (Integer)handler.getOption( " accesses " );
if (counter == null )
counter = new Integer( 0 );
counter = new Integer(counter.intValue() + 1 );
Date date = new Date();
msgContext.getMessage().writeTo(System.out);
String result = " 在 " + date + " : Web 服务 " +
msgContext.getTargetService() +
" 被调用,现在已经共调用了 " + counter + " 次. " ;
handler.setOption( " accesses " , counter);
writer.println(result);
writer.close();
} catch (Exception e) {
throw AxisFault.makeFault(e);
}
}
}
前面我们说过, Handler 实现类必须实现 invoke 方法, invoke 方法是 Handler 处理其业务的入口点。 LogHandler 的主要功能是把客户端访问的 Web 服务的名称和访问时间、访问的次数记录到一个日志文件中。
下面部署这个前面开发的 Web 服务对像,然后为 Web 服务指定 Handler 。编辑 Axis_Home/WEB-INF/ server-config.wsdd 文件,在其中加入以下的内容:
< parameter name = " allowedMethods " value = " * " />
< parameter name = " className " value = " com.hellking.webservice.HandleredService " />
< parameter name = " allowedRoles " value = " chen " />
< beanMapping languageSpecificType = " java:com.hellking.webservice.Card "
qname = " card:card " xmlns:card = " card " />
< requestFlow >
< handler name = " logging " type = " java:com.hellking.webservice.LogHandler " >
< parameter name = " filename " value = " c://MyService.log " />
</ handler >
</ requestFlow >
</ service >
…
</ globalConfiguration >
…
< handler name = " logging " type = " java:com.hellking.webservice.LogHandler " >
< parameter name = " filename " value = " c://MyService.log " />
</ handler >
…
< service name = " HandleredService " provider = " java:RPC " >
…
< requestFlow >
< handler type = " logging " />
… <!-- 在这里可以指定多个Handler -->
</ requestFlow >
</ service >
http://127.0.0.1:8080/handler/services/HandleredService?wsdl&method=publicMethod&name=chen
注意:这个 URL 需要根据具体情况改变。
在 Sun Jul 06 22:42:03 CST 2003: Web 服务 HandleredService 被调用,现在已经共调用了 1 次 .
在 Sun Jul 06 22:42:06 CST 2003: Web 服务 HandleredService 被调用,现在已经共调用了 2 次 .
在 Sun Jul 06 22:42:13 CST 2003: Web 服务 HandleredService 被调用,现在已经共调用了 3 次 .
使用Handler 对用户的访问认证
使用 Handler 为用户访问认证也是它的典型使用,通过它,可以减少在 Web 服务端代码中认证的麻烦,同时可以在部署描述符中灵活改变用户的访问权限。
对用户认证的 Handler 代码如下:
例程 5 认证的 Handler
import ….
// 此handler的目的是对用户认证,只有认证的用户才能访问目标服务。
public class AuthenticationHandler extends BasicHandler
{
/** invoke,每一个handler都必须实现的方法。
*/
public void invoke(MessageContext msgContext) throws AxisFault
{
SecurityProvider provider = (SecurityProvider)msgContext.getProperty( " securityProvider " );
if (provider == null )
{
provider = new SimpleSecurityProvider();
msgContext.setProperty( " securityProvider " , provider);
}
if (provider != null )
{
String userId = msgContext.getUsername();
String password = msgContext.getPassword();
// 对用户进行认证,如果authUser==null,表示没有通过认证,
抛出Server.Unauthenticated异常。
org.apache.axis.security.AuthenticatedUser authUser
= provider.authenticate(msgContext);
if (authUser == null )
throw new AxisFault( " Server.Unauthenticated " ,
Messages.getMessage( " cantAuth01 " , userId), null , null );
// 用户通过认证,把用户的设置成认证了的用户。
msgContext.setProperty( " authenticatedUser " , authUser);
}
}
}
在 AuthenticationHandler 代码里,它从 MessageContext 中获得用户信息,然后进行认证,如果认证成功,那么就使用 msgContext.setProperty("authenticatedUser", authUser) 方法把用户设置成认证了的用户,如果认证不成功,那么就抛出 Server.Unauthenticated 异常。
部署这个 Handler ,同样,在 server-config 里加入以下的内容:
< handler name = " authen " type = " java:com.hellking.webservice.AuthenticationHandler " />
…
< service name = " HandleredService " provider = " java:RPC " >
< parameter name = " allowedRoles " value = " chen " />
…
</ service >
WEB-INF/users.lst 文件中加入以下用户:
hellking hellking
chen chen
http://127.0.0.1:8080/handler/services/HandleredService?wsdl&method=publicMethod&name=chen
将会提示输入用户名和密码,如图 2 所示。
<!--[if !vml]--><!--[endif]-->

图 2 访问 web 服务时的验证
如果客户端是应用程序,那么可以这样在客户端设置用户名和密码:
例程 6 在客户端设置用户名和密码
http://127.0.0.1:808
Service service = new Service();
Call call = (Call) service.createCall();
call.setTargetEndpointAddress( new java.net.URL(endpointURL) );
call.setOperationName( new
QName( " HandleredService " , " orderProduct " ) ); // 设置操作的名称。
// 由于需要认证,故需要设置调用的用户名和密码。
call.getMessageContext().setUsername( " chen " );
call.getMessageContext().setPassword( " chen " );
使用Handler 对用户的访问授权
对于已经认证了的用户,有时在他们操作某个特定的服务时,还需要进行授权,只有授权的用户才能继续进行操作。我们看对用户进行授权的 Handler 的代码。
例程 7 对用户进行授权的代码
import …
// 此handler的目的是对认证的用户授权,只有授权的用户才能访问目标服务。
public class AuthorizationHandler extends BasicHandler
{
/** invoke,每一个handler都必须实现的方法。
*/
public void invoke(MessageContext msgContext)
throws AxisFault
{
AuthenticatedUser user = (AuthenticatedUser)msgContext.getProperty( " authenticatedUser " );
if (user == null )
throw new AxisFault( " Server.NoUser " , Messages.getMessage( " needUser00 " ), null , null );
String userId = user.getName();
Handler serviceHandler = msgContext.getService();
if (serviceHandler == null )
throw new AxisFault(Messages.getMessage( " needService00 " ));
String serviceName = serviceHandler.getName();
String allowedRoles = (String)serviceHandler.getOption( " allowedRoles " );
if (allowedRoles == null )
{
return ;
}
SecurityProvider provider = (SecurityProvider)msgContext.getProperty( " securityProvider " );
if (provider == null )
throw new AxisFault(Messages.getMessage( " noSecurity00 " ));
for (StringTokenizer st = new StringTokenizer(allowedRoles, " , " ); st.hasMoreTokens();)
{
String thisRole = st.nextToken();
if (provider.userMatches(user, thisRole))
{
return ; // 访问授权通过。
}
}
// 没有通过授权,不能访问目标服务,抛出Server.Unauthorized异常。
throw new AxisFault( " Server.Unauthorized " ,
Messages.getMessage( " cantAuth02 " , userId, serviceName), null , null );
}
}
在 service-config.wsdd 文件中,我们为 Web 服务指定了以下的用户:
< parameter name = " allowedRoles " value = " chen,hellking " />
provider.userMatches(user, thisRole) 将匹配允许访问 Web 服务的用户,如果匹配成功,那么授权通过,如果没有授权成功,那么抛出 Server.Unauthorized 异常。
使用 Handler 对 SOAP 消息进行加密、解密
由于 SOAP 消息在 HTTP 协议中传输,而 HTTP 协议的安全度是比较低的,怎么保证信息安全到达对方而不泄漏或中途被撰改,将是 Web 服务必须解决的问题。围绕 Web 服务的安全,有很多相关的技术,比如 WS-Security , WS-Trace 等,另外,还有以下相关技术:
XML Digital Signature ( XML 数字签名)
XML Encryption ( XML 加密)
XKMS (XML Key Management Specification)
XACML (eXtensible Access Control Markup Language)
SAML (Secure Assertion Markup Language)
ebXML Message Service Security
Identity Management & Liberty Project
不管使用什么技术,要使信息安全到达对方,必须把它进行加密,然后在对方收到信息后解密。为了提供开发的方便,可以使用 Handler 技术,在客户端发送信息前,使用客户端的 Handler 对 SOAP 消息中的关键信息进行加密;在服务端接收到消息后,有相应的 Handler 把消息进行解密,然后才把 SOAP 消息派发到目标服务。
下面我们来看一个具体的例子。加入使用 SOAP 消息发送订单的信息,订单的信息如下:
例程 8 要发送的订单 SOAP 消息
< soap - env:Header />
< soapenv:Body >
< ns1:orderProduct soapenv:encodingStyle = " http://schemas.xmlsoap.org/soap/encod
ing / " xmlns:ns1= " HandleredService " >
< arg0 xsi:type = " xsd:string " > hellking </ arg0 >
< arg1 xsi:type = " xsd:string " > beijing </ arg1 >
< arg2 xsi:type = " xsd:string " > music - 100 </ arg2 >
< arg3 xsi:type = " xsd:int " > 10 </ arg3 >
< arg4 href = " #id0 " />
</ ns1:orderProduct >
< multiRef id = " id0 " soapenc:root = " 0 " soapenv:encodingStyle = " http://schemas.xmls
oap.org / soap / encoding / " xsi:type= " ns2:card " xmlns:soapenc= " http: // schemas.xmlsoa
p.org / soap / encoding / " xmlns:ns2= " card " >
< cardId xsi:type = " xsd:string " > 234230572 </ cardId >
< cardType xsi:type = " xsd:string " > visa </ cardType >
< password xsi:type = " xsd:string " > 234kdsjf </ password >
</ multiRef >
</ soapenv:Body >
</ soap - env:Envelope >
上面的黑体字是传输的敏感信息,故需要加密。我们可以使用 Message Digest 之类的方法进行加密。加密之后的信息结构如下:
例程 9 把 SOAP 消息某些部分加密
< soapenv:Envelope …
< soapenv:Body >
< ns1:orderProduct … >
…
< arg4 href = " #id0 " />
</ ns1:orderProduct >
< multiRef … >
< ns3:EncryptedData xmlns:ns3 = " http://www.w3.org/2000/11/temp-xmlenc " >
< ns3:DigestMethod Algorithm = " http://www.w3.org/2000/09/xmldsig#sha1 " />
< ns3:DigestValue > rO0ABXQAkyA8Y2FyZ…….
</ ns3:DigestValue >
</ ns3:EncryptedData >
</ multiRef >
</ soapenv:Body >
</ soapenv:Envelope >
图 3 是使用 Handler 对 SOAP 消息进行加密、解密后, SOAP 消息在传递过程中结构的改变。
<!--[if !vml]--><!--[endif]-->

图 3 SOAP 消息的加密和解密
从上图可以看出,通过使用加密、解密的 Handler ,可以确保消息的安全传递。进一步说,如果把这种 Handler 做成通用的组件,那么就可以灵活地部署到不同的服务端和客户端。
客户端的 Handler 的功能是把 SOAP 消息使用一定的规则加密,假如使用 Message Digest 加密方式,那么可以这样对敏感的信息加密:
例程 10 对 SOAP 消息的敏感部分加密
SOAPElement ele = soapBodyElement.addChildElement(envelope.createName
( " EncryptedData " , "" , " http://www.w3.org/2000/11/temp-xmlenc " ));
ele.addChildElement( " DigestMethod " ).addAttribute(envelope.createName
( " Algorithm " ), " http://www.w3.org/2000/09/xmldsig#sha1 " );
byte [] digest = new byte [ 100 ];
ByteArrayOutputStream out = new ByteArrayOutputStream ( 100 );
MessageDigest md = MessageDigest.getInstance( " SHA " );
ObjectOutputStream oos = new ObjectOutputStream(out);
// 要加密的信息
String data = " <cardId xsi:type='xsd:string'>234230572
</ cardId >< cardType xsi:type = ' xsd:string ' > visa </ cardType >
< password xsi:type = ' xsd:string ' > 234kdsjf </ password > " ;
byte buf[] = data.getBytes();
md.update(buf);
oos.writeObject(data);
oos.writeObject(md.digest());
digest = out.toByteArray();
out.close();
ele.addChildElement( " DigestValue " ).addTextNode( new
sun.misc.BASE64Encoder().encode(digest)); // 对加密的信息编码
在客户端发送出 SOAP 消息时,客户端的 Handler 拦截发送的 SOAP 消息,然后对它们进行加密,最后把加密的信息传送到服务端。
服务端接收到加密的信息后,解密的 Handler 会把对应的加密信息解密。服务端 Handler 代码如例程 11 所示。
例程 11 服务端解密 Handler
import …
// 此handler的目的是把加密的SOAP消息解密成目标服务可以使用的SOAP消息。
public class MessageDigestHandler extends BasicHandler
{
/** invoke,每一个handler都必须实现的方法。
*/
public void invoke(MessageContext msgContext) throws AxisFault
{
try
{
// 从messageContext例取得SOAPMessage对象。
SOAPMessage msg = msgContext.getMessage();
SOAPEnvelope env = msg.getSOAPPart().getEnvelope();
Iterator it = env.getBody().getChildElements();
SOAPElement multi = null ;
while (it.hasNext())
{
multi = (SOAPElement)it.next(); // multi是soapbody的最后一个child。
}
String value = "" ; // value表示加密后的值。
SOAPElement digestValue = null ;
Iterator it2 = multi.getChildElements();
while (it2.hasNext())
{
SOAPElement temp = (SOAPElement)it2.next();
Iterator it3 = temp.getChildElements(env.createName( " DigestValue " ,
" ns3 " , " http://www.w3.org/2000/11/temp-xmlenc " ));
if (it3.hasNext())
value = ((SOAPElement)it3.next()).getValue(); // 获得加密的值
}
// 把加密的SOAPMessage解密成目标服务可以调用的SOAP消息。
SOAPMessage msg2 = convertMessage(msg, this .decrypte(value));
msgContext.setMessage(msg2);
}
catch (Exception e)
{
e.printStackTrace();
}
}
// 这个方法是把加密的数据进行解密,返回明文。
public String decrypte(String value)
{
String data = null ;
try
{
ByteArrayInputStream fis = new
ByteArrayInputStream( new sun.misc.BASE64Decoder().decodeBuffer(value));
ObjectInputStream ois = new ObjectInputStream(fis);
Object o = ois.readObject();
if ( ! (o instanceof String)) {
System.out.println( " Unexpected data in string " );
System.exit( - 1 );
}
data = (String) o;
System.out.println( " 解密后的值: " + data);
o = ois.readObject();
if ( ! (o instanceof byte [])) {
System.out.println( " Unexpected data in string " );
System.exit( - 1 );
}
byte origDigest[] = ( byte []) o;
MessageDigest md = MessageDigest.getInstance( " SHA " );
md.update(data.getBytes());
}
…
return data;
}
// 把解密后的信息重新组装成服务端能够使用的SOAP消息。
public SOAPMessage convertMessage(SOAPMessage msg,String data)
{
….
}
}
总结
通过以上的讨论,相信大家已经掌握了 Handler 的基本使用技巧。可以看出,通过使用 Handler ,可以给 Web 服务提供一些额外的功能。在实际的开发中,我们可以开发出一些通用的 Handler ,然后通过不同的搭配方式把它们部署到不同的 Web 服务中。
public void init();
public void cleanup();
public void invoke(MessageContext msgContext) throws AxisFault ;
public void onFault(MessageContext msgContext);
public void setOption(String name, Object value);
public Object getOption(String name);
public void setName(String name);
public String getName();
public Element getDeploymentData(Document doc);
public void generateWSDL(MessageContext msgContext) throws AxisFault;
…
}