IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  SOA and Web services  >

Web 服务编程技巧与窍门: 使用 SOAP 头扩展 JAX-RPC Web 服务

使用 JAX-RPC 1.0 SOAP 处理程序来处理 SOAP 1.1 头

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

Richard A. Sitze (rsitze at us.ibm.com), 顾问软件工程师, IBM Software Group

2004 年 4 月 01 日

在本文中,作者研究了 JAX-RPC SOAP 处理程序如何处理 SOAP 消息头。具体来说,他展示了处理程序如何将 SOAP 头添加到传出消息,以及对应的处理程序如何从传入消息中删除 SOAP 头。此外,他还提出了 JAX-RPC 的程序化配置和部署模型,因为它们都与这个主题有关。

在本文中,我向您展示了如何通过添加 SOAP 头来扩展由 WSDL 定义的 Web 服务。我将展示 SOAP处理程序如何创建和处理 SOAP 消息头以及如何正确地配置处理程序。

基本 JAX-RPC SOAP 处理程序

让我们从研究 图 1中的 JAX-RPC 调用开始。


图 1. JAX-RPC/SOAP 消息流
JAX-RPC/SOAP 消息流

一个 JAX-RPC 调用用直线表示为两个 SOAP 消息:请求和响应。WSDL 文档定义了服务接口,因而也就定义了请求和响应的 SOAP 内容。

当您将 JAX-RPC SOAP 处理程序部署到消息流中时,您就可以访问这两个涉及远程过程调用的消息。JAX-RPC SOAP 处理程序是 JAX-RPC 处理程序,因为它们通过 handleRequest()handleResponse()handleFault() 方法支持 RPC 调用模型。它们也是 SOAP 处理程序,因为消息内容以 SAAJ 对象的形式作为 SOAP 内容提供给处理程序。(请参见 参考资料部分以获得关于这些对象的更多信息。

J2EE Web 服务 SOAP 处理程序

基于 JAX-RPC 的 J2EE Web 服务运行时在 SOAP 处理程序可能对 SOAP 消息进行的更改上强加了一些限制。即使是这样,也很可能会出现错误。处理程序对行为的正确性仍然负有责任。

处理程序直接操纵原始的 SOAP 内容,它并不知道在 WSDL 中定义的期望内容。处理程序开发人员对 SOAP 消息的正确性负责,这与强类型的 JAX-RPC 服务接口形成对比。

扩展服务


SOAP 提供通道来允许带外信息在客户端和服务之间交换。这种额外信息的交换使得有可能扩展服务接口所表示的行为而不用更改接口,因此也不用更改 WSDL。这样做是正确的,因为 SOAP 头不需要在 WSDL 中定义。(要获得更多这方面的信息,请查阅 WS-I 基本概要(WS-I Basic Profile)版本 1.0a;请参阅 参考资料以获得相关链接。)

虽然消息可以通过不由 WSDL/Schema 定义的 SOAP 头进行扩展,但是 SOAP 头的内容必须仍然是定义良好的。该消息头的所有生产者和使用者都必须就它的内容达成一致意见。

JAX-RPC SOAP 处理程序对于处理这样的带外信息来说是一种可操作的方法。具体来说,您可以通过在客户端部署一个处理程序并在服务处部署对应的处理程序来处理这样的信息,如 图 2所示。


图 2. 带有处理程序的 JAX-RPC/SOAP 消息流
带有处理程序的 JAX-RPC/SOAP 消息流

对于 SOAP 头信息,由两个处理程序表示的四个处理点是:

  • 客户端 handleRequest()将扩展内容附加到 传出请求消息中。
  • 服务器端 handleRequest()传入请求消息提取扩展内容。
  • 服务器端 handleResponse() / handleFault()将扩展内容附加到 传出响应响应消息中。
  • 客户端 handleResponse() / handleFault()传入响应消息提取扩展内容。

注意,每个处理程序内的信息交换点从请求消息到响应消息都使用 MessageContext ,正如文章“How to create a simple JAX-RPC handler”(请参见 参考资料以获得相关链接)中所讨论的。

让我们通过一些具体的例子来研究这些概念。





回页首


示例:处理消息内容

对于这个简单的示例,我部署了一些协作的处理程序,这些处理程序通过将签名放入消息头来签署传出消息,并且验证传入消息中的签名是否正确。该签名是消息体的一种表示;如果消息体发生改变,则签名验证就将失败。签署者的名字必须在处理程序的配置中;不要求客户端和服务端使用相同的名字进行签署。

您需要一种用于签署消息的算法,这种算法由 SignatureTool 接口表示。客户端处理程序和服务处理程序都必须签署传出消息并验证传入消息,这样,抽象类 SignHandler 就将提供通用代码。 ClientSignHandlerServiceSignHandler 扩展 SignHandler 并将所有的东西集中到一起。

清单 1. SignatureTool.java
interface SignatureTool {
    /**
     * @return Result of signer signing content.
     */
    public SOAPElement getSignature(String signersName,
                                    SOAPElement content) throws SOAPException;
    /**
     * @return true if content was signed, unchanged, by signer of signature.
     * This is only true if the content is the same content signed originally
     * by a signer, resulting in signature.
     */
    public boolean isSignatureValid(SOAPElement signature,
                                    SOAPElement content) throws SOAPException;
}
                

处理程序可以通过调用 SignatureTool.getSignature() 使用从传出 SOAP 消息中提取的内容来签署消息。类似地,签名的验证也通过调用 isSignatureValid() 使用从传入 SOAP 消息中提取的签名和内容来进行。

处理传出消息


您可以通过将签名作为一个新的头信息块添加到消息中来处理传出消息。 SignHandler.signOutgoing() (如 清单 2所示)演示了如何将一个头信息块添加到一个传出 SOAP 消息中。这个方法将:

  • 定位信封中的 SOAPHeader
  • 添加新的子 SOAP 头元素。
  • 为新的头元素创建和设置内容。
清单 2. SignHandler.signOutgoing()
abstract class SignHandler implements Handler
{
    . . .
    /**
     * Name of required property, in HandlerInfo handler configuration,
     * that specifies who signs outgoing messages.
     */
    public  static String SIGNERS_NAME_PROPERTY;
    
    private SignatureTool  signatureTool;
    private HandlerInfo    info;
    /**
     * Obtain signer's name from handler configuration (HandlerInfo),
     * and sign message.
     * @throws SignException if SIGNERS_NAME_PROPERTY is not available
     *                       in the handler config.
     */
    public void signOutgoing(SOAPMessageContext mc) throws SignException {
         // SIGNERS_NAME_PROPERTY is required to be on the configuration.
        Map config = info.getHandlerConfig();
        Object nameObj = config.get(SIGNERS_NAME_PROPERTY);
        String name = (String)nameObj;
        
        try {
            // Dig down into message, locate or create header block.
            SOAPMessage msg = mc.getMessage();
            SOAPPart part = msg.getSOAPPart();
            SOAPEnvelope envelope = part.getEnvelope();
            SOAPHeader header = envelope.getHeader();
            /**
             * Create new header element.
             * We don't specify a role on this header element,
             * meaning the target role is the "ultimate destination".
             */
            SOAPHeaderElement headerElement
                = (SOAPHeaderElement)header.addChildElement(SIGN_ELEMENT,
                                                            SIGN_PREFIX,
                                                            SIGN_NS_URI);
            // Locate portion of message content that is to be signed.
            SOAPElement content = getContent(part);
            /**
             * Create new element representing signature,
             * and add as child to new header element.
             */
            SOAPElement element = signatureTool.getSignature(name, content);
            headerElement.addChildElement(element);
        } catch (SOAPException e) {
            e.printStackTrace();
            throw new SignException("Unable to sign message", e);
        }
    }
    . . .
    // See resources for full sample code.
}
                

在处理之后,SOAP 信封将看起来如 清单 3所示。

清单 3. Outbound SOAP message
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <!-- original header content -->
        
    <!-- New header element, inserted by handler -->
    <sign:sign xmlns:sign="uri://org.example.webservices.signature.Sign">
      <!-- output of SignatureTool.getSignature() -->
    </sign:sign>

  </soap:Header>
  <soap:Body>
    <!-- original body content -->
  </soap:Body>
</soap:Envelope>
                
      

处理传入消息


您可以通过处理一个传入消息来验证签名是否有效。 SignHandler.checkIncoming() (如 清单 4所示)演示了如何定位头信息块,这是一个不可见的过程。这个方法将:

  • 定位信封中的 SOAPHeader
  • 决定当前的 SOAP 节点正在扮演的角色集(下面有更多的关于这方面的内容的介绍)。
  • 决定当前的处理程序理解的消息头集。
  • 处理这个处理程序理解的以及当前的 SOAP 节点正在扮演的角色集的消息头。
清单 4. SignHandler.checkIncoming()
abstract class SignHandler implements Handler
{
    . . .
    /**
     * Name of required property, in HandlerInfo handler configuration,
     * that specifies who signs outgoing messages.
     */
    public  static String SIGNERS_NAME_PROPERTY;
    
    private static QName   SIGN_HEADER;
    private SignatureTool  signatureTool;
    private HandlerInfo    info;
    /**
     * Look for signature on incoming message.
     * If signature not found, then continue message processing.
     * If signature is found, then verify that it is "correct".
     * If correct, then continue message processing.
     * If not correct, then throw SignException.
     */
    public void checkIncoming(SOAPMessageContext mc) throws SignException {
        try {
            SOAPMessage msg = mc.getMessage();
            SOAPPart part = msg.getSOAPPart();
            SOAPEnvelope envelope = part.getEnvelope();
            
            /**
             * Locate portion of message content that is to be signed.
             */
            SOAPElement content = getContent(part);
            /**
             * Dig down through SOAP headers looking for matches to
             * SIGN_HEADER, that are also targeted for this SOAP Node.
             */
            SOAPHeader header = envelope.getHeader();
            if (header != null) {
                /**
                 * The roles the node acts in are specified by the
                 * MessageContext.getRoles() method.
                 * If roles not found, default to "ultimate destination".
                 */
                String[] roles = mc.getRoles();
                if (roles == null  ||  roles.length == 0) {
                    roles = new String[] { "" };
                }
                for (int ridx = 0; ridx < roles.length; ridx++) {
                    String role = roles[ridx];
                    /**
                     * Examine headers bound to each role this node acts in.
                     * Headers are determined to be targeted for a SOAP Node by
                     * matching the node's roles with the header's actor role.
                     *
                     * So now go through list of headers associated with
                     * the role we are currently working on.
                     */
                    Iterator headerElementIter
                        = header.examineHeaderElements(role);
    
                    while (headerElementIter.hasNext()) {
                        SOAPHeaderElement headerElement
                            = (SOAPHeaderElement)headerElementIter.next();
    
                        // Is header recognized by this handler?
                        Name headerElementName
                            = headerElement.getElementName();
                        if (equals(headerElementName, SIGN_HEADER)) {
                            /**
                             * Look for SOAPElement(s) in header,
                             * ignoring mixed content.
                             */
                            Iterator headerIter
                                = headerElement.getChildElements();
    
                            while (headerIter.hasNext()) {
                                Object elementObj = headerIter.next();
    
                                if (elementObj instanceof SOAPElement) {
                                    SOAPElement element
                                        = (SOAPElement)elementObj;
                                    signatureTool.isSignatureValid(element,
                                                                   content);
                                }
                            }
                        }
                    }
                }
            }
        } catch (SOAPException e) {
            e.printStackTrace();
            throw new SignException("Unable to verify signature", e);
        }
    }
    . . .
    // See resources for full sample code.
}
                

关于角色的更多内容
SOAP 节点扮演的角色有望可以从 MessageContext 使用。 清单 4中的代码演示了 WebSphere Application Server 如何将空的字符串映射到最终目的地(缺省角色)上。最终目的地的正确值并没有在 SOAP 1.1 规范中明确地指定,因此这个值可能随着实现的不同而不同。更详细的讨论要等到将来的技巧出来之后才能进行。

处理程序


SignHandler 类是面向处理传入消息和传出消息的。处理程序接口则是面向处理请求消息和响应信息的。这两者将同时出现在处理程序的实现中。

ClientSignHandler (如 清单 5中所示)是非常简单的。它将请求消息上下文定向到作为传出消息进行处理,而将响应/故障消息上下文定向到作为传入消息进行处理。这补充了 ServiceSignHandler

清单 5. ClientSignHandler
public class ClientSignHandler extends SignHandler
{
    public boolean handleRequest(MessageContext mc) {
        signOutgoing((SOAPMessageContext)mc);
        return true;
    }
    public boolean handleResponse(MessageContext mc) {
        checkIncoming((SOAPMessageContext)mc);
        return true;
    }
    public boolean handleFault(MessageContext mc) {
        checkIncoming((SOAPMessageContext)mc);
        return true;
    }
}
                

ServiceSignHandler (如 清单 6所示)也一样简单。它将请求消息上下文定向到作为传入消息进行处理,而将响应/故障消息上下文定向到作为传出消息进行处理。这补充了 ClientSignHandler

清单 6. ServiceSignHandler
public class ServiceSignHandler extends SignHandler
{
    public boolean handleRequest(MessageContext mc) {
        checkIncoming((SOAPMessageContext)mc);
        return true;
    }
    public boolean handleResponse(MessageContext mc) {
        signOutgoing((SOAPMessageContext)mc);
        return true;
    }
    public boolean handleFault(MessageContext mc) {
        signOutgoing((SOAPMessageContext)mc);
        return true;
    }
}
                





回页首


配置 JAX-RPC SOAP 处理程序

为了完善这个示例,我配置并部署了处理程序。处理程序查找一个指定签署者名字的配置属性。更重要的是,运行时期望所有由处理程序处理的头元素都在部署信息中指定。

对于客户端,我将使用 JAX-RPC 程序化接口来部署处理程序。对于服务,我将使用针对 Web 服务部署描述符的 J2EE 来部署处理程序。

JAX-RPC 1.0 程序化配置:HandlerInfo()


HandlerInfo() (如 清单 7中所示)定义了处理程序及其配置和处理程序将要处理的头元素。在这个示例中,头元素是作为静态属性 ClientSignHandler.HEADERS 公开的。

清单 7. 设置 HandlerInfo()
        Map hConfig = new HashMap();
        hConfig.put(ClientSignHandler.SIGNERS_NAME_PROPERTY, "Linus");
        HandlerInfo hInfo = new HandlerInfo(ClientSignHandler.class,
                                            hConfig,
                                            ClientSignHandler.HEADERS);
                

为了注册处理程序,必须将 HandlerInfo() 添加(附加)到处理程序链的末端。对处理程序是否在处理程序链上不作任何假定是一个良好的实践。

清单 8. 注册处理程序
        // Obtain service, for JAX-RPC 1.0 this may be implementation specific.
        StockQuoteService service = ...;
        // Must match WSDL's port QName
        QName portQName = new QName("http://stock.webservices.example.org",
                                    "StockQuote");
        service.getHandlerRegistry().getHandlerChain(portQName).add(hInfo);
                

用于 J2EE1.3 部署模型的 Web 服务:webservices.xml


在 J2EE 管理的环境中,使用程序化接口将会导致异常。必须通过用于 Web 服务的 J2EE 部署描述符 webservices.xml 来指定部署在端口上的处理程序。

Web 服务部署描述符程序化地使用 HandlerInfo()来反映信息集。它指定了名字/值对配置属性( init-param )、处理程序类名( handler-class )以及处理程序理解的消息头( soap-header )。您可以在 清单 9中看到所有这些都在使用。

清单 9. webservices.xml
  . . .
    <port-component>
      <port-component-name>StockQuote</port-component-name>
      <wsdl-port>
        <namespaceURI>http://stock.webservices.example.org</namespaceURI>
        <localpart>StockQuote</localpart>
      </wsdl-port>
      . . .
      <handler>
        <handler-name>Signature Handler</handler-name>
        <handler-class>org.example.webservices.signature.
        ServiceSignHandler</handler-class>
        <init-param>
          <param-name>org.example.webservices.signature.SignersName</param-name>
          <param-value>Snoopy</param-value>
        </init-param>
        <soap-header>
          <namespaceURI>uri://
          org.example.webservices.signature.Sign</namespaceURI>
          <localpart>sign</localpart>
        </soap-header>
      </handler>
    </port-component>
  . . .
                





回页首


实现处理程序的一些最佳实践

我已经向您展示了一些设计和实现处理程序的最佳实践:

  • 在合适的地方利用消息流的对称性。客户端请求处理由服务响应/故障处理反映出来,而服务请求处理由客户端响应/故障处理反映出来,这是处理程序开发中的再现模式。可以提供基于传入/传出模式的通用实现来利用通用代码。

  • SOAPHeaderElement 的处理与头内容的处理分开。 在这个示例中,我将 SOAPHeaderElement 的处理与头内容的处理分开。处理程序定位并处理 SOAPHeaderElement 。通过将其内容抽象为单个 SOAPElement ,头元素内容的结构和处理已经委托给 SignatureTool

  • 理解的头应该由 HandlerInfo 或部署描述符定义。 这更多的是一条规则而不是一个最佳实践。所有要由处理程序处理的消息头都应该通过 HandlerInfo 或合适的部署描述符定义为运行时。JAX-RPC 没有为由处理程序创建的消息头提供相似的配置点。在以后的技巧中,将会再次讨论这个主题。





回页首


总结

现在基于 SOAP 的 Web 服务可以由协作处理传出消息和传入消息的客户端/服务处理程序来进行扩展。处理传出消息是非常简单的,然而处理传入消息却不是轻而易举的。在本技巧中,您了解了处理传入消息的发展旅程,并且知道运行时必须始终使用头元素(每个处理程序都将处理头元素)来配置。



参考资料



关于作者

Richard A. Sitze 是 IBM WebSphere Web 服务开发小组的成员。他参与了Apache Axis SOAP 引擎、Jakarta Commons Logging 以及 Jakarta Commons Discovery 开放源代码项目。他在 IBM 的前期工作包括 CORBA ORB 可互操作性以及 Internet 银行服务。他在加入 IBM 之前的工作包括业务系统、实时控制系统、防火墙、网络通信协议,线程内核以及多处理器 UNIX 内核开发。您可以通过 rsitze@us.ibm.com与 Richard 联系。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款