原文作者: 陳亞強

原文鏈接: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服務進行交互,它通常稱為Provideraxis中常用的ProviderJavaRPCjavaMSGjavaEJB。一個Web服務可以部署一個或者多個Handler 

Apache axis
中的Handler體系結構和JAX-RPC 1.0JSR101)中的體系結構稍有不同,需要聲明的是,本文的代碼在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的部分代碼

public interface Handler extends Serializable {
    
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;
   …
}

 
為了提供開發的方便,在編寫Handler時,只要繼承org.apache.axis.handlers. BasicHandler即可,BasicHandlerHandler的一個模板,我們看它的部分代碼: 

例程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所示。 

例程目標Web服務的端點實現類

package com.hellking.webservice;
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的代碼

package com.hellking.webservice;

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!",
                                    
nullnull);
            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文件,在其中加入以下的內容:
<service name="HandleredService" provider="java:RPC">
  
<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代碼如下:

例程認證的Handler
package com.hellking.webservice;
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]-->
 

訪問web服務時的驗證 

如果客戶端是應用程序,那么可以這樣在客戶端設置用戶名和密碼:

例程在客戶端設置用戶名和密碼

http://127.0.0.1:808
     
String endpointURL = "http://127.0.0.1:8080/handler/services/HandleredService?wsdl";            
            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的代碼。 

例程對用戶進行授權的代碼

package com.hellking.webservice;

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"), nullnull);
        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), nullnull);
    }     
}


service-config.wsdd文件中,我們為Web服務指定了以下的用戶:

<parameter name="allowedRoles" value="chen,hellking"/>


provider.userMatches(user, thisRole)
將匹配允許訪問Web服務的用戶,如果匹配成功,那么授權通過,如果沒有授權成功,那么拋出Server.Unauthorized異常。 

使用HandlerSOAP消息進行加密、解密

由于SOAP消息在HTTP協議中傳輸,而HTTP協議的安全度是比較低的,怎么保證信息安全到達對方而不泄漏或中途被撰改,將是Web服務必須解決的問題。圍繞Web服務的安全,有很多相關的技術,比如WS-SecurityWS-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技術,在客戶端發送信息前,使用客戶端的HandlerSOAP消息中的關鍵信息進行加密;在服務端接收到消息后,有相應的Handler把消息進行解密,然后才把SOAP消息派發到目標服務。 

下面我們來看一個具體的例子。加入使用SOAP消息發送訂單的信息,訂單的信息如下:

例程要發送的訂單SOAP消息

<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
    
<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之類的方法進行加密。加密之后的信息結構如下: 

例程SOAP消息某些部分加密

<?xml version="1.0" encoding="UTF-8"?>
<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是使用HandlerSOAP消息進行加密、解密后,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

package com.hellking.webservice;
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的操作是相反的過程。

總結
通過以上的討論,相信大家已經掌握了Handler的基本使用技巧。可以看出,通過使用Handler,可以給Web服務提供一些額外的功能。在實際的開發中,我們可以開發出一些通用的Handler,然后通過不同的搭配方式把它們部署到不同的Web服務中。