构建基于 J2ME 的安全 SOAP 客户机

 

(源自:https://www6.software.ibm.com/developerworks/cn/education/webservices/ws-soa-securesoap1/index.html

构建基于 J2ME 的安全 SOAP 客户机: 第 1 部分:探索 J2ME 的 Web 服务 API (WSA)

将安全组件集成到 WSA 中

第 1 页,共 11 页


对本教程的评价

帮助我们改进这些内容


级别: 中级

Bilal Siddiqui (bsiddiqui@xml4java.com), 自由顾问

2006 年 9 月 30 日

本系列教程共三部分,将介绍如何构建基于 Java 2 Micro Edition (J2ME) 的安全 Web 服务客户机。第 1 部分将介绍允许无线访问 Web 服务的应用程序场景。第 1 部分还将讨论安全 Web 服务应用程序的体系结构,并说明不同技术组件如何在 J2ME 应用程序中彼此协作来提供安全功能。我们将深入研究数个 WSA 应用程序的内部情况,从而对 J2ME 的 Web 服务 API (WSA) 进行详细讨论。本教程的后续部分将详细阐述这些概念,从而将安全机制加入到 WSA 应用程序中。

开始之前

关于本系列教程

本系列教程演示如何将基于 Java™ 2 Micro Edition (J2ME) 的无线访问中的安全机制集成到 Web 服务中。我们将在 J2ME MIDlet 中使用以下组件和技术:

 

  1. J2ME 的 Web 服务 API (WSA)
  2. 加密
  3. XML 数字签名
  4. Java Card

 

首先将介绍几个应用程序场景,将需要在其中为 Web 服务加入无线访问安全机制。

WSA 使用存根类的概念,因此加密、XML 签名和 Java Card 技术等其他技术组件必须适应 WSA 存根类。因此,我们将讨论 WSA 存根类如何工作,并说明如何将其他技术组件与 WSA 结合。

本系列教程还将说明用于集成不同技术组件的各种测试和调试参数。本系列最后要将所有概念放入到一个“存根增强器工具”中。此工具将通过加入安全功能来增强 WSA 存根类的功能。

 


回页首


关于本教程

本系列教程的第 1 部分将介绍集成各种 Internet 技术来在 J2ME 中构建安全客户端 Web 服务应用程序所涉及到的概念。

我们还将提供示例应用程序场景,并就不同技术组件如何一起工作来构建安全的 Web 服务客户机进行全面的体系结构讨论。

我们还将提供一张安全体系结构中不同模块的图片,并标识每个模块各自的角色。

对 WSA 存根类进行分析后,将进行体系结构讨论。我们采用的是循序渐进的方法:首先讨论一个简单 Web 服务的存根类,然后再讨论更为复杂的 Web 服务。

本教程的最后将介绍安全 Web 服务的接口。在本系列的后续部分,我们将实现各个安全功能。




回页首


先决条件

由于本教程讨论的都是集成各种技术组件的问题,因此务必对这些组件有基本的了解。具体来说,本文假定读者具有以下背景:

 

  1. 您应该为 Java 程序员,并对 J2ME MIDlet 有基本了解。
  2. WSA 使用 Web 服务描述语言(Web Services Definition Language,WSDL)和简单对象访问协议(Simple Object Access Protocol,SOAP)。因此,您需要知道 WSDL 接口如何映射到 SOAP 方法调用。
  3. 您还需要知道 W3C 的 XML 模式的基本知识,特别是 xsd:element 和 xsd:complexType 的使用。

 

而且,具有一定的 XML 签名方法的背景也会有所帮助。

IBM developerWorks 提供了很多有关这些主题的优秀文章和教程。参考资料部分列出了一些供参考的资料。




回页首


我是否应学习本教程?

本教程将指导您为 Web 服务启用安全无线访问。

如果您希望在 Web 服务的非无线访问中实现安全性,本教程也可提供一些有用的内容。




回页首


教程主题

第 1 部分组织为以下七个部分:

  1. 教程介绍
  2. 要求对 Web 服务进行无线访问的示例应用程序场景。此部分还将介绍 WSA 体系结构和存根类
  3. 说明为什么需要保证 Web 服务的无线访问的安全。此部分还包含了一个演示,用以说明不同技术组件如何一起工作来提供安全机制
  4. 示例客户端 WSA 应用程序的详细分析
  5. 示例客户端 WSA 应用程序的更多分析
  6. 讨论更为全面的 WSA 应用程序。此讨论提供关于 WSA 存根和其他类工作的所有详细信息,为了将安全机制集成到 WSA 中,需要对这些信息加以了解
  7. 总结




回页首


代码示例和安装要求

我们使用 J2ME Wireless Toolkit V2.2 来生成和测试第 1 部分的代码。

本教程稍后的部分还将需要以下软件工具,这些工具都可以免费下载(请参阅参考资料)。

  1. Sun Java Wireless Toolkit Version 2.3 Beta。我们在第 1 部分使用的是 2.2 版,因为在撰写第 1 部分时,2.3 版尚处于测试阶段。但我们将同时在 2.2 和 2.3 版上对本教程的后续部分的代码进行测试。
  2. IBM alphaWorks 提供的 XML Security Suite for Java (XSS4J)。
  3. Sun 网站提供的 Java Card Development Kit。

 




回页首

构建基于 J2ME 的安全 SOAP 客户机: 第 1 部分:探索 J2ME 的 Web 服务 API (WSA)

将安全组件集成到 WSA 中

第 2 页,共 11 页


对本教程的评价

帮助我们改进这些内容


Web 服务的无线访问

使用 Web 服务的无线客户机

Java Specification Request 172 (JSR 172) 定义了一个标准 API,J2ME 客户机可以使用其调用基于 SOAP 和 XML 的 Web 服务。此 API 是以 J2ME 的可选包的形式提供的,称为 J2ME 的 Web 服务 API (WSA)。WSA 实际上是 JSR 101 定义的 Java API for XML-based Remote Procedure Call (JAX-RPC) 的子集。

JAX-RPC 使用流行的 Web 服务概念“端点”和“客户机”。端点公开 Web 服务,而 JAX-RPC 客户机调用——访问、使用或利用——端点公开的服务。

作为 JAX-RPC 的子集,WSA 仅包含一组用于定义 Web 服务客户机的接口。由于 J2ME 设备并不可能公开其自己的 Web 服务端点,因此这样做很有意义。J2ME 设备仅期望使用服务端点公开的 Web 服务。此场景如图 1 中所示。

图 1:使用 Web 服务端点公开的服务的 Web 服务客户机

示例 Web 服务应用程序场景部分,我们提供了一些示例应用程序场景,将需要在其中公开 Web 服务,以供 J2ME 客户机使用。




回页首


使用 WSDL 生成存根类

JAX-RPC 和 WSA 使用 Web 服务描述语言 (WSDL) 来定义 Web 服务接口,并生成其客户端存根。Web 服务存根是充当实际 Web 服务端点的本地代理的类集。

MIDlet 应用程序将使用存根类来调用远程 Web 服务。MIDlet-存根的交互将为本地的,在 J2ME 设备中发生。存根类将处理所有与远程 Web 服务端点的通信。具体如图 2 中所示。

图 2:MIDlet-存根和存根-端点交互

Sun 的 J2ME Wireless Toolkit V2.2(请参阅参考资料)提供了一个存根生成器工具,该工具可接受一个 WSDL 文件为输入,然后为 WSDL 文件定义的 Web 服务生成存根类集。

我们将此集称为 WSA 存根类

参考资料部分提供了指向 developerWorks 文章“Web Services APIs for J2ME, Part 1: Remote service invocation API”的链接。通过阅读这篇文章,您可以了解 JSR 172 的体系结构,以及如何生成和利用 WSA 存根类来使用 Web 服务。

另一篇 developerWorks 文章“设计移动 Web 服务”(请参阅参考资料)讨论了在设计将通过无线使用的 Web 服务时涉及到的相关服务器端问题。可以参考此文章来了解为何需要对无线客户机使用的 Web 服务进行具体计划。




回页首


Web 服务无线访问的工作原理

以下是一些关于 Web 服务无线访问的工作原理的重要信息。

所有 JSR172 兼容的设备都必须支持简单对象访问协议 (SOAP) V1.1。不过请注意,JSR 172 并不要求 J2ME 设备根据 XML 1.0 支持 SOAP 编码(或表示形式)。这意味着 JSR 设备可以(也可能无法)理解 SOAP 消息的 XML1.0 表示。

如果 JSR 172 兼容的无线设备没有实现 XML1.0 编码,则可以使用 XML 信息集(Information Set,Infoset,请参阅参考资料)中定义的其他编码方式。在这种情况下,无线运营商的网络需要负责消息在其他编码方式与 XML 之间的转换,因此,送达 Web 服务端点的消息可以根据 XML 格式进行互操作。

这意味着服务端点将仅看到来自 WSA 设备的 SOAP/XML。此场景如图 3 中所示。

图 3:无线运营商的网络确保 SOAP 消息的 XML 编码

这还意味着,即使 J2ME 设备中的 WSA 实现不支持 XML 1.0,也必须产生能转换为可互操作的 XML 1.0 表示的 SOAP 表示。

让我们看一些示例应用程序场景,这些场景中的 Web 服务将需要供 J2ME 客户机应用程序使用。这些场景还将帮助您理解为何访问 Web 服务时需要安全机制。




回页首


示例 Web 服务应用程序场景

在很多情况下,都需要将 Web 服务向无线客户机公开。下面让我们看一组此类 Web 服务,这些 Web 服务允许 J2ME 设备用户使用手机进行一些预订操作。

假定您已实现了一组允许用户进行以下操作的 Web 服务:

  1. 在用户将要访问的城市预订酒店房间
  2. 检查重要邮件,如来自特定发件人的邮件
  3. 向任何当地餐厅订餐
  4. 预订出租车服务
  5. 支付一定款项
  6. 从用户的当前位置呼叫紧急汽车维修或救护服务

您的客户可能会随时需要此类服务,因此我们将其称为“日常 Web 服务”(Everyday Web services)。

为了提供日常 Web 服务,您将需要与不同位置的服务提供商进行相应的安排。例如,您将需要在不同物理位置开展业务的合作出租车服务。类似地,您将需要位于不同城市的合作酒店和餐厅。

您将为日常 Web 服务中包含的每个服务设计 Web 服务端点。您的合作公司(如出租车服务、餐厅和酒店)将实现和托管您设计的端点。

从这些服务本身的特性可以看出,只有在用户能够无线访问这些服务的情况下,他们才能从此类 Web 服务获得真正的价值。

因此,您还将向日常 Web 服务用户提供 J2ME 应用程序。J2ME 应用程序将在您客户的手机上运行,允许他们以移动的方式访问合作伙伴的服务。我将此 J2ME 应用程序称为“日常 MIDlet” (Everyday MIDlet)。

我们需要使用一系列 Web 服务应用程序场景来演示如何设计、开发、实现和测试 Web 服务的安全移动访问。您的“日常 Web 服务”示例提供了所需的各种 Web 服务。

因此,我们将在本教程中使用日常 Web 服务作为示例应用程序场景。

我们将首先给出您的日常 MIDlet 将发送到 Web 服务端点的 SOAP 消息。




回页首


日常 Web 服务的 SOAP 消息

关于 WSDL 和 SOAP 术语的说明

在 WSDL 术语中,“SOAP 方法调用”是一个“操作”。本教程中使用的术语“方法调用”和“操作”是可互换的。


假定您的某个客户 Alice 正在前往附近一个城市的路上,希望在酒店中预订一间房间。她将使用日常 MIDlet 来向日常 Web 服务端点发送 SOAP 消息,以查找合适的房间。

例如,她可能希望首先检查特定地区中特定房价范围内的房间。一旦知道有可用房间,可能希望选择并预订房间。

请参见清单 1,其中显示了 Alice 将发送用于检查特定地区中特定房价范围内的房间可用性的 SOAP 消息:
清单 1:Alice 用于检查房间可用性的 SOAP 消息

                    
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:tns="http://www.everdaywebservices.com/hotelservice">
<soap:Body>
<tns:checkForRoomAvailability>
   <roomRequired>
      <city>Lahore</city>
      <area>Gulberg</area>
      <maxRent>2500</maxRent>
   </roomRequired>
</tns:checkForRoomAvailability>
</soap:Body>
</soap:Envelope>

请注意,清单 1 中的 SOAP 消息包含对名为 checkForRoomAvailability 的远程方法的方法调用。

checkForRoomAvailability 方法调用包含具有以下三个子元素的 roomRequired 元素:

  1. city(Alice 将要查找酒店房间的城市)
  2. area(Alice 希望逗留的地区的名称)
  3. maxRent(Alice 愿意支付的最高价格)。

 

清单 2 显示了日常 Web 服务的响应。
清单 2 对 Alice 的 checkForRoomAvailability 调用的响应

                    
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:tns="http://www.everdaywebservices.com/hotelservice">
<soap:Body>
<tns:checkForRoomAvailabilityResponse>
   <tns:roomsAvailable>
      <room>
        <hotelName>Hotel1</hotelName>
        <location>Gulberg 2</location>
        <roomType>BusinessClass</roomType>
        <roomRent>2500</roomRent>
      </room>
      <room>
        <hotelName>Hotel2</hotelName>
        <location>Gulberg 5</location>
        <roomType>Economy</roomType>
        <roomRent>2000</roomRent>
      </room>
      <room>
        <hotelName>Hotel3</hotelName>
        <location>Gulberg 3</location>
        <roomType>Economy</roomType>
        <roomRent>1800</roomRent>
      </room>
   </tns:roomsAvailable>
</tns:checkForRoomAvailabilityResponse>
</soap:Body>
</soap:Envelope>
        

请注意,清单 2 中的 roomsAvailable 元素包含一系列 room 元素。每个 room 元素包含 hotelNamelocationroomType 和 roomRent 元素。

我将这个与酒店相关的日常 Web 服务称为“日常酒店服务”(Everday hotel service) 或直接称为“酒店服务”(hotel service)。




回页首


日常酒店服务的其他一些 SOAP 消息

请参见清单 3,其中显示了对名为 reserve 的方法的 SOAP 调用。
清单 3:reserve SOAP 方法调用

                    
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:tns="http://www.everdaywebservices.com/hotelservice">
<soap:Body>
<tns:reserve>
   <room>
    <hotelName>Hotel3</hotelName>
    <location>Gulberg 3</location>
    <roomType>Economy</roomType>
    <roomRent>1800</roomRent>
  </room>
</tns:reserve>
</soap:Body>
</soap:Envelope>
       

reserve 方法接受一个 room 元素并返回预订状态,如清单 4 中所示。
清单 4:reserve 方法调用的响应中返回的预订状态

                    
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:tns="http://www.everdaywebservices.com/hotelservice">
<soap:Body>
<tns:reserveResponse>
  <bookingStatus>Confirmed</bookingStatus>
</tns:reserveResponse>
</soap:Body>
</soap:Envelope>
       

日常酒店服务的 WSDL 定义

清单 5 显示了日常酒店服务的 WSDL 定义,其中包含两个操作(checkForRoomAvailability 和 reserve)。
清单 5:酒店服务的 WSDL

                    
<?xml version="1.0" encoding="UTF-8"?>
<definitions 
    xmlns="http://schemas.xmlsoap.org/wsdl/" 
    xmlns:tns="http://www.everydaywebservices.com/hotelservice" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
    name="hotelService" 
    targetNamespace="http://www.everydaywebservices.com/hotelservice">
   <types>
    <schema xmlns="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.everydaywebservices.com/hotelservice">
      <xsd:element name="roomRequired">
        <xsd:complexType>
           <xsd:sequence>
            <xsd:element name="city" type="xsd:string"/>
            <xsd:element name="area" type="xsd:string"/>
            <xsd:element name="maxRent" type="xsd:int"/>
           </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="room">
        <xsd:complexType>
            <xsd:sequence>
              <xsd:element name="hotelName" type="xsd:string"/>
              <xsd:element name="location" type="xsd:string"/>
              <xsd:element name="type" type="xsd:int"/>
              <xsd:element name="rent" type="xsd:int"/>
            </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="roomsAvailable">
        <xsd:complexType>
         <xsd:sequence>
            <xsd:element name="room" ref="tns:room" minOccurs="0"
            maxOccurs="unbounded"/>
         </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="bookingStatus" type="xsd:string"/>
    </schema>
   </types>
<message name="roomRequired">
  <part name="roomRequired" element="tns:roomRequired"/>
</message>
<message name="roomsAvailable">
  <part name="roomsAvailable" element="tns:roomsAvailable"/>
</message>
<message name="room">
  <part name="room" element="tns:room"/>
</message>
<message name="bookingStatus">
  <part name="bookingStatus" element="tns:bookingStatus"/>
</message>
<portType name="hotelService">
   <operation name="checkForRoomAvailability">
    <documentation>
      Check room availability by city, location and rent. 
    </documentation>
    <input message="tns:roomRequired"/>
    <output message="tns:roomsAvailable"/>
  </operation>
  <operation name="reserve">
    <documentation>
      Reserve a room.
    </documentation>
    <input message="tns:room"/>
    <output message="tns:bookingStatus"/>
  </operation>
</portType>
<binding name="hotelServiceBinding" type="tns:hotelService">
 <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
 <operation name="checkForRoomAvailability">
    <input>
      <soap:body use="literal"/>
    </input>
    <output>
      <soap:body use="literal"/>
    </output>
    <soap:operation
 soapAction="http://www.everdaywebservices.com/hotelservice/checkForRoomAvailability"/>
  </operation>
 <operation name="reserve">
    <input>
      <soap:body use="literal"/>
    </input>
    <output>
      <soap:body use="literal"/>
    </output>
    <soap:operation 
       soapAction="http://www.everdaywebservices.com/hotelservice/reserve"/>
  </operation>
  </binding>
   <service name="HotelService">
   <port name="hotelServicePort"
        binding="tns:hotelServiceBinding">
    <soap:address location="http://www.everdaywebservices.com/hotelService"/>
  </port>
</service>
</definitions>

我们不打算深入讨论清单 5 中使用的 WSDL 格式的细节,但要指出的是,清单 5 中包含四个重要的 WSDL 组件:

 

  1. 为酒店服务定义的数据类型(清单 5 中的 types 元素)
  2. 输入和输出消息(清单 5 中的 message 元素)
  3. 端口类型定义(清单 5 中的 portType 元素)
  4. 与传输机制的端口类型绑定(清单 5 中的 binding 元素)
  5. 端口的详细信息(清单 5 中的 service 元素)

 




回页首


酒店 Web 服务的 WSDL 组件

以下是酒店服务的五个 WSDL 组件的简单描述。

清单 5 中的 types 元素定义在酒店服务中使用的数据类型。请注意 xsd:element 里的名为 room 的复杂类型定义,该定义提供了对酒店房间的简单描述。其中包含四个子元素,即 hotelNamelocationtype 和 rent

hotelName 元素指定酒店的名称。location 元素以文本字符串的格式指定酒店的物理位置。type 和 rent 元素分别指定房间的类型和房价。

清单 5 中的 message 元素定义在 Internet 上传递的用于调用远程服务的方法(即在前面的清单 1 中看到的 checkForRoomAvailability SOAP 消息)。

请注意,清单 5 中的每个 message 元素都具有一个 part 子元素。part 元素定义消息的部分。为了简单起见,我们在本教程中使用仅包含一个部分的消息。因此,我们在所有 message 元素中都将看到一个 part 子元素。

每个 part 元素都使用其 element 属性引用 types 元素中的一个数据类型定义。例如,清单 5 中的 part 元素的 element 属性的值为 roomRequired。此值与 types 元素内的 xsd:element 的 name 属性匹配。

清单 5 中的 portType 元素定义酒店服务的端口类型。端口类型定义指定 Web 服务的抽象接口,其中包含 Web 服务中包括的各个操作。例如,清单 5 中所示的酒店服务的 WSDL 包含两个操作(checkForRoomAvailablity 和 reserve)。可以在清单 5 中的 portType 元素找到两个 operation 子元素,一个用于 checkForRoomAvailability 操作,而另一个用于 reserve 操作。

清单 5 中的 binding 元素为 Web 服务定义消息编码和传输协议 SOAP。对于本教程中讨论的所有日常 Web 服务,我都将使用 SOAP 绑定。

请特别注意 soap:operation element清单 5 中 binding 元素的二级子元素)的 soapAction 属性。soapAction 是特定于 SOAP 的属性,指定 SOAP 服务器将如何调用 SOAP 方法。

清单 5 中的 service 元素定义承载远程 Web 服务的服务端点。

请注意,相同的 Web 服务通常由提供相同服务的不同端点承载,即,可能会有不同的合作公司在不同城市提供酒店服务。

这意味着不同的服务端点将承载您的端口类型,所承载的每个服务都将提供自己的服务的详细信息。

例如,清单 5 中的 service 元素有一个名为 soap:address 的二级子元素。soap:address 具有一个名为 location 的属性,用于指定端点的网络地址。每个服务端点将指定其自身的 location 属性值。

在稍后的探索存根类部分,我们会在 WSDL 组件与 WSA 存根类之间建立联系,说明存根类如何表示 Web 服务的客户端实现。

您已经了解了 SOAP 格式以及酒店服务的 WSDL 定义。酒店服务目前并不安全。下一部分将讨论酒店服务中的安全机制。




回页首


构建基于 J2ME 的安全 SOAP 客户机: 第 1 部分:探索 J2ME 的 Web 服务 API (WSA)

将安全组件集成到 WSA 中

第 3 页,共 11 页


对本教程的评价

帮助我们改进这些内容


保证 Web 服务无线访问的安全

为什么需要在酒店服务中提供安全机制?

清单 5 的酒店服务中,您看到了两个操作 checkForRoomAvailability 和reserve

您可能会希望允许普通公众调用您的酒店服务的 checkForRoomAvailability方法。这意味着公众中任何人都能够随时检查任何位置的房间可用性情况,而不用为您的酒店服务支付任何费用。

因此,checkForRoomAvailability 操作并不需要任何身份验证和授权之类的安全措施。

另一方面,reserve 操作的确要求在代表客户预订房间前对发出请求的客户进行身份验证。这是因为您需要对酒店预订收费,但是不能在没有确定客户真的需要这项服务的情况下收取费用。

这意味着需要一个身份验证机制来标识请求预订服务的客户。这样可确保服务器认为在为某个客户提供服务时,没有任何其他人(如黑客)能够访问该服务。

在本教程中,我们使用数字签名作为身份验证机制。

在下一部分,我们将回顾一下使用数字签名时可用的加密选项,以保证您的 Web 服务无线访问的安全。然后,我们将提供清单 5 中所示的“reserve”SOAP 消息的一个增强版本。我们将对允许把身份验证数据合并到 SOAP 消息中的增强功能进行说明。

随后,我们将说明不同的无线和安全相关技术如何协作,从而构建安全的 J2ME Web 服务客户机。我们还将列出 J2ME 中的一些限制,在开始为 Web 服务构建安全 J2ME 客户机前,必须对这些限制加以考虑。




回页首


公钥和密钥加密

可以使用公钥或密钥加密来将身份验证逻辑加入到 SOAP 请求消息中。

密钥加密使用通信方共享保密信息的概念。Kerberos 是一项非常受欢迎的安全协议,定义了用于安全交换共享保密信息的机制。

IBM developerWorks 提供了一些非常不错的参考资料,介绍了如何在 J2ME 应用程序中使用 Kerberos 密钥。请参阅参考资料部分,以了解有关在 J2ME MIDlet 中使用 Kerberos 的更多信息。

公钥加密并不需要共享保密信息。相反,这种加密方法允许生成一对密钥,分别称为私钥和公钥。

应将私钥严密保存,而不应提供给任何人。另一方面,任何人都可以获得您的公钥,如您的朋友、家人和同事。

您将使用私钥产生加密签名,您的朋友将使用公钥对此签名进行验证。如果消息上的签名能够通过使用公钥的加密验证,则意味着消息的确是由您发出的。

无论使用密钥或公钥,都可以使用本教程中提供的安全体系结构。




回页首


使用 Java Card 技术

我们在本系列教程中将结合使用多种技术。其中的一项技术就是 Java Card。Java Card 技术提供了安全存储保密信息、私钥和密钥的方法。

Java Card 技术属于智能卡类型的技术,智能卡是磁卡的增强版本,其存储信息的容量要大数千倍。智能卡还能够包含计算和处理逻辑——用于计算数字签名的逻辑。参考资料部分提供了一些链接,可通过这些链接找到讨论智能卡和 Java Card 的文章。

Java Card 技术是一种智能卡技术,智能卡是磁卡的增强版本,其存储信息的容量要大数千倍。智能卡还能够包含计算和处理逻辑——用于计算数字签名的逻辑。参考资料部分提供了一些链接,可通过这些链接找到讨论智能卡和 Java Card 的文章。

在本教程中,我们将 Java Card 技术用于两个用途:

  1. 安全存储加密密钥
  2. 实现签名计算逻辑

 

以下是我们将使用 Java Card 技术的场景。

假定 Alice 是 Java Card 用户,她希望使用 Java Card 产生加密签名。她将需要在访问 Java Card 时提供用户名和密码。

用户名-密码对将只能用于 Alice 的 Java Card。因此,如果黑客盗走了她的 Java Card,黑客还将需要知道该特定 Java Card 的用户名-密码对。

这种类型的安全机制有时候称为双要素安全机制,在如何使用 SATSA 将 Java Card 集成到 J2ME MIDlet 中的 developerWorks 文章中对此机制进行了说明(请参阅参考资料)。

为了与 Java Card 通信,J2ME 设备将需要使用称为 Security and Trust Services API (SATSA) 的 API。在本系列的第三篇教程中,我们将说明 WSA 存根类如何使用 SATSA 与 Java Card 应用程序进行通信。




回页首


使用 XML 签名

本教程中另一个重要技术是 XML 签名。我们使用 XML 签名来将加密签名包装到 XML 消息中。为此,我们将使用 W3C 制订的 XML 签名规范(请参阅参考资料)。

清单 6 显示了清单 3 中所示的 reserve 方法调用请求的增强版本。
清单 6:reserve SOAP 请求的签名版本

                    
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:tns="http://www.everydaywebservices.com/hotelservice">
<soap:Body>
<tns:reserve Id="R1">
 <room>
    <hotelName>Hotel3</hotelName>
    <location>Gulberg 3</location>
    <roomType>Economy</roomType>
    <roomRent>1800</roomRent>
 </room>
</tns:reserve>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
 <SignedInfo>
   <CanonicalizationMethod 
     Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">
    </CanonicalizationMethod>
    <SignatureMethod 
     Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1">
 </SignatureMethod>
 <Reference URI="#R1">
    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod>
    <DigestValue> gmwIRIS+Od1t7+kBmCEsflI4I3U=</DigestValue>
   </Reference>
 </SignedInfo>
 <SignatureValue>
    VfWjfJqN7AV78v+Ye8B3qACRmhzC+FCXZTp78DJpn1bP5hUSimMHwgh
    Wf/F0M9D1PoLZUN6mG131MiDDfSCVQo/4zEo9aw+seyfAGh0rvxQ/3x+LCz9c5pEfmy+Um1/1I+EBOrT
    NxFPMyykBq3JhRoRt3VZIwGzRxkjq+cWOR4c=
 </SignatureValue>
 <KeyInfo>
   <KeyName>Alice</KeyName>
  </KeyInfo>
</Signature>
</soap:Body>
</soap:Envelope>

如果将清单 3 与其增强版本清单 6 作一下比较,您将发现清单 6 中的 reserve 方法包含了一个 Signature 元素。清单 6 中的 Signature 元素包含三个子元素:

  1. Signature 元素的 SignedInfo 子元素指定用于产生加密签名的算法。其中还包含对用于进行签名操作来产生 XML 签名的数据的引用。
  2. SignatureValue 子元素指定实际的加密签名值。
  3. KeyInfo 元素具有一个 KeyName 子元素,可指定用于产生签名值的密钥。

 

对于本教程,我们主要关注如何保证 Web 服务的无线访问的安全,而不打算详细讨论 XML 签名的细节。IBM developerWorks 已经提供了很多详细说明 XML 签名格式的参考资料。本教程的参考资料部分提供了有关 XML 签名的进一步信息。

您可以专门参考一下 Secure XML messaging with JMS, Part 1(developerWorks,2005 年 11 月)的“Data wrapped inside an XML signature”部分,其中对 Signature 元素的三个子元素进行了详细讨论。




回页首


WSA 的一个重要限制

使用 Web 服务的无线客户机部分中提到的,JSR 172 将 WSA 定义为 JAX-RPC 的一个子集。在定义 WSA 功能时,JSR 172 考虑到 J2ME 设备的计算资源有限。因此 JSR 172 排除了一些 JAX-RPC 功能。

保证 Web 服务无线访问的安全时必须面对的一个重要 WSA 限制是,WSA 中无法创建 XML 属性。

实例化简单电子邮件服务的元素部分,我们将通过演示说明 WSA 无法创建任何 XML 元素。这意味着不能在 SOAP 请求消息中包含 XML 属性。

另一方面,根据 XML 签名规范,清单 6 中的 Signature 元素只规定包含了若干 XML 属性,如清单 6中的 Reference 元素的 URI 属性。这意味着无法使用当前的 WSA 版本创建严格符合 XML 签名规范的Signature 元素。




回页首


克服 WSA 限制

清单 7 显示了前面的清单 6 中的 SOAP 消息的一个经过编辑的版本。清单 6 和清单 7 的唯一区别在于,我们使用 content 或 element 节点对清单 6 的 attribute 节点进行了更改。
清单 7:WSA 可以创建的带 Signature 元素的 SOAP 请求

                    
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:xsd="http://www.w3.org/2001/XMLSchema"
     xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
     xmlns:tns="http://www.everydaywebservices.com/hotelservice">
<soap:Body>
<tns:reserve Id="R1">
   <room>
    <hotelName>HotelGulberg</hotelName>
    <location>GulbergIII</location>
    <roomType>Economy</roomType>
    <roomRent>1800</roomRent>
 </room>
</tns:reserve>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
  <CanonicalizationMethod>
     http://www.w3.org/TR/2001/REC-xml-c14n-20010315
  </CanonicalizationMethod>
  <SignatureMethod>
    http://www.w3.org/2000/09/xmldsig#rsa-sha1
 </SignatureMethod>
 <Reference>
     <DigestMethod>
        http://www.w3.org/2000/09/xmldsig#sha1
     </DigestMethod>
     <DigstValue xmlns="">gmwIRIS+Od1t7+kBmCEsflI4I3U=</DigstValue>
     <URI>#R1</URI>
  </Reference>
</SignedInfo>
<SignatureValue>
    VfWjfJqN7AV78v+Ye8B3qACRmhzC+FCXZTp78DJpn1bP5hUSimMHwgh
    Wf/F0M9D1PoLZUN6mG131MiDDfSCVQo/4zEo9aw+seyfAGh0rvxQ/3x+LCz9c5pEfmy+Um1/1I+EBOrT
    NxFPMyykBq3JhRoRt3VZIwGzRxkjq+cWOR4c=
</SignatureValue>
<KeyInfo>
  <KeyName>userName</KeyName>
</KeyInfo>
</Signature>
</soap:Body>
</soap:Envelope>

请注意,仍然可以在服务器端使用标准 XML 签名处理工具对清单 7 中的 Signature 元素进行处理。这将需要一个简单的 XML 转换。

在本系列教程的第 2 部分和第 3 部分,我们将演示如何使用 XML Security Suite for Java (XSS4J);XSS4J 是 IBM alphaWorks 提供的开放源代码 XML 签名处理和创建工具,可用于处理和验证 WSA 客户机创建的签名。




回页首


实现 WSA 安全机制的步骤

前面我们已经对本系列教程中将使用的不同技术组件(WSA、加密、XML 签名和 Java Card)进行了讨论,接下来要将所有这些组件组合到一起。

图 4 显示了以下步骤:

  1. 假定酒店服务用户 Alice 希望访问酒店服务来预订一个房间。Alice 的 J2ME 手机上在运行安全的日常 MIDlet。因此,她将调用这个 MIDlet。
  2. MIDlet 会将请求传递给本系列教程中构建的增强存根类。如图 4 中所示,增强存根类使用四种技术——XML 签名、加密、SATSA/Java Card 和 WSA——来创建包装用户身份验证数据的安全 reserve SOAP 请求。
  3. 增强存根类使用加密和 SATSA 来获取安全 SOAP 请求所需的所有加密支持。
  4. SATSA 将随后使用 Java Card 应用程序基于 Alice 的 SOAP 请求计算加密签名值。
  5. 接下来,增强存根类使用 XML 签名支持来创建完整的 XML 签名,并将签名包装在 SOAP 请求中。
  6. 增强存根类使用 WSA 框架将请求发送到远程 Web 服务端点实现。
  7. 远程 Web 服务实现将需要对传入 reserv SOAP 请求进行转换(请参阅克服 WSA 限制部分),然后才进行处理。远程 Web 服务中承载的转换模块将进行此转换任务。我们将在本系列教程的第 3 部分构建此转换模块。
  8. 转换模块会将请求传递给实际 Web 服务实现。
  9. Web 服务将执行预订工作。
  10. Web 服务会将预订状态以 SOAP 响应的形式发送回 WSA 框架。
  11. WSA 将处理 SOAP 请求,提取预订状态,并向 Alice 显示同一个信息。


图 4:Web 服务无线访问的安全组件
 





回页首


增强存根类的目标

在本系列的以后部分,我们将讨论以下两点:

  1. 分析存根生成器工具生成的 WSA 存根类包含的内容
  2. 如何扩展和增强存根类,以加入安全机制

不过,在开始构建这一组增强存根类前,我们希望对几个重要的方面强调一下:

  1. 您现在应该能够直接将安全 XML 消息中的身份验证代码(清单 7 的 Signature 元素)转换为标准 W3C XML 签名格式(清单 6 的 Signature 元素)。这意味着应该能够使用标准签名处理工具处理 XML 签名。
  2. J2ME 设备除了包含 WSA 和 SATSA 实现外,您不应对 J2ME 设备进行任何假设,如假定其包含任何专用 API 等。这意味着,您的增强存根类集应能在任何具有 WSA 和 SATSA 的 J2ME 设备上工作。

知道了这些后,我们将开始讨论存根类,在下一部分中我们将对其进行增强。




回页首



构建基于 J2ME 的安全 SOAP 客户机: 第 1 部分:探索 J2ME 的 Web 服务 API (WSA)

将安全组件集成到 WSA 中

第 4 页,共 11 页


对本教程的评价

帮助我们改进这些内容


探索存根类

示例 WSDL 文件

我们将使用前一部分中的酒店服务作为示例应用程序,以说明 J2ME 中的客户端 Web 服务应用程序的使用模式和体系结构。现在我们将了解在 J2ME 中开发安全 Web 服务客户机应用程序的实现细节。

我们在使用 WSDL 生成存根类部分提到,WSA 使用从 WSDL 文件生成的存根类来调用 Web 服务。

在本部分,我们将使用随 J2ME Wireless Toolkit Version 2.2 一起提供的存根类生成器工具来为简单的 WSDL 文件生成存根类。我们随后将对存根类进行分析,以便在本系列教程的第 2 部分说明如何增强存根类来包含安全机制。

首先,我们将分析一个简单的 WSDL 文件的存根类,该文件并不具有复杂数据类型和很多操作。这将帮助说明存根类如何工作。因此,在此部分,我们将考虑生成简单的电子邮件服务的 WSDL(清单 8),并以此来讨论存根类。

我们将此服务称为简单移动电子邮件服务 (simple mobile email service) 或简单电子邮件服务 (simple email service)。

在本系列教程的第 3 部分,我们将再次讨论酒店服务并提供其安全的客户端 J2ME 实现。
清单 8:简单电子邮件服务的 WSDL

                    
<?xml version="1.0" encoding="UTF-8"?>
<definitions 
  xmlns="http://schemas.xmlsoap.org/wsdl/" 	
  xmlns:tns="http://www.everydaywebservices.com/emailservice" 
  xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
  name="emailService" 
  targetNamespace="http://www.everydaywebservices.com/emailservice">
  <types>
   <schema xmlns="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.everydaywebservices.com/emailservice">
    <xsd:element name="subjectsList">
        <xsd:complexType>
         <xsd:sequence>
            <xsd:element name="subjectLine" type="xsd:string" minOccurs="0"
                maxOccurs="unbounded"/>
         </xsd:sequence>
       </xsd:complexType>
     </xsd:element>
     <xsd:element name="senderEmailAddress" type="xsd:string"/>
   </schema>
  </types>
  <message name="senderEmailAddress">
    <part name="senderEmailAddress" element="tns:senderEmailAddress"/>
  </message>
  <message name="subjectsList">
    <part name="subjectsList" element="tns:subjectsList"/>
  </message>
 
  <portType name="simpleMobileEmailService">
    <operation name="getSubjects">
      <documentation>
        Get subject lines of emails from a particular sender.
      </documentation>
      <input message="tns:senderEmailAddress"/>
      <output message="tns:subjectsList"/>
    </operation>
  </portType>
  <binding name="simpleMobileEmailServiceBinding" type="tns:simpleMobileEmailService">
   <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
   <operation name="getSubjects">
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
      <soap:operation soapAction=
         "http://www.everydaywebservices.com/emailservice/getsubjects"/>
    </operation>
  </binding>
  <service name="SimpleMobileEmailService">
    <port name="simpleMobileEmailServicePort"
        binding="tns:simpleMobileEmailServiceBinding">
      <soap:address location="http://www.everydaywebservices.com/emailservice"/>
    </port>
   </service>
</definitions>

简单电子邮件服务仅包含一个操作 getSubjects,该操作以电子邮件地址为输入参数,将获取从该邮件地址接收到电子邮件的主题行。

日常服务的用户将在预期有来自特定发件人的电子邮件时使用此简单电子邮件服务。




回页首


简单电子邮件服务的 SOAP 消息

清单 9 显示了与 getSubjects SOAP 请求对应的 SOAP 消息。
清单 9: getSubjects SOAP 请求

                    
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:tns="http://www.everydaywebservices.com/emailservice">
<soap:Body>
<tns:getSubjects>
   <tns:senderEmailAddress>
     bob@mycompany.com
   </tns:senderEmailAddress>
</tns:getSubjects>
</soap:Body>
</soap:Envelope>

清单 10 显示了与 getSubjects SOAP 请求对应的 SOAP 响应。请注意,响应由一个 subjectsList 元素组成,其中包含数个 subjectLine 子元素。每个 subjectLine 子元素都包含一封电子邮件的主题行。
清单 10:getSubjects 请求的 SOAP 响应

                    
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:tns="http://www.everydaywebservices.com/emailservice">
<soap:Body>
<tns:subjectsList>
   <tns:subjectLine>BirthdayParty_Invitation</tns:subjectLine>
   <tns:subjectLine>GetTogather_update_dated20060404</tns:subjectLine>
</tns:getSubjects>
</soap:Body>
</soap:Envelope>





回页首


简单电子邮件服务的存根类

请注意,清单 8 中包含了在日常酒店服务的 WSDL 定义中看到的所有五个 WSDL 组件,即数据类型、输入和输出消息、端口类型、绑定和端点详细信息。

下面让我们了解一下 WSDL 文件的这些组件如何反映在存根生成器工具生成的存根类中。此时,您可以使用存根生成器工具为清单 8 的 WSDL 文件生成存根类。

您将看到存根生成器将生成以下三个文件。

  1. SimpleMobileEmailService_PortType
  2. SimpleMobileEmailService_PortType_Stub
  3. SubjectsList

简单电子邮件服务的相应文件可从本教程的源代码下载部分获得。

SubjectsList 文件在此阶段并非真的必要。在将 WSDL 端口类型与存根接口类匹配部分,您将了解到关于此文件的更多信息。我们暂时仅讨论前两个存根文件。




回页首


在存根文件中反映端口类型

我们首先看一下 SimpleMobileEmailService_PortType.java 文件,从而开始对存根类的分析;该文件中包含一个名为 SimpleMobileEmailService_PortType 的接口。清单 11 显示了 SimpleMobileEmailService_PortType 接口。
清单 11:SimpleMobileEmailService_PortType 接口

                    
public interface SimpleMobileEmailService_PortType extends java.rmi.Remote {
	public java.lang.String[] getSubjects(java.lang.String senderEmailAddress) 
       throws java.rmi.RemoteException;
}

SimpleMobileEmailService_PortType 接口对 java.rmi.Remote 接口进行了扩展,而后者属于 J2ME 中的远程方法调用(Remote Method Invocation,RMI)框架。

RMI 框架设计用于方便远程对象的方法调用。

只要您的应用程序要求使用 RMI 调用远程对象的方法,就需要使用一个公开与远程对象公开的方法相同的方法的本地对象。

您的应用程序将调用本地对象的方法,而此本地对象将处理与远程对象的通信。您可以说本地对象充当了实际远程对象的本地代理或存根。

java.rmi.Remote 接口并不包含任何方法。它只是显示实现此接口的类实际上是远程对象的本地代理。

WSA 使用 RMI 框架来调用远程 Web 服务。您可能已猜到,WSA 存根类应该实现相应的逻辑来启用与远程 Web 服务的 SOAP 通信。

SimpleMobileEmailService_PortType 接口中仅包含一个方法 getSubjects()。这是因为清单 8 的 WSDL 文件(用于生成 SimpleMobileEmailService_PortType 接口)仅包含一个操作。

请注意,SimpleMobileEmailService_PortType 接口的名称与清单 8 中的 WSDL 文件的 portType 元素的 name 属性匹配,不过其第一个字母大写了,并在 name 属性值后面追加了“_PortType”。




回页首


将 WSDL 端口类型与存根接口类匹配

您可以看到,清单 8 所示的 WSDL 文件中的三个事项与清单 11 的 SimpleMobileEmailService_PortType 接口匹配:

  1. 简单电子邮件服务操作的名称: 
    清单 8 中,只有一个 operation 元素,其 name 属性值设置为 getSubjects。此值与清单 11 所示的 SimpleMobileEmailService_PortType 接口的 getSubjects() 方法匹配。
    这意味着 WSDL 操作的名称映射到了存根接口类中的一个方法名称。
  2. 传递给 Web 服务操作的参数: 
    清单 6 所示的 WSDL 文件中的 operation 元素具有定义 getSubjects 操作调用的输入参数的 input 子元素。input 元素具有一个 message 属性,其值 (tns:senderEmailAddress) 与清单 8 中的第一个 message 元素的 name 属性值匹配。
    这意味着 getSubjects 操作调用的输入参数的名称为 senderEmailAddress
    现在让我们看看清单 11 所示的 SimpleMobileEmailService_PortType 接口中的 getSubjects() 方法调用对应的 senderEmailAddress 参数。此参数名称与 WSDL 文件中定义的输入参数匹配。
    还要注意,清单 8 所示的 WSDL 文件中的 senderEmailAddress 参数的数据类型为 xsd:string。而清单 11 中的 senderEmailAddress 参数的数据类型为 String。这意味着存根生成器自动将 XML xsd:string 数据类型映射为 Java 中的 String 对象。
  3. Web 服务操作返回的数据类型: 
    现在看看清单 8 中的 getSubjects operation 元素的 output 子元素。其 message 属性的值为 subjectsList,类型为 xsd:complexType。WSA 将 subjectLine 复杂类型映射到名为 SubjectsList 的存根类(简单电子邮件服务的存根类部分中列出的第三个文件)。SubjectsList 类将主题行序列包装在 String 对象数组中。
    通常,存根生成器会将复杂类型的输入和输出 WSDL 参数映射到自定义对象。在本例中,输出复杂类型映射到 SubjectsList 类。
    不过,SubjectsList 类非常简单。它仅包装一个 String 对象数组。在稍后的使用 Operation 对象调用远程服务部分,您将看到 SubjectsList 类根本不使用存根类。相反,它将直接使用一个 String 数组。

扩展电子邮件服务并保证其安全部分,我们将对简单电子邮件服务进行增强,从而提供存根类实际用于映射输入和输出参数的自定义对象的示例。




回页首


存根生成器实现端口类型接口的方式

下面介绍存根生成器如何实现简单电子邮件服务的客户端,我们刚刚在前一部分中讨论了其端口类型。

SimpleMobileEmailService_PortType 接口在名为 SimpleMobileEmailService_PortType_Stub 的类中实现,已经将其对应内容复制到清单 12 中。
清单 12:SimpleMobileEmailService_PortType_Stub 类

                    
public class SimpleMobileEmailService_PortType_Stub implements 
       simple.SimpleMobileEmailService_PortType, 
       javax.xml.rpc.Stub 
{
    private String[] _propertyNames;
    private Object[] _propertyValues;
    public SimpleMobileEmailService_PortType_Stub() {
        _propertyNames = new String[] {ENDPOINT_ADDRESS_PROPERTY};
        _propertyValues = new Object[] {
                         "http://www.everydaywebservices.com/emailservice"};
    }
    /***************************
    // Handling RPC properties
    /***************************
    public void _setProperty(String name, Object value) {
        int size = _propertyNames.length;
        for (int i = 0; i < size; ++i) {
            if (_propertyNames[i].equals(name)) {
                _propertyValues[i] = value;
                return;
            }
        }
        String[] newPropNames = new String[size + 1];
        System.arraycopy(_propertyNames, 0, newPropNames, 0, size);
        _propertyNames = newPropNames;
        Object[] newPropValues = new Object[size + 1];
        System.arraycopy(_propertyValues, 0, newPropValues, 0, size);
        _propertyValues = newPropValues;
        _propertyNames[size] = name;
        _propertyValues[size] = value;
    }
    public Object _getProperty(String name) {
        for (int i = 0; i < _propertyNames.length; ++i) {
            if (_propertyNames[i].equals(name)) {
                return _propertyValues[i];
            }
        }
        if (ENDPOINT_ADDRESS_PROPERTY.equals(name) || 
            USERNAME_PROPERTY.equals(name) || 
            PASSWORD_PROPERTY.equals(name)) {
            return null;
        }
        if (SESSION_MAINTAIN_PROPERTY.equals(name)) {
            return new java.lang.Boolean(false);
        }
        throw new JAXRPCException("Stub does not recognize property: "+name);
    }
    protected void _prepOperation(Operation op) {
        for (int i = 0; i < _propertyNames.length; ++i) {
            op.setProperty(_propertyNames[i], _propertyValues[i].toString());
        }
    }
    /*******************************************
    // Web service client-side implementation
    /*******************************************
    public java.lang.String[] getSubjects(java.lang.String senderEmailAddress) 
        throws java.rmi.RemoteException {
        Operation op = Operation.newInstance(_qname_getSubjects, 
                                             _type_senderEmailAddress,
                                             _type_subjectsList);
        _prepOperation(op);
        op.setProperty(Operation.SOAPACTION_URI_PROPERTY,
                       "http://www.everdydaywebservices.com/
                        extendedemailservice/getSubjects");
        Object resultObj;
        try {
            resultObj = op.invoke(senderEmailAddress);
        } catch (JAXRPCException e) {
            Throwable cause = e.getLinkedCause();
            if (cause instanceof java.rmi.RemoteException) {
                throw (java.rmi.RemoteException) cause;
            }
            throw e;
        }
        java.lang.String[] result;
        Object subjectLineObj = ((Object[])resultObj)[0];
        result = (java.lang.String[]) subjectLineObj;
        return result;
    }
    /*************************
    // Data members
    /*************************
    protected static final QName _qname_getSubjects = 
        new QName("http://www.everdaywebservices.com/emailservice/", "getSubjects");
    protected static final QName _qname_senderEmailAddress = 
        new QName("http://www.everdaywebservices.com/emailservice/", 
                  "senderEmailAddress");
    protected static final QName _qname_subjectLine = 
        new QName("http://www.everdaywebservices.com/emailservice/", "subjectLine");
    protected static final QName _qname_subjectsList = 
        new QName("http://www.everdaywebservices.com/emailservice/", "subjectsList");
    protected static final Element _type_senderEmailAddress;
    protected static final Element _type_subjectsList;
    static {
        _type_senderEmailAddress = new Element(_qname_senderEmailAddress, Type.STRING);
        Element _type_subjectLine;
        _type_subjectLine = new Element(_qname_subjectLine, Type.STRING, 0, -1, false);
        ComplexType _complexType_subjectsList;
        _complexType_subjectsList = new ComplexType();
        _complexType_subjectsList.elements = new Element[1];
        _complexType_subjectsList.elements[0] = _type_subjectLine;
        _type_subjectsList = new Element(_qname_subjectsList, _complexType_subjectsList);
    }
}

请注意,存根生成器工具直接在端口类型接口的名称后追加“_Stub”来形成客户端实现类的名称。

SimpleMobileEmailService_PortType_Stub 类包含三个重要的代码段。您需要理解每个代码段的作用。清单 12 包含用于表示这三个代码段的注释,如下所示:

  1. SimpleMobileEmailService_PortType_Stub 包含各种受保护的静态字段。我们已在清单 12 中将这些字段标记为“数据成员”。我们将在存根类的数据成员部分讨论这些字段。
  2. SimpleMobileEmailService_PortType_Stub 还实现了另一个接口 javax.xml.rpc.Stub,其中包括两个方法 _setProperty() 和 _getProperty()。我们在清单 12 中将 _setProperty() 和 _getProperty() 方法实现标记为“处理 RPC 属性”(Handling RPC properties)。我将稍后在指定配置属性部分对 javax.xml.rpc.Stub 接口及其方法进行说明。
  3. SimpleMobileEmailService_PortType_Stub 实现您在前面的清单 11 中看到的 SimpleMobileEmailService_PortType 接口。SimpleMobileEmailService_PortType 接口中仅包含一个方法 getSubjects()。在清单 12 中,我们将 getSubjects() 实现标记为“Web 服务客户端实现”(Web service client-side implementation),将在实现 getSubjects() 部分对此进行说明。

现在让我们对 SimpleMobileEmailService_PortType_Stub 类的三个代码段进行分析。




回页首


存根类的数据成员

为了便于理解,我们将清单 12 中的数据成员代码段复制到清单 13 中。
清单 13:存根类的数据成员

                    
protected static final QName _qname_getSubjects = 
    new QName("http://www.everdaywebservices.com/emailservice/", "getSubjects");
protected static final QName _qname_senderEmailAddress = 
    new QName("http://www.everdaywebservices.com/emailservice/", "senderEmailAddress");
protected static final QName _qname_subjectLine = 
    new QName("http://www.everdaywebservices.com/emailservice/", "subjectLine");
protected static final QName _qname_subjectsList = 
    new QName("http://www.everdaywebservices.com/emailservice/", "subjectsList");
protected static final Element _type_senderEmailAddress;
protected static final Element _type_subjectsList;
static {
    _type_senderEmailAddress = new Element(_qname_senderEmailAddress, Type.STRING);
    Element _type_subjectLine;
    _type_subjectLine = new Element(_qname_subjectLine, Type.STRING, 0, -1, false);
    ComplexType _complexType_subjectsList;
    _complexType_subjectsList = new ComplexType();
    _complexType_subjectsList.elements = new Element[1];
    _complexType_subjectsList.elements[0] = _type_subjectLine;
    _type_subjectsList = new Element(_qname_subjectsList, _complexType_subjectsList);
}

请注意,清单 13 中 SimpleMobileEmailService_PortType_Stub 类的前四个数据成员(_qname_getSubjects_qname_ senderEmailAddress _qname_subjectLine 和 _qname_subjectsList)均是 QName类的实例。

QName 类属于 WSA 的 javax.xml.namespace 包,用于表示 XML 限定名称。

清单 13 中的第一个 QName 对象名为 _qname_getSubjects,表示 getSubjects 操作的限定名称。请注意清单 13 中 SimpleMobileEmailService_PortType_Stub 类如何使用带两个参数的 QName 构造函数实例化 _qname_getSubjects 对象。

QName 构造函数的第一个参数指定 QName 对象的命名空间 URI。可以将传递给清单 13 中的 QName 构造函数的命名空间 URI 与清单 8 中的 WSDL 文件的目标命名空间匹配。两个命名空间是相同的。存根生成器会从 WSDL 文件将命名空间 URI 复制到存根实现中。

第二个参数指定 QName 对象表示的元素的本地名称 (getSubjects)。

您可能会感到奇怪,为什么 QName 构造函数仅接受两个参数(命名空间 URI 和本地名称),而 XML 中的限定名称却包含三个部分(命名空间 URI、本地名称和命名空间前缀)?

实际上,QName 有三个构造函数。最简单的是具有一个参数的构造函数,仅接受 String 格式的本地名称。第二个是具有两个参数的构造函数,即刚刚用于创建 _qname_getSubjects 对象的构造函数。第三个是具有三个参数的构造函数,接受三个 String 类型的数据,用于表示 XML 限定名称的所有三个部分。

请注意,存根生成器将从 WSDL 文件读取限定名称的三个部分。在简单电子邮件服务的 WSDL 文件(清单 8)中定义数据类型部分时,我们并未指定限定名称的前缀值,因此,存根生成器使用具有两个参数的 QName 构造函数来实例化 _qname_getSubjects 对象。

现在看一下剩下的三个 QName 对象。_qname_senderEmailAddress 对象表示发件人的邮件地址的限定名称。

与此类似,其他两个 QName 对象的名称也非常明了易懂。_qname_subjectLine 对象表示清单 8 中的 subjectLine 元素的限定名称,该元素对主题行进行包装。_qname_subjectsList 对象表示清单 8 中的 subjectsList 元素的限定名称,但该元素对一系列 subjectLine 元素进行包装。




回页首


实例化简单电子邮件服务的元素

现在将了解 SimpleMobileEmailService_PortType_Stub 类如何使用四个 QName 对象创建 XML 元素,以便将对远程电子邮件服务的 Web 服务调用请求按清单 9 所示的 SOPA 请求方式进行传递。

请注意,在清单 13 中对实例化四个 QName 对象后,SimpleMobileEmailService_PortType_Stub 类将实例化一系列 Element 和 ComplexType 对象。Element 和 ComplexType 类在 WSA 框架中非常重要,需要对其进行一些说明。

Element 类表示 xsd:element 在 Web 服务 WSDL 文件中定义的 XML 元素。例如,SimpleMobileEmailService_PortType_Stub 类使用 Element 对象表示在清单 8 中的 WSDL 的数据类型部分定义的 senderEmailAddress 元素。

Element 构造函数接受五个参数,用于指定 xsd:element 声明所需的数据。这个五个参数是:

 

  • QName 对象,表示元素的限定名称。
  • Type 对象,表示要在 Element 对象中存储的内容的类型。WSA 将不同的值定义为 Type 类的静态字段,其中每个值都表示一种数据类型。例如,如果要使用元素来包含字符串值,将使用 Type 类的 Type.STRING 静态字段。 
    Type 类包含八个不同的静态字段,分别与 XML 1.0 规范定义的基元数据类型对应。这八个基元类型为 Boolean、Byte、Double、Float、Int、Long、Short 和 String。
    要注意的一点是,Type 类的每个静态字段(如 Type.STRING 字段)本身就是 Type 对象。因此,如果您使用某个类对 Type 类进行了扩展,就可以向 Element 构造函数传递该类的实例,而不用传递 Type 静态字段。当您的元素表示复杂类型内容时,这就非常有用。您将在使用 ComplexType 类部分看到采用此方式的一个示例。
  • 第三个参数和第四个参数均为 Integer 类型,分别指定该元素的最低出现次数和最高出现次数。
  • 最后一个参数是 Boolean 类型,指定元素是否可以为空。如果 nillable 参数设置为 True,这意味着即使该元素不包含内容,也仍然有效。如果元素不包含任何内容,且不能为空,则将会在客户端处理元素时引发异常。

 

Element 类表示 WSDL 文件中的 xsd:element 声明。xsd:element 声明定义可以包含属性的 XML 元素。WSA 实现的当前版本不包含任何添加或处理 XML 属性的方法。正是由于这个原因,所以无法使用 WSA 创建属性。您可以再回头看看 WSA 的一个重要限制部分中的相关内容,其中说明了 WSA 无法创建 XML 属性。

ComplexType 对象表示WSDL 文件中的 xsd:complexType 声明。清单 8 所示的 WSDL 文件仅包含一个名为 subjectsList 的复杂类型定义,用于表示来自特定发件人的电子邮件的主题行列表。现在我们将说明 SimpleMobileEmailService_PortType_Stub 类如何使用 ComplexType 类表示 WSDL 文件中的复杂类型定义。




回页首


使用 ComplexType 类

由于简单电子邮件服务的 WSDL 文件中只有一个复杂类型定义,因此在清单 13 所示的存根类数据成员中仅有一个 ComplexType 对象(名为 _complexType_subjectsList)。

ComplexType 对象在数组中包含了一系列 Element 对象。该数组驻留在 ComplexType 对象中,作为该对象公开提供的字段 elements

ComplexType 构造函数不接受任何参数。实例化 ComplextType 对象后,将实例化 ComplexType 对象的 elements 字段,并逐个设置希望包装在复杂类型定义中的元素。可以在清单 13 中看到 SimpleMobileEmailService_PortType_Stub 类是如何完成此任务的。

清单 13 中的最后一行代码非常有意思:_type_subjectsList = new Element(_qname_subjectsList, _complexType_subjectsList);

这里,您将 subjectsList 复杂类型包装到 Element 对象内。之所以需要这样做,是因为稍后在实现 getSubjects() 部分将需要 Element 对象形式的主题行数组(目前是不包含内容的 ComplexType 对象)。

为了从 _complexType_subjectsList 对象得到 Element 对象,将需要使用具有两个参数的 Element 构造函数。这个具有两个参数的 Element 构造函数接受前面实例化实例化简单电子邮件服务的元素部分所述的五个参数中的前两个。

第一个参数是 _qname_subjectsList 对象,表示 subjectsList 复杂类型。

第二个参数是 Type 对象,指定 Element 对象将包含的元素内容的类型。本例中的元素内容应为复杂类型。因此,您会将 _complexType_subjectsList 对象作为第二个参数传递给 Element 构造函数。

请注意,由于 ComplexType 类继承 Type 类,因此可以传递 ComplexType 对象来替代 Type 对象。

您现在已经了解了 SimpleMobileEmailService_PortType_Stub 类的所有数据成员。这些数据成员指定在调用简单电子邮件服务的 getSubjects 操作时要使用的元素和复杂类型。下一部分将说明 SimpleMobileEmailService_PortType_Stub 类如何使用这些数据成员来充当 Web 服务客户机。




回页首


构建基于 J2ME 的安全 SOAP 客户机: 第 1 部分:探索 J2ME 的 Web 服务 API (WSA)

将安全组件集成到 WSA 中

第 5 页,共 11 页


对本教程的评价

帮助我们改进这些内容


配置和使用 WSA

指定配置属性

我们在存根生成器实现端口类型接口的方式部分的第 2 点中提到,SimpleMobileEmailService_PortType_Stub 类实现了一个名为 javax.xml.rpc.Stub 的接口。

现在我们将说明 javax.xml.rpc.Stub 接口的用途(该接口包含两个方法,分别名为 _setProperty() 和 _getProperty())。

javax.xml.rpc.Stub 接口属于 WSA 的一部分,所有存根实现类都应该实现此接口。

javax.xml.rpc.Stub 接口公开了用于维护配置属性集的机制。这些配置属性指定存根类为了调用远程服务所需要知道的信息。

例如,javax.xml.rpc.Stub 接口定义了一个名为 ENDPOINT_ADDRESS_PROPERTY 的属性,用于指定远程服务侦听的端点的网络地址。

与此类似,javax.xml.rpc.Stub 接口还具有一个名为 SESSION_MAINTAIN_PROPERTY 的属性,用于指定客户机是否请求服务器保持此请求的会话。

另外两个属性名为 USERNAME_PROPERTY 和 PASSWORD_PROPERTY,允许您指定身份验证数据。

接下来,我们将说明 SimpleMobileEmailService_PortType_Stub 类如何实现 javax.xml.rpc.Stub 接口的 _setProperty() 和 _getProperty() 方法,以维护配置属性。

实现 _setProperty()

_setProperty() 方法用于设置配置属性的值。J2ME MIDlet 将调用 _setProperty() 方法来将配置属性设置到存根类中。

属性采取的是名称-值对的形式。名称标识属性,而值则是属性中包含的数据。因此,_setProperty() 方法接受两个参数 name 和 value。

我们将清单 12 中的 _setProperty() 方法实现复制到清单 14 中,为了便于理解,我们还为代码添加了步骤编号和注释。
清单 14:_setProperty() 实现

                    
    public void _setProperty(String name, Object value) {
        int size = _propertyNames.length;
        for (int i = 0; i < size; ++i) {
            //Step1: Check if the property already exists.
            //       If it does, replace its value with the new value.
            if (_propertyNames[i].equals(name)) {
                _propertyValues[i] = value;
                return;
            }
        }
        //Step2: If the property doesn't exist, expand the arrays.
        String[] newPropNames = new String[size + 1];
        System.arraycopy(_propertyNames, 0, newPropNames, 0, size);
        _propertyNames = newPropNames;
        Object[] newPropValues = new Object[size + 1];
        System.arraycopy(_propertyValues, 0, newPropValues, 0, size);
        _propertyValues = newPropValues;
        //Step3: Write the new property at the last position in the arrays.
        _propertyNames[size] = name;
        _propertyValues[size] = value;
    }

根据清单 14 中标记的步骤和注释,可以看到 _setProperty() 方法将配置属性的名称-值对存储在名为 _propertyNames 和 _propertyValues 的数组中。

_setProperty() 方法首先检查希望设置的属性是否已经存在于数组中。如果属性存在,_setProperty() 方法将直接使用新值覆盖旧值。

如果属性不存在,_setProperty() 方法将对数组进行扩展,将新属性(名称-值对)复制到扩展后的数组的最后位置。

实现 _getProperty()

现在让我们看看清单 15 中的 _getProperty() 方法,该方法将返回配置属性的值。
清单 15:_getProperty() 实现

                    
    public Object _getProperty(String name) {
        for (int i = 0; i < _propertyNames.length; ++i) {
            //Step1: Check if the property exists.
            //       If exists, return its value.
            if (_propertyNames[i].equals(name)) {
                return _propertyValues[i];
            }
        }
        //Step2: Return default values.
        if (ENDPOINT_ADDRESS_PROPERTY.equals(name) || 
            USERNAME_PROPERTY.equals(name) || 
            PASSWORD_PROPERTY.equals(name)) {
            return null;
        }
        if (SESSION_MAINTAIN_PROPERTY.equals(name)) {
            return new java.lang.Boolean(false);
        }
        throw new JAXRPCException("Stub does not recognize property: "+name);
    }

可以发现 getProperty() 方法相当智能。如果属性存在,它将返回值。如果属性不存在,将根据应用程序希望读取的配置属性返回一个缺省值。

例如,如果要查找 ENDPOINT_ADDRESS_PROPERTY,缺省值则是一个 null 对象,即表示服务端点的网络地址未知。

另一方面,如果查找 SESSION_MAINTAIN_PROPERTY,则缺省值为 java.lang.Boolean 对象,其对应值为 false,表示客户机不希望端点为此请求保持会话。

构造函数

现在让我们看看清单 16 中所示的 SimpleMobileEmailService_PortType_Stub 构造函数。
清单 16:SimpleMobileEmailService_PortType_Stub 构造函数

                    
public SimpleMobileEmailService_PortType_Stub() {
    _propertyNames = new String[] {ENDPOINT_ADDRESS_PROPERTY};
    _propertyValues = new Object[] {"http://www.everdaywebservices.com/emailservice"};
}

SimpleMobileEmailService_PortType_Stub 构造函数设置名为 ENDPOINT_ADDRESS_PROPERTY 的属性的值,该属性指定承载服务的网络地址。

请注意,存根生成器工具从 WSDL 文件中 soap:address 元素的 location 属性读取地址信息,将此信息设置到 ENDPOINT_ADDRESS_PROPERTY 中。

在存根构造函数中设置 ENDPOINT_ADDRESS_PROPERTY 属性仅为了方便 J2ME 程序员。远程端点的网络地址通常在 WSDL 文件中指定,因此在存根构造函数中设置此属性的值可以减少一行 MIDlet 代码。

不过,J2ME 程序员仍然可以通过在 MIDlet 中调用 _setProperty() 方法来覆盖此属性。

实现 getSubjects()

现在让我们说明一下存根生成器实现端口类型接口的方式部分的第 3 点提到的 getSubjects() 方法实现。

请参见清单 17 中的 getSubjects() 方法实现。
清单 17:实现 getSubjects()

                    
public java.lang.String[] getSubjects(java.lang.String senderEmailAddress) 
      throws java.rmi.RemoteException {
    /****Step1****/
    Operation op = Operation.newInstance(_qname_getSubjects, 
                        _type_senderEmailAddress, _type_subjectsList);
    /****Step2****/
    _prepOperation(op);
    op.setProperty(Operation.SOAPACTION_URI_PROPERTY,
                       "http://www.everydaywebservices.com/emailservice");
    Object resultObj;
    try {
        /**** Step 3****/
        resultObj = op.invoke(senderEmailAddress);
    } catch (JAXRPCException e) {
        Throwable cause = e.getLinkedCause();
        if (cause instanceof java.rmi.RemoteException) {
            throw (java.rmi.RemoteException) cause;
        }
        throw e;
    }
    /**** Step 4****/
    java.lang.String[] result;
    Object subjectLineObj = ((Object[])resultObj)[0];
    result = (java.lang.String[]) subjectLineObj;
    return result;
}

getSubjects() 方法接受发件人的电子邮件地址作为其输入,并返回来自该特定发件人的所有电子邮件的主题行列表。

为了获取主题行,getSubjects() 方法将使用名为 Operation 类,该类是 WSA 的 javax.microedition.xml.rpc 包的一部分。Operation 类包含用于调用远程操作的方法。

让我们看看清单 17 中的步骤 1,其中 getSubjects() 方法将通过调用名为 newInstance() 的静态方法实例化新 Operation 对象。newInstance() 方法接受三个参数:

  1. Operation 构造函数的第一个参数是名为 _qname_getSubjects 的 QName 对象。_qname_getSubjects 对象指定希望使用 Operation 对象调用的 WSDL 操作。
    存根类的数据成员部分,我们曾说过,SimpleMobileEmailService_PortType_Stub 类为清单 8 所示的 WSDL 文件的 getSubjects 元素实例化了一个 QName 对象。在清单 17 的步骤 1 中,会将相同的 QName 对象传递给 Operation 构造函数。这是因为您将要使用 Operation 对象来调用 getSubjects 操作。
  2. Operation 构造函数的第一个参数已指定了希望调用的操作。还需要指定操作调用所需的输入参数。第二个参数是一个名为 _type_senderEmailAddress 的 Element 对象,用于表示输入参数。我们在前面的清单 13 中创建了这个 _type_senderEmailAddress 对象。
    还要注意另一个重要点。由于 _type_senderEmailAddress 对象表示 WSDL 文件的 senderEmailAddress 元素(清单 8),因此 _type_senderEmailAddress 对象仅定义输入参数的 XML 结构。它没有定义进入输入参数中的实际内容。在稍后的使用 Operation 对象调用远程服务部分中,您将在调用操作时提供此内容。
  3. 第三个参数也是一个 Element 对象,即 named _type_subjectsList。第三个参数 subjectsList 表示操作调用的输出或返回元素。
    存根类的数据成员部分,我们提到,_type_subjectsList 对象对清单 8 中的 WSDL 内定义的subjectsList 复杂类型结构进行包装。 
    为什么 Operation 对象需要知道 subjectsList 复杂类型结构呢?因为 subjectsList 复杂类型结构表示的是 getSubjects() 方法的输出或返回格式。稍后,当使用 Operation 对象调用 WSDL 操作时,Operation 对象将需要处理来自远程 Web 服务的响应,以提取输出返回数据。此时,Operation 对象将使用 subjectsList 复杂类型结构来了解响应消息的格式,并使用远程 Web 服务端点返回的内容对其进行填充。

准备 Operation 对象

实例化 Operation 对象后,您将对其进行准备工作,以便用于远程服务调用。请参见清单 17 中的步骤 2,getSubjects() 方法将调用名为 _prepOperation() 的方法,并将 Operation 对象随方法调用一起传递。

_prepOperation() 是 SimpleMobileEmailService_PortType_Stub 类中受保护的 Helper 方法,用于对存储在 Operation 对象内的 SimpleMobileEmailService_PortType_Stub 类中的所有配置属性进行设置。

为了设置 Operation 对象内的配置属性,_prepOperation() 将多次调用 Operation 类的 setProperty() 方法,每个配置属性一次。

setProperty() 方法接受表示配置属性的名称-值对,并对 Operation 对象中的属性进行设置。

接下来,getSubjects() 方法设置 Operation 对象中另一个属性 SOAPACTION_URI_PROPERTYSOAPACTION_URI_PROPERTY 并未在 javax.xml.rpc.Stub 接口中定义,而其他属性(如 ENDPOINT_ADDRESS_PROPERTY)均在此接口中定义。

SOAPACTION_URI_PROPERTY 在 Operation 类中定义。

您可能会奇怪,为什么此属性会与在 javax.xml.rpc.Stub 接口中定义的其他四个属性分开呢?

SOAPACTION_URI_PROPERTY 是一个 SOAP 特定的属性,用于指定目标操作的 URL。

在 javax.xml.rpc.Stub 接口中定义的其他四个属性并不是 SOAP 特定的属性。例如,ENDPOINT_ADDRESS_PROPERTY 直接定义端点实现的网络地址。

Operation 类应进行 SOAP 消息创建和处理工作,因此包含 SOAP 特定的属性。

请注意,存根生成器会将 ENDPOINT_ADDRESS_PROPERTY 的值从清单 8 所示的 WSDL 文件中的 soap:operation 元素的 soapAction 属性复制到 SimpleMobileEmailService_PortType_Stub 类中。这意味着,无论在 WSDL 文件中为 soapAction 属性设置的值是什么,都会将其设置到对应的存根类中。

使用 Operation 对象调用远程服务

您的 Operation 对象现在已完成了所有设置,可以用于调用远程服务了。因此在步骤 3 中,可以调用Operation 对象的 invoke() 方法,并同时将发件人的电子邮件地址作为输入数据传递。invoke() 方法将在内部完成以下工作:

  1. 创建 getSubjects() 方法调用的 SOAP 请求
  2. 使用 J2ME 中的网络支持将请求发送到远程端点
  3. 获取 SOAP 响应消息
  4. 分析响应,以提取主题行列表
  5. 将主题行列表加载到 String 对象数组中,其中的每个 String 对象表示一个主题行
  6. 将这个字符串数组作为 Object 实例返回给调用应用程序

可以看到,远程 Web 服务调用过程中所涉及的大多数难点任务都是由 Operation 对象完成的。

invoke()方法返回 Object 实例时,getSubjects() 方法会将这个实例强制转换为一个字符串数组,并将该数组返回给调用应用程序,如清单 17中的步骤 4 所示。

现在,我们已经对存根生成器实现端口类型接口的方式部分中提及的所有三个代码段进行了分析。我们对 SimpleMobileEmailService_PortType_Stub 类的讨论也就到此结束了。




回页首


使用简单电子邮件服务

接下来,您将了解 J2ME 应用程序如何使用简单电子邮件服务的存根类调用 getSubjects 操作。

为了调用 getSubjects 操作,需要执行三个步骤,如清单 18 所示:

  1. 实例化简单电子邮件服务存根类
  2. 设置 Web 服务的配置属性
  3. 调用存根类的 getSubjects() 方法,同时将发件人电子邮件地址作为参数随方法调用一起传入。getSubjects() 方法将返回一个字符串数组,其中包含从特定电子邮件地址接收到的电子邮件主题行

清单 18:调用 getSubjects 操作的步骤
                    
/**** Step1 ****/
SimpleMobileServiePortType_Stub service = new SimpleMobileServiePortType _Stub();
/**** Step2 ****/
service._setProperty(SignatureServicePortType_Stub.SESSION_MAINTAIN_PROPERTY, 
                     new Boolean(true));
/**** Step3 ****/
String[] subjectsList = service.getSubjects("bob@mycompany.com");

本教程的源代码下载部分包含一个名为 SimpleEmailServiceMIDlet 的 MIDlet,它演示了本部分和前面部分中提出的所有概念。

为了测试 SimpleEmailServiceMIDlet,还需要将简单电子邮件服务部署到 SOAP 服务器上。源代码下载中还包含了名为 RequestReceiver 的简单类,可作为简单电子邮件服务的模拟器。可以使用下面的命令行语句来运行这个模拟器:%JAVA_HOME%/java -classpath . RequestReceiver

该模拟器将侦听并保存来自 J2ME 客户机的传入请求,以便了解 SimpleEmailServiceMIDlet 创建了哪些内容。




回页首


构建基于 J2ME 的安全 SOAP 客户机: 第 1 部分:探索 J2ME 的 Web 服务 API (WSA)

将安全组件集成到 WSA 中

第 6 页,共 11 页


对本教程的评价

帮助我们改进这些内容


扩展电子邮件服务并保证其安全

扩展电子邮件服务的 WSDL

在分析了简单电子邮件服务对应的存根类如何工作之后,接下来我们对简单电子邮件服务进行扩展,以了解 WSDL 文件中的多个操作和复杂类型如何反映在存根类之中。

我们将简单电子邮件服务的扩展版本称为“扩展移动电子邮件服务”(extended mobile email service)或“扩展电子邮件服务”(extended email service)。

请参见清单 19,其中显示了名为 ExtendedMobileEmailService 的扩展电子邮件服务的 WSDL 文件。
清单 19:扩展电子邮件服务的 WSDL

                    
<?xml version="1.0" encoding="UTF-8"?>
<definitions 
    xmlns="http://schemas.xmlsoap.org/wsdl/" 
    xmlns:tns="http://www.everydaywebservices.com/extendedemailservice" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
    name="emailService" 
    targetNamespace="http://www.everydaywebservices.com/extendedemailservice">
   <types>
    <schema xmlns="http://www.w3.org/2001/XMLSchema"
               targetNamespace="http://www.everydaywebservices.com/extendedemailservice">
      <xsd:element name="subjectsList">
         <xsd:complexType>
            <xsd:sequence>
                <xsd:element name="subjectLine" type="xsd:string" minOccurs="0"
                 maxOccurs="unbounded"/>
            </xsd:sequence>
         </xsd:complexType>
      </xsd:element>
      <xsd:element name="senderEmailAddress" type="xsd:string"/>
      <xsd:element name="messageIdentifier">
         <xsd:complexType>
            <xsd:sequence>
              <xsd:element name="subject" type="xsd:string" nillable="true"/>
              <xsd:element name="from" type="xsd:string" nillable="true"/>
            </xsd:sequence>
         </xsd:complexType>
      </xsd:element>
      <xsd:element name="message">
         <xsd:complexType>
            <xsd:sequence>
              <xsd:element name="subject" type="xsd:string" nillable="true"/>
              <xsd:element name="from" type="xsd:string" nillable="true"/>
              <xsd:element name="messageText" type="xsd:string" nillable="true"/>
            </xsd:sequence>
         </xsd:complexType>
      </xsd:element>
    </schema>
   </types>
  <message name="senderEmailAddress">
    <part name="senderEmailAddress" element="tns:senderEmailAddress"/>
  </message>
  <message name="subjectsList">
    <part name="subjectsList" element="tns:subjectsList"/>
  </message>
  <message name="messageIdentifier">
    <part name="messageIdentifier" element="tns:messageIdentifier"/>
  </message>
  <message name="message">
    <part name="message" element="tns:message"/>
  </message>
  <portType name="extendedMobileEmailService">
    <operation name="getSubjects">
      <documentation>
        Get subject lines of emails from a particular sender.
      </documentation>
      <input message="tns:senderEmailAddress"/>
      <output message="tns:subjectsList"/>
    </operation>
    <operation name="getMessage">
      <documentation>Get an email message from your inbox.</documentation>
      <input message="tns:messageIdentifier"/>
      <output message="tns:message"/>
    </operation>
  </portType>
  <binding name="extendedMobileEmailServiceBinding" type="tns:extendedMobileEmailService">
    <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
    <operation name="getSubjects">
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/></output>
      <soap:operation 
      soapAction="http://www.everdydaywebservices.com/extendedemailservice/getSubjects"/>
    </operation>
    <operation name="getMessage">
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
      <soap:operation 
       soapAction="http://www.everdydaywebservices.com/extendedemailservice/getMessage"/>
    </operation>
  </binding>
  <service name="ExtendedMobileEmailServiceBinding">
    <port name="ExtendedMobileEmailServicePort" 
             binding="tns:extendedMobileEmailServiceBinding">
      <soap:address 
         location=" http://www.everdydaywebservices.com/extendedemailservice"/>
    </port>
   </service>
</definitions>

如果将清单 19 与前面清单 8 中的简单电子邮件服务的 WSDL 文件进行比较,将会发现以下差别:

  1. 简单电子邮件服务仅包含一个操作 getSubjects。扩展电子邮件服务除了 getSubjects 操作外,还包含另一个操作 getMessagegetMessage 操作接受主题行和发件人电子邮件地址作为参数,将返回来自特定发件人的具有匹配主题行的传入电子邮件内容(如果有)。
  2. 输入参数是名为 messageIdentifier 的复杂类型,其中包含两个 String 类型的元素,即 subject(要查找的邮件主题行)和 from(发件人的电子邮件地址)。
  3. getMessage 操作返回的电子邮件内容是一个名为 message 的复杂类型,它包含三个文本字符串,即邮件主题行 subject、发件人电子邮件地址 from 和文本消息 messageText

扩展电子邮件服务的存根类

存根生成器将为清单 19 所示的WSDL 生成以下文件:

  1. ExtendedMobileEmailService_PortType
  2. ExtendedMobileEmailService_PortType_Stub
  3. SubjectsList
  4. MessageIdentifier
  5. Message

可以在本教程的源代码下载部分找到扩展电子邮件服务的存根文件。

现在,我们将说明这些存根文件与前面简单电子邮件服务的存根类部分中看到的简单电子邮件服务存根文件之间的差别。

可以看到,ExtendedMobileEmailService_PortType_Stub.java 文件包含名为 ExtendedMobileEmailService_PortType 的接口,其中定义了两个方法,分别是 getSubjects() 和 getMessage()。这两个方法表示扩展电子邮件服务(清单 19 中的 WSDL 文件)的两个操作。

很容易猜到,扩展电子邮件服务的 ExtendedMobileEmailService_PortType 接口对应于前面在在存根文件中反映端口类型部分看到的简单电子邮件服务的 SimpleMobileEmailService_PortType 接口。

我们将在稍后说明存根类如何实现 ExtendedMobileEmailService_PortType 接口,但首先将介绍一下存根类如何处理 WSDL 文件的数据。




回页首


扩展电子邮件服务的数据类型

扩展电子邮件服务存根文件包含 SubjectsList MessageIdentifier 和 Message 类,它们分别表示清单 19 中扩展电子邮件服务的 WSDL 文件的 subjectsListmessageIdentifier 和 message 复杂类型。

为了说明如何在存根类中反映复杂类型,我们将 Message 类复制到了清单 20 中。Message 表示扩展电子邮件服务的 WSDL 文件的 message 复杂类型。
清单 20:扩展电子邮件服务的 message 类

                    
public class Message {
    protected java.lang.String subject;
    protected java.lang.String from;
    protected java.lang.String messageText;
    
    public Message() {
    }
    
    public Message(java.lang.String subject, 
                   java.lang.String from, 
                   java.lang.String messageText) {
        this.subject = subject;
        this.from = from;
        this.messageText = messageText;
    }
    
    public java.lang.String getSubject() {
        return subject;
    }
    
    public void setSubject(java.lang.String subject) {
        this.subject = subject;
    }
    
    public java.lang.String getFrom() {
        return from;
    }
    
    public void setFrom(java.lang.String from) {
        this.from = from;
    }
    
    public java.lang.String getMessageText() {
        return messageText;
    }
    
    public void setMessageText(java.lang.String messageText) {
        this.messageText = messageText;
    }
}

请注意,Message 类包含三个 setter 和 getter 方法集,分别与复杂类型的三个字段对应。例如,getSubject() 方法不接受任何参数,将返回表示电子邮件主题行的字符串。类似地,setSubject() 方法用于将主题行设置到 String 对象中。

这些数据类型类用于实现 ExtendedMobileEmailService_PortType 接口。ExtendedMobileEmailService_PortType_Stub 类实现扩展电子邮件服务的 ExtendedMobileEmailService_PortType 接口。接下来将了解 ExtendedMobileEmailService_PortType_Stub 类在实现扩展电子邮件服务的客户端时如何使用Message 数据类型类。

如何实现 getMessage()

ExtendedMobileEmailService_PortType_Stub.java 类包含扩展电子邮件服务的 getSubjects() 和 getMessage() 方法的实现。

getMessage() 实现与实现 getSubjects() 部分中的 getSubjects() 方法实现相似,二者仅有两点不同。

为了说明这两点不同,我们将 getMessage() 实现从 ExtendedMobileEmailService_PortType_Stub.java 文件复制到了清单 21 中。
清单 21:getMessage() 实现

                    
public enhanced.Message getMessage(java.lang.String subject, java.lang.String from) 
        throws java.rmi.RemoteException {
    /**** Putting input parameters into an Object array ****/               
    Object[] inputObject = new Object[2];
    inputObject[0] = subject;
    inputObject[1] = from;
    /**** Step 1 ****/        
    Operation op = Operation.newInstance(_qname_getMessage, 
                _type_messageIdentifier, 
                _type_message);
    /**** Step 2 ****/
    _prepOperation(op);
    op.setProperty(Operation.SOAPACTION_URI_PROPERTY,
         "http://www.everdydaywebservices.com/extendedemailservice/getMessage");
    Object resultObj;
    try {
        /**** Step 3 ****/        
        resultObj = op.invoke(inputObject);
    } catch (JAXRPCException e) {
        Throwable cause = e.getLinkedCause();
        if (cause instanceof java.rmi.RemoteException) {
            throw (java.rmi.RemoteException) cause;
        }
        throw e;
    }
    /**** Step 4 ****/
    enhanced.Message result;
    if (resultObj == null) {
        result = null;
    } else {
        result = new enhanced.Message();
        java.lang.String string;
        Object subjectObj = ((Object[])resultObj)[0];
        string = (java.lang.String)subjectObj;
        result.setSubject(string);
        java.lang.String string2;
        Object fromObj = ((Object[])resultObj)[1];
        string2 = (java.lang.String)fromObj;
        result.setFrom(string2);
        java.lang.String string3;
        Object messageTextObj = ((Object[])resultObj)[2];
        string3 = (java.lang.String)messageTextObj;
        result.setMessageText(string3);
    }
    return result;
}

第一个不同出现在清单 21 中标记为“将输入参数放入 Object 数组中”(Putting input parameters into an Object array) 的代码中,在这段代码中,getMessage() 方法会将输入参数置入名为 inputObject 的 Object 数组中。在清单 17 的 getSubjects() 方法实现中并没有这个代码段。

这是因为 getMessage() 方法接受两个输入参数,而 getSubjects() 方法仅接受一个参数。

getSubjects() 方法直接将输入参数传递给 Operation.invoke() 方法(清单 17 中的步骤 3)。另一方面,getMessage() 方法会将 inputObject 数组(该数组对两个输入参数进行包装)传递给 Operation.invoke() 方法(清单 21 中的步骤 3)。

请注意,无论在清单 17 还是在清单 21 中,输入参数都被传递给 Operation.invoke() 方法,只不过在清单 17 是直接传递,而在清单 21 中则是间接传递。

清单 17 中的 getSubjects() 方法的步骤 4 与清单 21 中的 getMessage() 方法的步骤 4 进行比较,会发现第二点不同。与 getSubjects() 方法的步骤 4 相比,getMessage() 的步骤 4 更为复杂。这是因为 getMessage() 方法必须从 SOAP 响应提取各个不同的消息部分,并将这些消息部分设置到 Message 对象中。这一操作过程全部是在清单 21 的步骤 4 中完成的,因此有些复杂。

getMessage() 实现的其他部分与 getSubjects() 完全相同。

使用扩展电子邮件服务的存根类

清单 22 显示了 J2ME MIDlet 为了使用扩展电子邮件服务的存根类来调用 getMessage 操作而需要遵循的步骤。
清单 22:MIDlet 为了使用扩展电子邮件服务的存根类而需要遵循的步骤

                    
/**** Step1 ****/
ExtendedMobileEmailService_PortType_Stub service =
        new ExtendedMobileEmailService_PortType_Stub();
/**** Step2 ****/
service._setProperty(SignatureServicePortType_Stub.SESSION_MAINTAIN_PROPERTY, 
                     new Boolean(true));
/**** Step3 ****/
Message message = service.getMessage("Recent Project", "bob@mycompany.com");
/**** Step4 ****/
String messageText = message.getMessageText();
System.out.println("Message text:" +messageText); 

可以将清单 22 的这些步骤与前面清单 18 的简单电子邮件服务 MIDlet 的步骤进行比较。两个 MIDlet 很相似,但清单 22 中包含一个额外的步骤(步骤 4)。

请注意,在清单 22 的步骤 3 中,getMessage() 将返回一个 Message 对象。在步骤 4 中,将通过调用 Message 对象的 getter 方法来处理 Message 对象。

本教程的源代码下载部分包含一个名为 ExtendedEmailServiceMIDlet 的 MIDlet。此 MIDlet 包含在本部分中提供的所有代码,提供了源代码和编译版两种形式。

ExtendedEmailServiceMIDlet 还附带了一个 Web 服务模拟器,可以使用此模拟器查看 MIDlet 发出的 SOAP 消息。

保证扩展电子邮件服务的安全

我们已经了解了简单电子邮件服务和扩展电子邮件服务对应的存根类如何工作。另外,还了解了 J2ME MIDlet 如何使用简单电子邮件服务和扩展电子邮件服务的存根类。

接下来,我们将开始进行保证扩展电子邮件服务的安全方面的工作了。我们将扩展电子邮件服务的安全版本称为“安全移动电子邮件服务”(secure mobile email service)或“安全电子邮件服务”(secure email service)。

首先要编写安全电子邮件服务的WSDL 文件,如 清单 23 所示。
清单 23:安全电子邮件服务的 WSDL

                    
<?xml version="1.0" encoding="UTF-8"?>
<definitions 
    xmlns="http://schemas.xmlsoap.org/wsdl/" 
    xmlns:tns="http://www.everydaywebservices.com/secureemailservice" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
    name="secureEmailService" 
    targetNamespace="http://www.everydaywebservices.com/secureemailservice">
   <types>
    <schema xmlns="http://www.w3.org/2001/XMLSchema"
               targetNamespace="http://www.everydaywebservices.com/secureemailservice">
      <xsd:element name="subjectsList">
         <xsd:complexType>
            <xsd:sequence>
                <xsd:element name="subjectLine" type="xsd:string" minOccurs="0"
                 maxOccurs="unbounded"/>
            </xsd:sequence>
         </xsd:complexType>
      </xsd:element>
      <xsd:element name="senderEmailAddress">
         <xsd:complexType>
            <xsd:sequence>
              <xsd:element name="senderEmailAddress" type="xsd:string"/>
              <xsd:element name="signature" type="tns:Signature" nillable="true"/>
            </xsd:sequence>
         </xsd:complexType>
      </xsd:element>
      <xsd:element name="messageIdentifier">
         <xsd:complexType>
            <xsd:sequence>
              <xsd:element name="subject" type="xsd:string" nillable="true"/>
              <xsd:element name="from" type="xsd:string" nillable="true"/>
              <xsd:element name="signature" type="tns:Signature" nillable="true"/>
            </xsd:sequence>
         </xsd:complexType>
      </xsd:element>
      <xsd:element name="message">
         <xsd:complexType>
            <xsd:sequence>
              <xsd:element name="subject" type="xsd:string" nillable="true"/>
              <xsd:element name="from" type="xsd:string" nillable="true"/>
              <xsd:element name="messageText" type="xsd:string" nillable="true"/>
            </xsd:sequence>
         </xsd:complexType>
      </xsd:element>
      <xsd:complexType name="Signature">
        <xsd:sequence> 
           <xsd:element name="SignedInfo"> 
             <xsd:complexType>
             <xsd:sequence> 
               <xsd:element name="CanonicalizationMethod" type="xsd:string"/>
               <xsd:element name="SignatureMethod" type="xsd:string"/>
               <xsd:element name="Reference">
                 <xsd:complexType>
                   <xsd:sequence> 
                     <xsd:element name="DigestMethod" type="xsd:string"/>
                     <xsd:element name="DigestValue" type="xsd:string"/> 
                     <xsd:element name="URI" type="xsd:string"/> 
                   </xsd:sequence>
                 </xsd:complexType>
               </xsd:element> 
             </xsd:sequence>  
             </xsd:complexType>
           </xsd:element>
           <xsd:element name="SignatureValue" type="xsd:string"/> 
           <xsd:element name="KeyInfo">
              <xsd:complexType>
                 <xsd:sequence>
                  <xsd:element name="KeyName" type="xsd:string"/> 
                 </xsd:sequence>
              </xsd:complexType> 
           </xsd:element> 
        </xsd:sequence>  
      </xsd:complexType>
    </schema>
   </types>
  <message name="senderEmailAddress">
    <part name="senderEmailAddress" element="tns:senderEmailAddress"/>
  </message>
  <message name="subjectsList">
    <part name="subjectsList" element="tns:subjectsList"/>
  </message>
  <message name="messageIdentifier">
    <part name="messageIdentifier" element="tns:messageIdentifier"/>
  </message>
  <message name="message">
    <part name="message" element="tns:message"/>
  </message>
  <portType name="secureMobileEmailService">
    <operation name="getSubjects">
      <documentation>
        Get subject lines of emails from a particular sender.
      </documentation>
      <input message="tns:senderEmailAddress"/>
      <output message="tns:subjectsList"/>
    </operation>
    <operation name="getMessage">
      <documentation> Get email message from your inbox. </documentation>
      <input message="tns:messageIdentifier"/>
      <output message="tns:message"/>
    </operation>
  </portType>
  <binding name="secureMobileEmailServiceBinding" type="tns:secureMobileEmailService">
    <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
    <operation name="getSubjects">
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/></output>
      <soap:operation 
       soapAction="http://www.everdaywebservices.com/secureemailservice/getSubjects"/>
    </operation>
    <operation name="getMessage">
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
      <soap:operation 
        soapAction="http://www.everdaywebservices.com/secureemailservice/getMessage"/>
    </operation>
  </binding>
  <service name="SecureMobileEmailService">
    <port name="secureMobileEmailServicePort"
          binding="tns:secureMobileEmailServiceBinding">
      <soap:address 
          location="http://www.everdaywebservices.com/secureemailservice"/>
    </port>
   </service>
</definitions>

可以看到,清单 23 的 WSDL 文件包含名为 Signature 的复杂类型的多个定义。Signature 复杂类型是安全电子邮件服务中定义的 getSubjects 操作和 getMessage 操作的输入参数。

清单 23 中的 Signature 复杂类型定义了一个完整 XML 签名的结构,用于承载用户(如 Alice)用于尝试访问安全电子邮件服务的身份验证信息。因此,Signature 复杂类型包含了在前面使用 XML 签名部分(讨论清单 7 中的安全 SOAP 消息时)看到的所有子元素。

在本系列教程的下一部分,您将使用清单 23 所示的 WSDL 文件来为安全电子邮件服务生成存根类,并对存根类进行增强,以便创建清单 7 中的安全 SOAP 消息。

构建基于 J2ME 的安全 SOAP 客户机: 第 1 部分:探索 J2ME 的 Web 服务 API (WSA)

将安全组件集成到 WSA 中

第 7 页,共 11 页


对本教程的评价

帮助我们改进这些内容


总结

本教程介绍了保证 Web 服务无线访问安全的相关概念。

文中提供了一些示例应用场景,需要在其中无线使用 Web 服务(采用安全机制或不采用安全机制)。我们讨论了用于集成各种技术组件的体系结构,以便在 WSA 应用程序中启用安全机制。

我们还对存根类和其他 WSA 类进行了详细分析,并且演示了其各自的功能。我们还提供了对 WSA 使用的 WSDL 和 SOAP 消息的说明。

我们已经了解了安全 Web 服务的 WSDL 接口。在本系列教程的第 2 部分,我们将把安全机制构建到这些服务中。

构建基于 J2ME 的安全 SOAP 客户机: 第 2 部分:增强 J2ME 的 Web 服务 API (WSA) 中的存根类

实现安全算法

第 1 页,共 12 页


对本教程的评价

帮助我们改进这些内容


级别: 中级

Bilal Siddiqui (bsiddiqui@xml4java.com), 自由顾问

2007 年 1 月 08 日

本系列教程共三部分,介绍如何构建基于 Java™ 2 Micro Edition (J2ME) 的安全 Web 服务客户机。本教程是其中的第二部分,将讨论安全电子邮件服务的存根类,并说明如何对其进行增强,以提供安全功能。我们还将详细讨论一些重要的安全算法,并演示如何在 J2ME 设备中实现它们。

开始之前

关于本系列教程

本系列教程演示如何将基于 Java 2 Micro Edition (J2ME) 的无线访问中的安全机制集成到 Web 服务中。我们将在 J2ME MIDlet 中使用以下组件和技术:

 

  1. J2ME 的 Web 服务 API (WSA)
  2. 加密
  3. XML 数字签名(XML Digital Signature,XMLDS)
  4. Java Card

 

本系列的第 1 部分,您已经了解了 WSA 存根类的工作方式。本系列的剩下部分将说明如何增强 WSA 存根类和如何将其他技术组件(如加密、XML 签名和 Java Card)集成到 WSA 存根类中。

本系列教程还将说明用于集成不同技术组件的各种测试和调试参数。本系列最后要将所有概念放入到一个“存根增强器工具”中。此工具将通过加入安全功能来增强 WSA 存根类的功能。




回页首


关于本教程

在本系列教程的第 1 部分中,我们通过简单和扩展电子邮件服务深入了解了 WSA 的信息。第 1 部分最后引入了电子邮件服务的安全版本。

第 2 部分将对安全电子邮件服务进行详细讨论。在此部分,您将为安全电子邮件服务生成存根类。您将分析安全电子邮件服务存根类中的每个类,并了解如何对其进行增强,以提供所需的安全功能。

您还将遇到安全电子邮件服务中的一些 Helper 类,这些 Helper 类处理 XML 和各种安全问题,如创建 XML 的规范格式和计算摘要值。

因此,第 2 部分还将说明为什么需要进行规范化和摘要算法,以保护对 Web 服务的无线访问。第 2 部分还将说明实现这些算法的技术,这些技术特别适合处理具有内存约束的无线设备。




回页首


先决条件

有关第一个先决条件,请参见本系列的第 1 部分

您将在本教程中遇到多个技术组件。因此,您务必对组件有基本了解。具体来说,假定了以下背景:

  1. 您应该熟悉 Java 编程,并对 J2ME MIDlet 有基本了解。
  2. WSA 使用 Web 服务描述语言(Web Services Definition Language,WSDL)和简单对象访问协议(Simple Object Access Protocol,SOAP)。因此,您需要知道 WSDL 接口如何映射到 SOAP 方法调用。

 

而且,具有一定的 XML 签名方法的背景也会有所帮助。

IBM developerWorks 提供了很多有关这些主题的优秀文章和教程。参考资料部分列出了一些供参考的资料。




回页首


谁应学习本教程?

本系列教程的主要目的是为了帮助您支持 Web 服务的无线访问——使用或不使用安全机制。

本系列教程的此部分讨论针对安全性进行 WSA 存根类的自定义。因此,只要您觉得 WSA 存根类标准集不适合您的应用程序,就可以参考此教程了解一些自定义相关知识。

另一个要点:此部分说明了如何在内存受约束的无线设备中实现规范化和摘要计算算法。因此,本教程还可能帮助您在无线设备中实现类似的算法。




回页首


教程主题

第 2 部分包含以下八个部分:

  1. 教程介绍
  2. 说明安全电子邮件服务的 WSDL 文件。此部分还介绍了安全电子邮件服务的存根类。
  3. 讨论 J2ME MIDlet 将如何使用安全电子邮件服务。此部分还将说明如何增强安全电子邮件服务的主存根类来提供安全功能。
  4. 讨论如何增强 Signature 存根类。
  5. 说明如何增强安全电子邮件服务中的其他存根类。
  6. 说明如何为 J2ME 应用程序实现 W3C 规范化算法。
  7. 说明如何计算摘要值。
  8. 总结

 




回页首


代码示例和安装要求

我们使用了 Java Development Kit (JDK) V1.5、J2ME Wireless Toolkit V2.2 和 Sun Java Wireless Toolkit V2.3 Beta 来生成和测试第 2 部分的代码。

我们还使用了 IBM alphaWorks 的 XML Security Suite for Java (XSS4J) 和 Apache 的 Xalan 来调试本教程的代码。这二者都是开放源代码工具,可以参考本教程的参考资料部分来下载。

我们将在本教程的第 3 部分构建一个 Java Card 应用程序。因此,第 3 部分还将使用 Sun 网站提供的 Java Card Development Kit 来测试此 Java Card 应用程序。




回页首


构建基于 J2ME 的安全 SOAP 客户机: 第 2 部分:增强 J2ME 的 Web 服务 API (WSA) 中的存根类

实现安全算法

第 2 页,共 12 页


对本教程的评价

帮助我们改进这些内容


部分 2. 分析安全电子邮件服务

安全电子邮件服务

本教程的第 1 部分通过两个名为“简单电子邮件服务”和“扩展电子邮件服务”的示例应用程序说明了 WSA 存根类的工作方式。我们在本系列教程的第 1 部分中详细讨论了这些示例应用程序,并在结束时引入了一个名为“安全电子邮件服务”的服务;此服务是扩展电子邮件服务的安全形式。

安全电子邮件服务的 WSDL 文件包括在第 1 部分中。让我们首先说明一下安全电子邮件服务的 WSDL(请参见第 1 部分的清单 23)和扩展电子邮件服务的 WSDL 文件(请参见第 1 部分的清单 19)有何差别。

安全电子邮件服务和扩展电子邮件服务的比较

安全电子邮件服务和扩展电子邮件服务都定义了两个 Web 服务操作 getSubjects 和 getMessage

这两个电子邮件服务的 WSDL 文件之间仅存在两个差异:

1. 安全电子邮件服务的 WSDL 文件的 data types 部分包含 Signature 元素的定义。 
Signature 已复制到清单 1 中。Signature 元素包含尝试访问安全电子邮件服务的用户的身份验证信息。扩展电子邮件服务并不包含 Signature 元素。

清单 1. 安全电子邮件服务的 Signature 元素的定义

                    
<xsd:complexType name="Signature">
  <xsd:sequence> 
     <xsd:element name="SignedInfo"> 
       <xsd:complexType>
         <xsd:sequence> 
           <xsd:element name="CanonicalizationMethod"
               type="xsd:string"/>
           <xsd:element name="SignatureMethod" 
               type="xsd:string"/>
           <xsd:element name="Reference">
             <xsd:complexType>
               <xsd:sequence> 
                 <xsd:element name="DigestMethod" type="xsd:string"/>
                 <xsd:element name="DigestValue" type="xsd:string"/> 
                 <xsd:element name="URI" type="xsd:string"/>
               </xsd:sequence>
             </xsd:complexType>
           </xsd:element> 
         </xsd:sequence>  
       </xsd:complexType>
     </xsd:element>
     <xsd:element name="SignatureValue" type="xsd:string"/> 
     <xsd:element name="KeyInfo">
       <xsd:complexType>
         <xsd:sequence>
           <xsd:element name="KeyName" type="xsd:string"/> 
         </xsd:sequence>
       </xsd:complexType> 
     </xsd:element> 
  </xsd:sequence>  
</xsd:complexType>

我们稍后将在 Signature 复杂类型的存根类部分讨论 Signature 元素。

2. 安全电子邮件服务的 getSubjects 和 getMessage 方法调用需要带 Signature 元素的一个实例。 
Signature 实例将用于在服务器端进行身份验证。这反映在安全电子邮件服务的 WSDL 文件的 data types 部分。您已经在第 1 部分的“酒店 Web 服务的 WSDL 组件”部分中看到了 WSDL 文件中的 data types。

清单 2 显示了安全电子邮件服务的 WSDL 文件的 data types 部分。

清单 2. 安全电子邮件服务的 WSDL 的 data types

                    
<types>
    <xsd:schema xmlns="http://www.w3.org/2001/XMLSchema"
          targetNamespace=
            "http://www.everydaywebservices.com/secureemailservice">
       <xsd:element name="getSubjects">
          <xsd:complexType>
             <xsd:sequence>
                <xsd:element name="senderEmailAddress" 
                   type="xsd:string"/>
                <xsd:element name="Signature" 
                   type="tns:Signature"/>
            </xsd:sequence>
         </xsd:complexType>
      </xsd:element>
      <xsd:element name="getMessage">
         <xsd:complexType>
            <xsd:sequence>
               <xsd:element name="subject" type="xsd:string"/>
               <xsd:element name="from" type="xsd:string"/>
               <xsd:element name="Signature" type="tns:Signature"/>
            </xsd:sequence>
         </xsd:complexType>
      </xsd:element>
       <xsd:element name="subjectsList">
          <xsd:complexType>
             <xsd:sequence>
                <xsd:element name="subjectLine" 
                   type="xsd:string" minOccurs="0"
                   maxOccurs="unbounded"/>
             </xsd:sequence>
          </xsd:complexType>
       </xsd:element>
      <xsd:element name="message">
         <xsd:complexType>
            <xsd:sequence>
              <xsd:element name="subject" type="xsd:string"/>
              <xsd:element name="from" type="xsd:string"/>
              <xsd:element name="messageText" type="xsd:string"/>
            </xsd:sequence>
         </xsd:complexType>
      </xsd:element> 
       <xsd:complexType name="Signature">
          <!-- Same as Listing 1 -->
       </xsd:complexType>
    </xsd:schema>
</types>

请注意,Signature 是作为清单 2 中的 getSubjects 和 getMessage 复杂类型的子元素出现的。getSubjects 方法调用将使用 getSubjects 复杂类型,而 getMessage 方法将使用 getMessage 复杂类型。由于 Signature 元素同时属于这两个复杂类型,因此这个元素将会同时出现在这两个方法调用中。




回页首


简化 Signature 元素

我们使用 W3C 的 XML Digital Signature (XMLDS) 规范来将身份验证功能合并到 WSA 应用程序中。

第 1 部分的“WSA 的一个重要限制”中对使用 XMLDS 时的一个 WSA 限制进行了说明。第 1 部分“克服 WSA 限制”部分说明了如何克服这个 WSA 限制。

我们在定义清单 1 中的 Signature 元素时,还引入了以下简化。

 

  1. XMLDS 中的 Signature 元素的原始定义具有很多扩展点,允许将来自其他命名空间和模式的元素嵌入到 Signature 元素中。
    例如,XMLDS 在其 KeyInfo 元素中指定了一个 xsd:Any 元素,即实例文档可以在 KeyInfo 元素内包含任意元素。
    清单 1 的 Signature 定义中,删除了所有来自 Signature 元素的扩展性点。这是因为 J2ME 客户机需要创建一个固定结构,因此并不需要扩展性。
  2. XMLDS 包括各种元素,涵盖了不同的应用程序场景。例如,Signature 元素的 KeyInfo 子项带有关于用于对消息进行签名的密钥的信息。XMLDS 定义了多种可以放置到 KeyInfo 元素中的元素类型。KeyName 和 X509Data 就是两个这样的元素。
    KeyName 元素包装密钥的名称。因此,如果希望按名称表示加密密钥,您将使用 KeyName 元素。 
    另一方面,如果您希望使用 X509 证书替代密钥名称,可以使用 X509Data 元素替代 KeyName元素。X509Data 元素将包装证书数据(而不是密钥名称)。

 

在本系列教程中,我们将仅使用密钥名称来标识加密密钥。因此,清单 1 中的 Signature 复杂类型仅允许将一个元素 (KeyName) 放置到 KeyInfo 元素中。

简而言之,我们尝试在不损失其与 XMLDS 的互操作性的前提下简化安全 WSA 客户机。 




回页首


安全电子邮件服务的安全 SOAP 请求

请看清单 3,其中显示了 getSubjects 操作的安全 SOAP 请求(或安全 SOAP 方法调用)。根据清单 1 的 Signature 定义,清单 3 包含 Signature 元素的一个实例。

清单 3. getSubjects 操作的安全 SOAP 请求

                    
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
        xmlns:tns="http://www.everydaywebservices.com/secureemailservice">
<soap:Body>
<tns:getSubjects>
   <tns:senderEmailAddress>
       bob@mycompany.com
   </tns:senderEmailAddress>
   <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
      <SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
         <CanonicalizationMethod 
            xmlns="http://www.w3.org/2000/09/xmldsig#">
            http://www.w3.org/2000/09/xmldsig#rsa-sha1
         </CanonicalizationMethod>
         <SignatureMethod 
            xmlns="http://www.w3.org/2000/09/xmldsig#">
            http://www.w3.org/2001/10/xml-exc-c14n#
         </SignatureMethod>
         <Reference xmlns="http://www.w3.org/2000/09/xmldsig#">
            <DigestMethod 
               xmlns="http://www.w3.org/2000/09/xmldsig#">
               http://www.w3.org/2000/09/xmldsig#sha1
            </DigestMethod>
            <DigestValue
               xmlns="http://www.w3.org/2000/09/xmldsig#">
               gmwIRIS+Od1t7+kBmCEsflI4I3U=
            </DigestValue>
            <URI xmlns="http://www.w3.org/2000/09/xmldsig#">
               //soap:Envelope/soap:Body/*
            </URI>
         </Reference>
      </SignedInfo>
      <SignatureValue 
         xmlns="http://www.w3.org/2000/09/xmldsig#">
           fWjfJqN7AV78v+Ye8B3qACRmhzC+FCXZT
           5pEfmy+Um1/1I+EBOrTNxFPMyykBq3JhR
      </SignatureValue>
      <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
         <KeyName xmlns="http://www.w3.org/2000/09/xmldsig#">
            Alice
         </KeyName>
      </KeyInfo>
    </Signature>
</tns:getSubjects>
</soap:Body>
</soap:Envelope>

清单 3 显示了 getSubjects SOAP 请求的结构和其中包含的数据(或内容)。请注意清单 3 中以粗体显示的部分。这些是唯一可为每个 getSubjects 请求动态创建的内容。结构的其他部分在所有 getSubjects 请求中是完全相同的。

以下对清单 3 中以粗体显示的所有动态部分进行了说明。

  1. senderEmailAddress 元素的内容,指定您所查找的电子邮件的发件人电子邮件地址。
  2. DigestValue 元素的内容,表示摘要值的 Base 64 编码形式。我们将在在 J2ME 中实现 SHA-1 摘要算法部分和本系列教程下一部分有关 Base 64 编码的详细信息中说明此摘要值的计算过程。
  3. SignatureValue 元素的内容,表示加密签名的 Base 64 编码形式。我们将在本教程的下一部分讨论计算此签名值的详细信息。
  4. KeyName 元素的内容,指定用于对消息进行签名的加密密钥的名称。KeyName 元素的创建在实例化 GetSubjectsSignatureKeyInfo 部分中说明。

 

与此类似,请看清单 4 中所示的 getMessage 操作的安全 SOAP 请求。您可以非常容易地在清单 4 中看到以粗体显示的动态创建的部分。

清单 4. getMessage 操作的安全 SOAP 请求

                    
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
        xmlns:tns="http://www.everydaywebservices.com/secureemailservice">
<soap:Body>
<tns:getMessage>
<tns:subject>Recent Project</tns:subject>
<tns:from>bob@mycompany.com</tns:from>
   <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
      <SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
         <CanonicalizationMethod 
            xmlns="http://www.w3.org/2000/09/xmldsig#">
            http://www.w3.org/2000/09/xmldsig#rsa-sha1
         </CanonicalizationMethod>
         <SignatureMethod 
            xmlns="http://www.w3.org/2000/09/xmldsig#">
            http://www.w3.org/2001/10/xml-exc-c14n#
         </SignatureMethod>
         <Reference xmlns="http://www.w3.org/2000/09/xmldsig#">
            <DigestMethod 
               xmlns="http://www.w3.org/2000/09/xmldsig#">
               http://www.w3.org/2000/09/xmldsig#sha1
            </DigestMethod>
            <DigestValue
               xmlns="http://www.w3.org/2000/09/xmldsig#">
               Tp9KxmwIRIS+kBsvD5+mCEsflI4I3U=
            </DigestValue>
            <URI xmlns="http://www.w3.org/2000/09/xmldsig#">
               //soap:Envelope/soap:Body/*
            </URI>
         </Reference>
      </SignedInfo>
      <SignatureValue 
         xmlns="http://www.w3.org/2000/09/xmldsig#">
           WcDcxS5yDsfWjfJqN7AV78v+Ye8B3qACRmhzC+FCXZT
           Ds+FE4dx5pEfmy+Um1/1I+EBOrTNxFPMyykBq3JhR
      </SignatureValue>
      <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
         <KeyName xmlns="http://www.w3.org/2000/09/xmldsig#">
            Alice
         </KeyName>
      </KeyInfo>
    </Signature>
</tns:getMessage>
</soap:Body>
</soap:Envelope>

我们已经讨论了安全电子邮件服务的 WSDL 文件和 SOAP 请求,接下来要为安全电子邮件服务生成存根类。

您可以使用存根生成器工具来为安全电子邮件服务的 WSDL 文件生成存根类。为了方便起见,我们在本教程的下载部分提供了 WSDL 文件及其对应的存根类的源代码。




回页首


安全电子邮件服务的存根类

存根生成器工具总共为安全电子邮件服务生成了十个类。十个类中的四个类(GetMessageGetSubjectsSubjectsList 和 Message)表示 getSubjects 和 getMessage 操作的输入和输出参数。存根生成器工具根据 WSDL 文件中的复杂类型元素命名这四个类。

其他的类如下:

  1. SecureMobileEmailService_PortType
  2. SecureMobileEmailService_PortType_Stub
  3. Signature
  4. GetSubjectsSignatureKeyInfo
  5. GetSubjectsSignatureSignedInfo
  6. GetSubjectsSignatureSignedInfoReference

 

SecureMobileEmailService_PortType 是包含 getSubjects() 和 getMessage() 的定义的接口(与您在第 1 部分的“extended email service 的存根类”中看到的 ExtendedMobileEmailService_PortType 接口类似)。

SecureMobileEmailService_PortType_Stub 实现 getSubjects() 和 getMessage() 方法。这意味着 SecureMobileEmailService_PortType_Stub 包含使用 WSA 类和接口进行以下工作的代码:

  1. 与远程电子邮件服务安全通信
  2. 从远程服务提取和解析响应
  3. 将结果返回给调用应用程序

 

正如您猜到的,SecureMobileEmailService_PortType_Stub 是所有存根类中最重要的。我们将其称为“安全服务存根”类。您稍后将在增强安全服务存根类部分了解如何修改(或增强)此类。

但我们将首先介绍表示清单 1 中所示的 Signature 复杂类型的四个类(SignatureGetSubjectsSignatureSignedInfoGetSubjectsSignatureSignedInfoReference 和 GetSubjectsSignatureKeyInfo)。




回页首


Signature 复杂类型的存根类

让我们看看 Signature 类;为了方便参考,我们已将其复制到了清单 5 中。

清单 5. Signature 类

                    
public class Signature {
    protected secure.GetSubjectsSignatureSignedInfo signedInfo;
    protected java.lang.String signatureValue;
    protected secure.GetSubjectsSignatureKeyInfo keyInfo;
    
    public Signature() {
    }
    
    public Signature(secure.GetSubjectsSignatureSignedInfo signedInfo,
        java.lang.String signatureValue, 
        secure.GetSubjectsSignatureKeyInfo keyInfo) {
        this.signedInfo = signedInfo;
        this.signatureValue = signatureValue;
        this.keyInfo = keyInfo;
    }
    
    public secure.GetSubjectsSignatureSignedInfo getSignedInfo() {
        return signedInfo;
    }
    
    public void setSignedInfo(
        secure.GetSubjectsSignatureSignedInfo signedInfo) {
        this.signedInfo = signedInfo;
    }
    
    public java.lang.String getSignatureValue() {
        return signatureValue;
    }
    
    public void setSignatureValue(java.lang.String signatureValue) {
        this.signatureValue = signatureValue;
    }
    
    public secure.GetSubjectsSignatureKeyInfo getKeyInfo() {
        return keyInfo;
    }
    
    public void setKeyInfo(
        secure.GetSubjectsSignatureKeyInfo keyInfo) {
        this.keyInfo = keyInfo;
    }
}

Signature 类将包装 Signature 复杂类型的实例可包含的所有内容(如清单 3 中的 Signature 元素的 SOAP 消息)。Signature 类并不包含任何对其包含的数据进行创建或处理的代码。

签名的 XML 结构的实际处理逻辑在安全服务存根类中实现。

Signature 类包含两个构造函数;无参数缺省构造函数或三参数构造函数。三参数构造函数接受三个对象,这些对象将成为 Signature 类的数据成员:

  1. 第一个是 GetSubjectsSignatureSignedInfo 类的实例,此类是表示清单 1 的 Signature 结构中定义的 SignedInfo 复杂类型的另一个存根类。
  2. 第二个是名为 signatureValue 的字符串,表示加密签名的 Base 64 加密形式。
  3. 最后一个是 GetSubjectsSignatureKeyInfo 类的实例,此类表示清单 1 的 KeyInfo 元素。

 

Signature 类具有 setter 和 getter 方法来设置和获取其三个数据成员。我们在图 1 中以方框套方框的形式显示了 Signature 类及其数据成员。 

图 1. Signature 类及其数据成员
 

您可以对图 1 进行扩展,以包括 Signature 的数据成员的数据成员。您将得到与图 2 类似的结果,其中显示了存根类如何将其他存根类以及基本数据类型作为其数据成员包括进去。图 2 中所有类名称都以粗体显示,而基本类型数据成员都以斜体显示。

您可以在图 2 中看到所有四个 Signature 相关的存根类。

图 2. 完整的 Signature 结构
 

请注意,存根生成器工具生成的类名称相当长,其中包含 WSDL 文件中不同复杂类型的名称。

例如,GetSubjectsSignatureSignedInfo 类名称包含清单 1 中所示 WSDL 文件的 SignedInfo 元素的所有祖先的名称(GetSubjects 和 Signature)。

为了方便起见,我们在本教程的讨论中不使用 GetSubjectsSignatureSignedInfoGetSubjectsSignatureSignedInfoReference 和 GetSubjectsSignatureSignedInfoKeyInfo 类的完整名称。我们将分别使用 SignedInfoReference 和 KeyInfo 予以替换。

现在已经介绍了安全电子邮件服务的存根类,我们将在下一部分说明和演示增强这些存根类的策略。




回页首


构建基于 J2ME 的安全 SOAP 客户机: 第 2 部分:增强 J2ME 的 Web 服务 API (WSA) 中的存根类

实现安全算法

第 3 页,共 12 页


对本教程的评价

帮助我们改进这些内容


部分 3. 增强安全服务存根类

getSubjects() 方法的安全版本

为了讨论如何增强安全服务存根类,我们将首先看看它的 getSubjects() 方法。此讨论将清楚地说明维护存根生成器生成的存根类原始形式并不足以提供包含您的电子邮件服务所需的身份验证功能。

清单 6 显示了存根生成器生成的原始形式的 getSubjects() 方法实现。

清单 6. getSubjects() 方法实现的原始形式

                    
public java.lang.String[] getSubjects(
      java.lang.String senderEmailAddress, 
      secure.Signature signature) 
      throws java.rmi.RemoteException 
{
    /**** Putting input parameters into an Object array ****/
    Object[] inputObject = new Object[2];
    inputObject[0] = senderEmailAddress;
    
    Object[] signatureObject;
    if (signature == null) {
        signatureObject = null;
    } else {
        Object[] getSubjectsSignatureSignedInfoObject;
        if (signature.getSignedInfo() == null) {
        getSubjectsSignatureSignedInfoObject = null;
        } else {
        Object[] getSubjectsSignatureSignedInfoReferenceObject;
        if (signature.getSignedInfo().getReference() == null) {
            getSubjectsSignatureSignedInfoReferenceObject = null;
        } else {
            getSubjectsSignatureSignedInfoReferenceObject = new Object[3];
            getSubjectsSignatureSignedInfoReferenceObject[0] =
                signature.getSignedInfo().getReference().getDigestMethod();
            getSubjectsSignatureSignedInfoReferenceObject[1] =
                signature.getSignedInfo().getReference().getDigestValue();
            getSubjectsSignatureSignedInfoReferenceObject[2] =
                signature.getSignedInfo().getReference().getURI();
        }
        getSubjectsSignatureSignedInfoObject = new Object[3];
        getSubjectsSignatureSignedInfoObject[0] = 
            signature.getSignedInfo().getCanonicalizationMethod();
        getSubjectsSignatureSignedInfoObject[1] = 
            signature.getSignedInfo().getSignatureMethod();
        getSubjectsSignatureSignedInfoObject[2] = 
            getSubjectsSignatureSignedInfoReferenceObject;
        }
        
        Object[] getSubjectsSignatureKeyInfoObject;
        if (signature.getKeyInfo() == null) {
            getSubjectsSignatureKeyInfoObject = null;
        } else {
            getSubjectsSignatureKeyInfoObject = new Object[1];
            getSubjectsSignatureKeyInfoObject[0] = 
            signature.getKeyInfo().getKeyName();
        }
        signatureObject = new Object[3];
        signatureObject[0] = getSubjectsSignatureSignedInfoObject;
        signatureObject[1] = signature.getSignatureValue();
        signatureObject[2] = getSubjectsSignatureKeyInfoObject;
    }
    inputObject[1] = signatureObject;
    /**** Step 1 ****/
    Operation op = Operation.newInstance(
                      _qname_getSubjects, 
                      _type_getSubjects, 
                      _type_subjectsList);
    /**** Step 2 ****/
    _prepOperation(op);
    op.setProperty(Operation.SOAPACTION_URI_PROPERTY,
        "http://www.everdaywebservices.com/secureemailservice/getSubjects");
    Object resultObj;
    try {
        /**** Step 3 ****/
        resultObj = op.invoke(inputObject);
    } catch (JAXRPCException e) {
        Throwable cause = e.getLinkedCause();
        if (cause instanceof java.rmi.RemoteException) {
        throw (java.rmi.RemoteException) cause;
        }
        throw e;
    }
    /**** Step 4 ****/
    java.lang.String[] result;
    Object subjectLineObj = ((Object[])resultObj)[0];
    result = (java.lang.String[]) subjectLineObj;
    return result;
}

清单 6 与您在第 1 部分的清单 17 中所看到的简单电子邮件服务的 getSubjects() 方法类似,不过有以下几处差别:

  1. 安全电子邮件服务要求随 getSubjects() 方法调用一起使用身份验证信息。正是由于这个原因,清单 6 的 getSubjects() 方法接受 Signature 对象作为其第二个参数。Signature 对象包装所需的身份验证信息。
    第 1 部分讨论的简单电子邮件服务和扩展电子邮件服务的 getSubjects() 方法并不要求任何身份验证信息,因此并不使用此 Signature 对象作为参数。
  2. 请回顾一下第 1 部分的清单 21 中标记为“Putting input parameters into an Object array”的代码片段。安全电子邮件服务的 getSubjects() 方法也包含“Putting input parameters into an Object array”代码片段。您可以对这两个“Putting input parameters into an Object array”代码片段进行一下比较。
    由于 Signature 是包含多个组件的复杂类型(如图 2 中所示),因此清单 6 中将输入参数包装为对象数组的过程更为复杂。清单 6 中的 getSubjects() 方法将所有输入参数包装为名为 inputObject 的数组。如果其中一个参数是具有多个组件的复杂类型(如 Signature),则会首先将组件放入到对象数组中(如清单 6 中名为 signatureObject 的数组),然后将 signatureObject 数组转换为主 inputObject 数组。

 




回页首


J2ME MIDlet 将如何使用 getSubjects()

现在让我们讨论一下 J2ME MIDlet 将如何使用 getSubjects() 或 getMessage() 方法。

请回顾一下在第 1 部分的“使用简单电子邮件服务”部分看到的三个步骤。使用安全服务存根类的方法的最简单方式是遵循与此相同的步骤。请看下面清单 7 中所示的步骤:

清单 7. 使用安全电子邮件服务的 getSubjects() 的一个可能方式

                    
/**** Step 1 ****/
SecureMobileEmailService_PortType_Stub service = 
    new SecureMobileEmailService_PortType_Stub();
/**** Step 2 ****/
service._setProperty(
    SecureMobileEmailService_PortType_Stub.SESSION_MAINTAIN_PROPERTY, 
    new Boolean(true));
/**** Extra step ****/
//Authoring the Signature structure
GetSubjectsSignatureSignedInfoReference reference = 
    new GetSubjectsSignatureSignedInfoReference (
        "http://www.w3.org/2000/09/xmldsig#sha1",
        "kCXsxdrWoDieW83lsjsX90SEf34HDf9ksddDG=",
        "signedElementId");
GetSubjectsSignatureSignedInfo signedInfo = 
    new GetSubjectsSignatureSignedInfo ( 
        "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
        "http://www.w3.org/2001/10/xml-exc-c14n#",
        reference);
    
GetSubjectsSignatureKeyInfo keyInfo = 
     new GetSubjectsSignatureKeyInfo ("alice");
Signature signature = 
    new Signature (
        signedInfo,
        "X5fdk3Df8dGHWks9231lf0DWsdGljlafdSskD32HRplSb9DwsdGJLv0d23D2d", 
        keyInfo);
/**** Step 3 ****/
//Making getSubjects request 
String[] subjectsList = service.getSubjects("bob@mycompany.com", signature);

如果将以前的三个步骤(请参见第 1 部分的清单 18)与清单 7 进行比较,您将只会在清单 7 中看到一个标记为“Extra step”的额外步骤。

在这个额外的步骤中,J2ME MIDlet 以 Signature 对象的形式创建了完整的签名结构,会在步骤 3 中将此对象随 getSubjects() 方法一起传递。

J2ME MIDlet 为何需要创建所有签名相关的对象?

清单 7 中所示的技术是您在 WSA 应用程序中采用复杂类型时将通常采用的标准方法。这意味着在调用 Web 服务的远程操作前,您的 J2ME MIDlet 会将客户端数据包装在 WSA 对象中。

不过,您可以在 getSubjects() 方法实现内方便地隐藏创建功能。

这意味着所有创建工作都能够在 getSubjects() 方法实现内进行。我们稍后将在增强 getSubjects() 方法实现部分演示 getSubjects() 或 getMessage() 方法如何实现所有签名创建逻辑。就目前而言,只要注意将所有签名创建逻辑隐藏在 getSubjects() 中后,J2ME MIDlet 就可以更方便地使用 getSubjects() 方法了,如清单 8 中所示。

清单 8. 使用在内部创建自己的签名的 getSubjects() 管理

                    
/**** Step 1 ****/
SecureMobileEmailService_PortType_Stub service = 
    new SecureMobileEmailService_PortType_Stub("alice", "password");
/**** Step 2 ****/
service._setProperty(
SecureMobileEmailService_PortType_Stub.SESSION_MAINTAIN_PROPERTY,
new Boolean(true));
/**** Step 3 ****/
String[] subjectsList = service.getSubjects("bob@mycompany.com");

您可以对清单 7 和清单 8 进行比较,会发现清单 8 中不再需要清单 7 中的额外步骤了。




回页首


提供用于访问 Java Card 的用户名和密码

请看清单 8 的步骤 1,其中您的 MIDlet 实例化 SecureMobileEmailService_PortType_Stub 对象,并同时向 SecureMobileEmailService_PortType_Stub 构造函数传递了两个参数(username和 password)。

SecureMobileEmailService_PortType_Stub 构造函数为何需要 username 和 password 呢?

第 1 部分的“使用 Java Card”部分,我们将 Java Card 用于以下两个目的:

  1. 安全存储加密密钥
  2. 实现签名计算逻辑

 

Java Card 内部的签名计算应用程序将需要用户-密码对,然后才能使用与其一起存储的用户加密密钥创建所需签名。因此,您的 MIDlet 需要将用户-密码对传递给 SecureMobileEmailService_PortType_Stub 构造函数,如清单 8 的步骤 1 中所示。

在本教程的下一部分,SecureMobileEmailService_PortType_Stub 类将使用用户-密码对来访问 Java Card 应用程序。

The SecureMobileEmailService_PortType_Stub 构造函数将 username 和 password 字符串存储在其类级别的变量中,以便存根类的不同方法使用 username 和 password 字符串访问 Java Card。




回页首


增强安全服务存根类的数据成员

您已经了解了如何使用 SecureMobileEmailService_PortType_Stub 类的 getSubjects() 方法。现在我们将了解如何增强 SecureMobileEmailService_PortType_Stub 类。要增强的第一个字符串是 SecureMobileEmailService_PortType_Stub 类的数据成员代码片段。

回顾一下我们在第 1 部分的“存根类的数据成员”部分中讨论的数据成员代码片段。

请看清单 9,其中复制了存根生成器生成的数据成员代码片段的原始形式。

清单 9. SecureMobileEmailService_PortType_Stub 类的数据成员

                    
//Signature related qualified names
protected static final QName _qname_CanonicalizationMethod = 
    new QName("", "CanonicalizationMethod");
protected static final QName _qname_DigestMethod = 
    new QName("", "DigestMethod");
protected static final QName _qname_DigestValue = 
    new QName("", "DigestValue");
protected static final QName _qname_KeyInfo = 
    new QName("", "KeyInfo");
protected static final QName _qname_KeyName = 
    new QName("", "KeyName");
protected static final QName _qname_Reference = 
    new QName("", "Reference");
protected static final QName _qname_Signature = 
    new QName("", "Signature");
protected static final QName _qname_SignatureMethod = 
    new QName("", "SignatureMethod");
protected static final QName _qname_SignatureValue = 
    new QName("", "SignatureValue");
protected static final QName _qname_SignedInfo = 
    new QName("", "SignedInfo");
protected static final QName _qname_URI = new QName("", "URI");
//Other data members
protected static final QName _qname_from = new QName("", "from");
protected static final QName _qname_messageText = 
    new QName("", "messageText");
protected static final QName _qname_senderEmailAddress = 
    new QName("", "senderEmailAddress");
protected static final QName _qname_subject = new QName("", "subject");
protected static final QName _qname_subjectLine = 
    new QName("", "subjectLine");
protected static final QName _qname_getMessage = 
    new QName("http://www.everydaywebservices.com/secureemailservice",
        "getMessage");
protected static final QName _qname_getSubjects = 
    new QName("http://www.everydaywebservices.com/secureemailservice",
        "getSubjects");
protected static final QName _qname_message = 
    new QName("http://www.everydaywebservices.com/secureemailservice",
        "message");
protected static final QName _qname_subjectsList = 
    new QName("http://www.everydaywebservices.com/secureemailservice",
        "subjectsList");
protected static final Element _type_getSubjects;
protected static final Element _type_subjectsList;
protected static final Element _type_getMessage;
protected static final Element _type_message;
static {
    // Static Element and ComplexType objects of data members section
}

请注意,在清单 9 中,我们已将数据成员代码片段划分为两个子片段,分别标记为“Signature-related qualified names”和“Other data members”。

让我们看看清单 9 中的“Signature-related qualified names”代码片段。您将发现存根生成器将多个命名空间 URI 声明(QName 构造的第一个参数)保留成了空字符串。这似乎有点奇怪,但随 Sun Java Wireless Toolkit 的 2.2 或 2.3 beta 版提供的存根生成器会产生这样的结果。

这意味着 WSA 创建的 SOAP 消息将不会包含签名相关的元素的命名空间声明。这可能会使服务器端的 XML 签名处理工具不知道如何处理。

因此,您需要提供所有签名相关的限定名称的正确命名空间,如清单 10 的增强数据成员代码片段部分中所示。

清单 10. 数据成员代码片段的增强形式

                    
protected static final QName _qname_CanonicalizationMethod = 
    new QName("
        http://www.w3.org/2000/09/xmldsig#", 
        "CanonicalizationMethod", "ds");
protected static final QName _qname_DigestMethod = 
    new QName(
        "http://www.w3.org/2000/09/xmldsig#", 
        "DigestMethod", "ds");
protected static final QName _qname_DigestValue = 
    new QName(
        "http://www.w3.org/2000/09/xmldsig#", 
        "DigestValue", "ds");
protected static final QName _qname_KeyInfo = 
    new QName(
        "http://www.w3.org/2000/09/xmldsig#", 
        "KeyInfo", "ds");
protected static final QName _qname_KeyName = 
     new QName(
         "http://www.w3.org/2000/09/xmldsig#", 
         "KeyName", "ds");
protected static final QName _qname_Reference = 
     new QName(
         "http://www.w3.org/2000/09/xmldsig#", 
         "Reference", "ds");
protected static final QName _qname_Signature = 
     new QName(
         "http://www.w3.org/2000/09/xmldsig#", 
         "Signature", "ds");
protected static final QName _qname_SignatureMethod = 
     new QName(
         "http://www.w3.org/2000/09/xmldsig#", 
         "SignatureMethod", "ds");
protected static final QName _qname_SignatureValue = 
     new QName(
         "http://www.w3.org/2000/09/xmldsig#", 
         "SignatureValue", "ds");
protected static final QName _qname_SignedInfo = 
     new QName(
         "http://www.w3.org/2000/09/xmldsig#", 
         "SignedInfo", "ds");
protected static final QName _qname_URI = 
     new QName(
         "http://www.w3.org/2000/09/xmldsig#", 
         "URI", "ds");
protected static final QName _qname_from = 
     new QName(
         "http://www.everydaywebservices.com/secureemailservice",
         "from", "tns");
protected static final QName _qname_messageText = 
     new QName(
         "http://www.everydaywebservices.com/secureemailservice", 
         "messageText", "tns");
protected static final QName _qname_senderEmailAddress = 
    new QName(
        "http://www.everydaywebservices.com/secureemailservice",
        "senderEmailAddress", "tns");
protected static final QName _qname_subject = 
    new QName(
        "http://www.everydaywebservices.com/secureemailservice",
        "subject", "tns");
protected static final QName _qname_subjectLine =
    new QName(
        "http://www.everydaywebservices.com/secureemailservice",
        "subjectLine", "tns");
protected static final QName _qname_secureemailservice_getMessage = 
    new QName(
        "http://www.everydaywebservices.com/secureemailservice",
        "getMessage", "tns");
protected static final QName _qname_secureemailservice_getSubjects = 
    new QName(
        "http://www.everydaywebservices.com/secureemailservice",
        "getSubjects", "tns");
protected static final QName _qname_getMessage = 
    new QName(
        "http://www.everydaywebservices.com/secureemailservice",
        "getMessage", "tns");
protected static final QName _qname_getSubjects = 
    new QName(
        "http://www.everydaywebservices.com/secureemailservice",
        "getSubjects", "tns");
protected static final QName _qname_message = 
    new QName(
        "http://www.everydaywebservices.com/secureemailservice",
        "message", "tns");
protected static final QName _qname_subjectsList = 
    new QName(
        "http://www.everydaywebservices.com/secureemailservice",
        "subjectsList", "tns");
protected static final Element _type_getSubjects;
protected static final Element _type_subjectsList;
protected static final Element _type_getMessage;
protected static final Element _type_message;
static {
    // Static Element and ComplexType objects of data members section
    _type_getSubjects = new Element(
        _qname_getSubjects, _complexType_getSubjects);
    /**** Enhanced data members ****/  
    ComplexType _complexType_getSubjects_enhanced;
    _complexType_getSubjects_enhanced = new ComplexType();
    _complexType_getSubjects_enhanced.elements = new Element[1];
    _complexType_getSubjects_enhanced.elements[0] = _type_senderEmailAddress;
    Element _type_getSubjects_enhanced = 
         new Element(_qname_getSubjects, 
            _complexType_getSubjects_enhanced);
   ComplexType _complexType_getMessage_enhanced;
   _complexType_getMessage_enhanced = new ComplexType();
   _complexType_getMessage_enhanced.elements = new Element[2];
   _complexType_getMessage_enhanced.elements[0] = _type_subject;
   _complexType_getMessage_enhanced.elements[1] = _type_from;
   Element _type_getMessage_enhanced = 
       new Element(_qname_getMessage, 
           _complexType_getMessage_enhanced);
}

还要注意,在清单 10 的增强数据成员代码片段中,我们在增强形式中使用的全部是三参数 QName 构造函数,而不是清单 9 的原始形式中使用的双参数 QName 构造函数。

双参数和三参数 QName 构造函数之间的唯一区别是,三参数为限定名称指定命名空间前缀(有关不同 QName 构造函数的详细信息,请参见 第 1 部分的“存根类的数据成员”部分)。

例如,清单 10 中以粗体显示的 _type_getSubjects Element 对象的限定名称是由名为 _qname_getSubjects 的 QName 对象指定的。_type_getSubjects Element 对象表示前面清单 3 的 SOAP 消息。在 _qname_getSubjects QName 构造函数中指定命名空间前缀可将安全服务存根类的输出与清单 3 的 SOAP 消息匹配。

我们还在清单 10 的最后包含了一个名为“Enhanced data members”的代码片段。“Enhanced data members”代码片段定义了一个 Element 对象(名为 _type_getSubjects_enhanced),其中包含 getSubjects 元素的结构,但并不包含签名。在计算签名值时,您将需要使用此结构。

Signature 类将使用此 _type_getSubjects_enhanced Element 对象来创建 XML 签名。 




回页首


增强 getSubjects() 方法实现

在前面的 getSubjects() 方法的安全版本部分中,清单 6 显示了存根生成器工具生成的 getSubjects() 方法实现的原始形式。我们现在将对清单 6 的原始形式进行增强。下面的清单 11 显示了其增强形式: 

清单 11. getSubjects() 方法的增强版本

                    
public java.lang.String[] getSubjects(
    java.lang.String senderEmailAddress) 
    throws java.rmi.RemoteException {
    //Enhancement code
    secure.Signature signature = new secure.Signature (
        _type_getSubjects_enhanced,
        username,  
        password);
    Object[] inputObject = new Object[2];
    inputObject[0] = senderEmailAddress;
    //Enhancement code
    signature.setApplicationData(inputObject);
    /*****************************/
    // Using getter methods of Signature object to author signatureObject
    /*****************************/ 
       Object[] signatureObject;
    	if (signature == null) {
	    signatureObject = null;
       } else {
           Object[] getSubjectsSignatureSignedInfoObject;
           if (signature.getSignedInfo() == null) {
               getSubjectsSignatureSignedInfoObject = null;
           } else {
               Object[] getSubjectsSignatureSignedInfoReferenceObject;
           if (signature.getSignedInfo().getReference() == null) {
               getSubjectsSignatureSignedInfoReferenceObject = null;
           } else {
               getSubjectsSignatureSignedInfoReferenceObject = new Object[3];
               getSubjectsSignatureSignedInfoReferenceObject[0] = 
                   signature.getSignedInfo().getReference().getDigestMethod();
               getSubjectsSignatureSignedInfoReferenceObject[1] =
                   signature.getSignedInfo().getReference().getDigestValue();
               getSubjectsSignatureSignedInfoReferenceObject[2] =
                   signature.getSignedInfo().getReference().getURI();
           }
           getSubjectsSignatureSignedInfoObject = new Object[3];
           getSubjectsSignatureSignedInfoObject[0] = 
               signature.getSignedInfo().getCanonicalizationMethod();
           getSubjectsSignatureSignedInfoObject[1] = 
               signature.getSignedInfo().getSignatureMethod();
           getSubjectsSignatureSignedInfoObject[2] = 
               getSubjectsSignatureSignedInfoReferenceObject;
     }
     
     Object[] getSubjectsSignatureKeyInfoObject;
     if (signature.getKeyInfo() == null) {
         getSubjectsSignatureKeyInfoObject = null;
     } else {
         getSubjectsSignatureKeyInfoObject = new Object[1];
         getSubjectsSignatureKeyInfoObject[0] =
             signature.getKeyInfo().getKeyName();
     }
         signatureObject = new Object[3];
         signatureObject[0] = getSubjectsSignatureSignedInfoObject;
         signatureObject[1] = signature.getSignatureValue();
         signatureObject[2] = getSubjectsSignatureKeyInfoObject;
     }
     inputObject[1] = signatureObject;
     Operation op = Operation.newInstance(
         _qname_getSubjects, 
         _type_getSubjects, 
         _type_subjectsList);
     _prepOperation(op);
     op.setProperty(Operation.SOAPACTION_URI_PROPERTY, 
        "http://www.everdaywebservices.com/secureemailservice/getSubjects");
     Object resultObj;
     try {
         resultObj = op.invoke(inputObject);
     } catch (JAXRPCException e) {
         Throwable cause = e.getLinkedCause();
         if (cause instanceof java.rmi.RemoteException) {
             throw (java.rmi.RemoteException) cause;
         }
	      throw e;
     }
     java.lang.String[] result;
     Object subjectLineObj = ((Object[])resultObj)[0];
     result = (java.lang.String[]) subjectLineObj;
     return result;
}

getSubjects() 方法的增强形式并不接受 Signature 对象作为参数,而仅接受一个参数,而不是传递给 getSubjects() 方法的原始形式的两个参数。

您可将清单 6 中的 getSubjects() 原始版本与清单 11 中的增强版本进行比较。您将发现清单 11 中仅添加了两行代码,即清单 11 中标记为“Enhancement code”的代码行。

这两个增强部分需要进行一定的讨论,即我们接下来将看到的两个部分。




回页首


实例化 Signature 对象

清单 11 中的第一个增强代码行与以下所示类似:

secure.Signature = new secure.Signature(
    _type_getSubjects_enhanced, username, password);

此处您通过向 Signature 构造函数提供三个参数来实例化 Signature 对象。我们在清单 5 中介绍了 Signature 构造函数的原始形式。有些读者可能已经注意到,清单 5 的 Signature 构造函数所接受的参数并不相同。

这意味着您还需要对 Signature 类的原始形式进行增强。我们稍后将在增强 Signature 类部分详细讨论 Signature 类的增强。

我们现在将对向清单 11 中的 Signature 构造函数传递三个参数进行说明。

第一个参数 _type_getSubjects_enhanced 是我们前面在增强安全服务存根类的数据成员部分讨论过的Element 对象。Signature 类应该创建 XML 签名结构。因此,Signature 类需要知道您希望进行签名的 XML 数据结构。数据成员代码片段的 _type_getSubjects_enhanced Element 对象指定要签名的数据的 XML 结构。正是由于这个原因,您需要将 _type_getSubjects_enhanced Element 对象传递给 Signature 构造函数。

稍后,在本教程的创建 XML 数据的规范形式部分,我们将演示如何使用 _type_getSubjects_enhanced Element 对象创建 XML 结构。

第二个和第三个参数分别是 username 和 password,已经在前面的提供用于访问 Java Card 的用户名和密码部分进行了说明。




回页首


将应用程序数据传递给 Signature 类

第一个参数是名为 _type_getSubjects_enhanced 的 Element 对象,指定您希望签名的 XML 结构。XML 结构充当应用程序数据的包装。

例如,对于 getSubjects() 方法,要签名的 XML 结构如下面的清单 12 中所示。它包含应用程序数据的一个占位符。

清单 12. 包含应用程序数据的占位符的 XML 结构

                    
<tns:getSubjects>
    <tns:senderEmailAddress>
       Placeholder for application data
    </tns:senderEmailAddress>
</tns:getSubjects>

此占位符中保存发件人的电子邮件地址。

如果您将应用程序数据放置在占位符中,就得到了清单 13 所示的结构。

清单 13. 带应用程序数据的完整 XML 结构

                    
<tns:getSubjects>
    <tns:senderEmailAddress>
       bob@mycompany.com
    </tns:senderEmailAddress>
</tns:getSubjects>

_type_getSubjects_enhanced 对象并不包含实际的应用程序数据。它仅指定 XML 结构。

Signature 类需要知道 XML 结构和应用程序数据,然后才能创建 XML 签名。因此,您还需要将应用程序数据传递给 Signature 类。

为此,我们在清单 11 的 getSubjects() 方法中的第二个增强代码行。可以从清单 11 中看到,第二个增强代码行调用 Signature.setApplicationData() 方法,并同时随此方法调用传递名为 inputObject 的对象数组。清单 11 的 inputObject 数组中包含应用程序数据(发件人的电子邮件地址)。

现在已经对 SecureMobileEmailService_PortType_Stub 类进行了增强,以提供安全功能。不过,还需要增强 Signature 类,我们将在下一部分进行此工作。




回页首



构建基于 J2ME 的安全 SOAP 客户机: 第 2 部分:增强 J2ME 的 Web 服务 API (WSA) 中的存根类

实现安全算法

第 4 页,共 12 页


对本教程的评价

帮助我们改进这些内容


部分 4. 增强 Signature 存根类

增强 Signature 类

在本部分,我们将说明如何增强前面在安全电子邮件服务的存根类部分遇到的四个 Signature 相关类(SignatureGetSubjectsSignatureSignedInfoGetSubjectsSignatureSignedInfoReference 和 GetSubjectsSignatureKeyInfo)。

清单 14 显示了 Signature 类的增强版本。

清单 14. Signature 类的增强形式

                    
public class Signature {
    private Element inputElement;
    private Object applicationData;
    private String username;
    private String password;
    protected secure.GetSubjectsSignatureSignedInfo signedInfo;
    protected java.lang.String signatureValue;
    protected secure.GetSubjectsSignatureKeyInfo keyInfo;
    
    public Signature ( 
        Element inputElement, 
        String username, 
        String password) 
    {
        this.inputElement = inputElement;
        this.username     = username;
        this.password    = password;
    }
    public Signature ( Element inputElement, 
                       Object inputObject, 
                       String username, 
                       String password) 
    {
        this.inputElement = inputElement;
        this.applicationData  = inputObject;
        this.username     = username;
        this.password    = password;
        populateDataMembers();
    }
    public void setApplicationData (Object inputObject) {
        this.applicationData = inputObject;
        populateDataMembers();
    }
    private void populateDataMembers() {
        GetSubjectsSignatureKeyInfo keyInfo = 
            new GetSubjectsSignatureKeyInfo (username);
        this.keyInfo = keyInfo; 
        GetSubjectsSignatureSignedInfo signedInfo = 
            new GetSubjectsSignatureSignedInfo ( 
               inputElement,
               applicationData);
        this.signedInfo = signedInfo;
        SignatureCalculator signatureCalculator = 
            new SignatureCalculator(signedInfo, username, password);
        byte[] signatureValueInBinaryForm = 
           signatureCalculator.getSignatureValue();
        Base64Encoder base64Encoder = new Base64Encoder();
        this.signatureValue = 
            base64Encoder.getBase64EncodedValue(signatureValueInBinaryForm);
    }
    
    public secure.GetSubjectsSignatureSignedInfo getSignedInfo() {
        return signedInfo;
    }
    
    public secure.GetSubjectsSignatureKeyInfo getKeyInfo() {
        return keyInfo;
    }
 
    public java.lang.String getSignatureValue() {
        return signatureValue;
    }
}

您可以将此增强版本与由存根生成器工具生成的原始形式(清单 5)相比较。您会发现,与前面的清单 5 相比,清单 14 具有以下方面的增强:

 

  1. Signature 类的增强形式具有两个构造函数——一个三参数构造函数和一个四参数构造函数,以及前面在将应用程序数据传递给 Signature 类部分中看到的 setApplicationData() 方法。将在下一部分对这两个构造函数和 setApplicationData() 方法进行说明。
  2. 清单 14 除了 setApplicationData() 方法外没有任何 setter 方法。 
    为了理解删除 setter 方法的原因,请参考在 J2ME MIDlet 将如何使用 getSubjects() 部分提到的三个步骤。这些步骤并不要求将任何数据成员设置到 Signature 类中。
    类似地,还可以参见清单 11 中所示的 getSubjects() 方法实现。您将看到,并没有任何 Signature.setXX() 方法调用。
    Signature 类的增强形式希望以向 Signature 构造函数(structureusername 和 password)提供的数据为基础创建自己的数据成员。这也是您没有在清单 14 中 Signature 类的增强形式中发现任何 setter 方法的一个原因。
    这意味着 Signature 类并不需要 setApplicationData() 方法之外的任何 setter 方法。
    但它为什么不需要 getter 方法呢?您稍后将在使用存储在 Signature 类中的数据部分获得有关这个问题的答案。我们目前的重点在于讨论 Signature 构造函数和 setApplicationData() 方法如何工作。
  3. Signature 类的增强形式包含一个名为 populateDataMembers() 的私有 Helpter 方法。由于 Signature 类需要创建自己的数据成员,因此需要此 Helper 方法来创建数据成员。将在实例化 Signature 类的数据成员部分对此方法进行说明。

 




回页首


Signature 构造函数

请看清单 14 中所示的两个 Signature 构造函数。三参数构造函数直接将传入参数(usernamepassword 和 Element 对象)存储在类级别的编码中,并不会进一步进行处理。这是因为只有具有应用程序数据时,它才会进行处理工作。

正如已经在将应用程序数据传递给 Signature 类部分中讨论过的,getSubjects() 方法将通过调用 setApplicationData() 方法提供第四个参数。清单 14 中所示的 setApplicationData() 方法将第四个参数作为类级别变量存储,并随后调用 populateDataMembers() 私有 Helper 方法。

populateDataMembers() 方法使用四个输入参数来实例化 Signature 类的数据成员:

  • inputElement
  • applicationData
  • username
  • password
populateDataMembers() 方法的工作情况将在下一部分进行说明。

 

清单 14 中的四参数 Signature 构造函数将四个参数作为类级别变量存储,并随后调用 populateDataMembers() 方法。

现在我们将说明 populateDataMembers() 方法如何实例化和存储 Signature 类的数据成员。




回页首


实例化 Signature 类的数据成员

请看清单 15,其中显示了 populateDataMembers() 方法如何通过四个步骤实例化 Signature 类的数据成员。

清单 15. populateDataMembers() 方法

                    
private void populateDataMembers() {
    /***** Step 1: Instantiating GetSubjectsSignatureKeyInfo ******/
    GetSubjectsSignatureKeyInfo keyInfo = 
        new GetSubjectsSignatureKeyInfo (username);
    this.keyInfo = keyInfo; 
    /***** Step 2: Instantiating GetSubjectsSignatureSignedInfo ******/
    GetSubjectsSignatureSignedInfo signedInfo = 
        new GetSubjectsSignatureSignedInfo ( 
            inputElement,
            applicationData);
    this.signedInfo = signedInfo;
    /***** Step 3: Calculating signature value ******/
    SignatureCalculator signatureCalculator = 
        new SignatureCalculator(signedInfo, username, password);
    byte[] signatureValueInBinaryForm = 
        signatureCalculator.getSignatureValue();
    /***** Step 4: Base 64 encoding the signature value ******/
    Base64Encoder base64Encoder = new Base64Encoder();
    this.signatureValue = 
        base64Encoder.getBase64EncodedValue(signatureValueInBinaryForm);
}

接下来的几个部分将对清单 15 的四个步骤进行说明。




回页首


实例化 GetSubjectsSignatureKeyInfo

清单 15 的步骤 1 中,populateDataMembers() 方法将实例化一个 GetSubjectsSignatureKeyInfo 对象,即您前面在图 2 中所看到的情况。GetSubjectsSignatureKeyInfo 构造函数仅接受一个参数 usernameGetSubjectsSignatureKeyInfo 类会将 username 存储在自己的数据成员中。GetSubjectsSignatureKeyInfo 具有一个 getKeyName() 方法,此方法会将 username 作为 String 对象返回。

username 字符串将用作 KeyName 元素的内容,该元素是在第 1 部分的“使用 XML 签名”部分引入的。KeyName 元素的内容指定用于进行签名的加密密钥的名称。这意味着用户的加密密钥将使用 username 进行标识。

另外还要注意,GetSubjectsSignatureKeyInfo 类并不需要任何增强。这是因为 GetSubjectsSignatureKeyInfo 对象并不需要任何内部处理,仅存储用于进行签名的密钥名称。因此,存根生成器工具 GetSubjectsSignatureKeyInfo 类的原始形式(清单 16)将按照这种方式在安全电子邮件服务中工作。

清单 16. GetSubjectsSignatureKeyInfo 类

                    
public class GetSubjectsSignatureKeyInfo {
    protected java.lang.String keyName;
    public GetSubjectsSignatureKeyInfo() {
    }
    
    public GetSubjectsSignatureKeyInfo(java.lang.String keyName) {
        this.keyName = keyName;
    }
    
    public java.lang.String getKeyName() {
        return keyName;
    }
    
    public void setKeyName(java.lang.String keyName) {
        this.keyName = keyName;
    }
}

实例化 GetSubjectsSignatureKeyInfo 对象后,populateDataMembers() 方法将 GetSubjectsSignatureKeyInfo 对象作为名为 keyInfo 的私有数据成员存储。Signature 类具有一个 getter 方法 getKeyInfo(),该方法将返回此私有数据成员。




回页首


实例化 GetSubjectsSignatureSignedInfo

清单 15 的步骤 2,populateDataMembers() 方法将实例化 GetSubjectsSignatureSignedInfo 对象。GetSubjectsSignatureSignedInfo 构造函数接受两个参数 inputElement 和 applicationDataGetSubjectsSignatureSignedInfo 类使用 inputElement 和 applicationData 参数来计算被签名的数据的摘要值。

请注意,GetSubjectsSignatureSignedInfo 类负责包装清单 1 的 SignedInfo 方法。创建 SignedInfo 元素将涉及到多个步骤,稍后将在实例化 SignedInfo 数据成员部分对这些步骤进行描述。

populateDataMembers() 方法将 GetSubjectsSignatureSignedInfo 对象作为其私有数据成员存储。Signature 的 signedInfo.getSignedInfo() 方法将返回 GetSubjectsSignatureSignedInfo 对象。




回页首


计算签名值

清单 15 的步骤 3 中,populateDataMembers() 方法实例化一个名为 SignatureCalculator 的对象。此类设计用于根据 getSubjects 或 getMessage SOAP 请求中包含的用户数据计算签名值。

SignatureCalculator 构造函数接受 GetSubjectsSignatureSignedInfousername 和 password 作为参数。

请注意,实例化 SignatureCalculator 对象后,populateDataMembers() 方法将调用其 getSignatureValue() 方法,而后者将以字节数组的形式返回签名值。

在本系列教程的第 3 部分,您将了解 SigantureCalculator 类如何使用 GetSubjectsSignatureSignedInfousername 和 password 参数计算签名值。

populateDataMembers() 方法将签名值存储在名为 signatureValueInBinaryForm 的变量中。

请注意,签名值应该为清单 1 的 SignatureValue 元素的内容。

SignatureCalculator.getSigntureValue() 方法返回的签名值为二进制形式。不能将二进制数据直接打包为 XML 格式,因为这样会给 XML 解析器造成混淆。因此,您需要进行一定的编码工作,以便以可读的形式(可以包装在 XML 格式中)表示二进制数据(如签名值或摘要值)。

能以可读文本的形式表示二进制数据的最流行编码技术称为 Base 64 编码,我们在本系列教程中就要实现并使用此技术。我们将在本系列教程的第 3 部分实现名为 Base64Encoder 类。Base64Encoder 类将接受原始二进制数据,并将其转换为 Base 64 编码形式。

就目前而言,只需注意,清单 15 的步骤 4 中的 populateDataMembers() 方法将初始化 Base64Encoder 对象,并同时将二进制签名值传递给 Base64Encoder 构造函数。接下来,它将调用 Base64Encoder对象的 getBase64EncodedValue() 方法获得 Base 64 编码的签名值。最后,它会将 Base 64 编码的值存储在名为 signatureValue 的私有数据成员中。

请注意,清单 14 中的 Signature 类的 getSignatureValue() 方法将返回签名值。




回页首


使用存储在 Signature 类中的数据

您已经了解了 populateDataMembers() 方法如何实例化 Signature 类的各个数据成员。但谁将调用 Signature 类的 getter 方法呢?

请看清单 7 的包含三步骤的 J2ME MIDlet 代码;其中说明了 J2ME MIDlet 将如何使用存根类的增强形式。您将发现其中没有对 Signature 类的 getter 方法的任何调用。这意味着您的 J2ME MIDlet 将不会调用 Signature 类的 getter 方法。

现在再请看 getSubjects() 方法的增强形式(清单 11)。您会发现,在创建“Using getter methods of Signature object”代码片段中的 inputObject 时,getSubjects() 方法将调用 Signature 类的 getter 方法来获取其数据成员。getSubjects() 方法将数据成员放入名为 signatureObject 的数组中,并随后将 signatureObject 放入名为 inputObject 的另一个数组中。

inputObject 数组现在包含所有应用程序数据和所有与签名相关的对象。

因此,Signature 类的 getter 方法的主要目的是用于构建 singatureObject,而后者将最终成为应用程序数据的一部分。应用程序数据以及签名相关的对象将由 WSA 用于创建清单 3 的 SOAP 请求。




回页首


规范化 XML

您已经了解了 Signature 对象使用 SignatureCalculator Helper 类来对 XML 数据进行签名。不过,对 XML 数据进行签名需要创建 XML 数据的规范形式。

而正是由于这个原因,安全电子邮件服务将需要一个额外的 Helper 类来创建要签名的 XML 的规范形式。稍后在创建 XML 数据的规范形式部分,我们将说明 CanonicalAuthor 类的工作方式。CanonicalAuthor 类将接受 inputElement 和 applicationData 参数,并将创建由 structure 对象表示的 XML 规范形式。

SignatureCalculator 类将使用 CanonicalAuthor 来创建 XML 数据的规范形式。

您已经了解了 Signature 类的所有数据成员和三个 Helper 类(CanonicalAuthorSignatureCalculator 和 Base64Encoder),您将需要构建这些内容来保证电子邮件服务的安全。

请注意,在继续实现这三个 Helper 类之前,还需要对您在清单 15 的步骤 2 中看到的 SignedInfo 类进行增强。下一部分将说明如何增强 GetSubjectsSignatureSignedInfo 类。




回页首



构建基于 J2ME 的安全 SOAP 客户机: 第 2 部分:增强 J2ME 的 Web 服务 API (WSA) 中的存根类

实现安全算法

第 5 页,共 12 页


对本教程的评价

帮助我们改进这些内容


部分 5. 增强 SignedInfo 类

实例化 SignedInfo 数据成员

清单 15 的步骤 2 中,您了解了 populateDataMembers() 方法将实例化 GetSubjectsSignatureSignedInfo 对象(或简称 SignedInfo)。

现在请看清单 17,其中显示了 SignedInfo 构造函数的增强形式如何实例化其数据成员。

清单 17. GetSubjectsSignatureSignedInfo 构造函数的增强形式

                    
public GetSubjectsSignatureSignedInfo(
    Element structure, 
    Object content) 
{
    /****** Step 1*******/
    this.canonicalizationMethod = 
        "http://www.w3.org/2001/10/xml-exc-c14n#"; 
    /****** Step 2*******/
    this.signatureMethod = 
        "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
    /****** Step 3*******/
    GetSubjectsSignatureSignedInfoReference reference =
        new GetSubjectsSignatureSignedInfoReference(
            structure,
            content);
    this.reference = reference;
}

SignedInfo 构造函数接受两个构造函数,即 structure 和 contentstructure 参数包含您希望进行签名的请求的 XML 结构。content 参数指定 XML 结构将包装的应用程序数据。

SignedInfo 构造函数的增强形式需要实例化三个数据成员,即 canonicalizationMethodsignatureMethod 和 reference。将在接下来的几个部分中对这三个数据成员进行说明。




回页首


实例化 canonicalizationMethod 和 signatureMethod 数据成员

CanonicalizationMethod 是一个 String 对象,指定要在 XML 签名创建期间使用的规范化算法。规范化可确保得到相同数字流的 XML 等效形式。以清单 18 的两个 XML 元素为例,这二者彼此等效,但具有不同的表示形式。

清单 18. 两个等效的 XML 表示形式

                    
<book id="B-001" type="Technical"/>
<book type="Technical" id="B-001"> </book>    

将规范化应用到这两个 XML 表示形式将得到相同的字节流。

我们在本教程中使用的是 W3C 的 Exclusive XML Canonicalization Specification V1.0(请参见参考资料)。我们稍后将在创建 XML 数据的规范形式部分中说明 W3C 规范化的详细信息。

清单 17 的 SignedInfo 构造函数将硬编码的值存储在 canoncalizationMethod 数据成员中。canonicalizationMethod 数据成员的内容将最终成为清单 3 中的 SOAP 消息的 CanonicalizationMethod 元素的内容。

CanonicalizationMethod 元素由 XML Digital Signature (XMLDS) 规范定义。XMLDS 定义规范化算法的标识符。我们在本教程中使用 W3C 的规范化算法时,会将其标识符硬编码到 canonicalizationMethod 数据成员中。

实例化 SignatureMethod 数据成员与此类似。SignatureMethod 数据成员指定签名算法“http://www.w3.org/2000/09/xmldsig#dsa-sha1”;由 XMLDS 定义,以在创建 XML 签名时使用。

我们在本教程中仅使用一个签名算法,XMLDS 为其定义的标识符为“http://www.w3.org/2000/09/xmldsig#dsa-sha1”。因此,我们将算法标识符硬编码为 signatureMethod 数据成员的值。




回页首


实例化参考数据成员

现在请看清单 17 的 SignedInfo 构造函数中的步骤 3,SignedInfo 构造函数实例化了一个名为 GetSubjectsSignatureSignedInfoReference(或简称 Reference)的类。Reference 构造函数接受两个相同的参数,即作为第一个和第二个参数向 Signature 构造函数(清单 14)传递的 structure和 content 对象。

Reference 类包装清单 3 的 SOAP 消息以下元素的内容:DigestMethodDigestValue 和 URI

请看清单 19,其中显示了 Reference 构造函数如何初始化其三个数据成员 digestMethoddigestValue 和 URI,这三个成员分别包装清单 3 的 DigestMethodDigestValue 和 URI 元素的内容。

清单 19. GetSubjectsSignatureSignedInfoReference 构造函数

                    
public GetSubjectsSignatureSignedInfoReference(
    Element structure,
    Object content) 
{
    /***** Step 1 ******/
    this.digestMethod = "http://www.w3.org/2000/09/xmldsig#sha1"; 
    /***** Step 2 ******/
    this.URI= "//soap:Envelope/soap:Body/*";
    /***** Step 3 ******/
    CanonicalAuthor canonicalAuthor = new CanonicalAuthor();
    String canonicalData = 
        canonicalAuthor.authorCanonicalXML(structure, content);
    /***** Step 4 ******/
    SHA1DigestCalculator digestCalculator = new SHA1DigestCalculator();
    byte[] digestValue = digestCalculator.getValue(canonicalData.getBytes());
    /***** Step 5 ******/
    Base64Encoder encoder = new Base64Encoder();
    this.digestValue = encoder.getBase64EncodedValue(digestValue);
}

下一部分将说明 Reference 构造函数如何实例化其数据成员。




回页首


实例化 DigestMethod 和 URI 数据成员

请注意,清单 19 中的 Reference 构造函数将其两个数据成员(digestMethod 和 URI)的值进行了硬编码。

它之所以对 digestMethod 数据成员进行硬编码,是因为 DigestMethod 元素指定 XML 签名的摘要算法,而我们在本教程中仅使用一个摘要算法。因此没有必要询问用户使用哪个摘要方法。正如您可能已经猜到的,XMLDS 定义本教程中使用的摘要方法的标识符“http://www.w3.org/2000/09/xmldsig#sha1”。

而对 URI 数据成员进行硬编码的原因需要进行一定的解释。

URI 数据成员的值将成为清单 3 中 URI 元素的内容。URI 值用于指定对哪个元素进行了签名来生成 XML 签名。

可以通过多种方式指定被签名的元素。其中一种方法是使用 XPath(请参见参考资料)。在本教程中,我们将始终对 SOAP 方法调用请求(即 SOAP Body 元素的直接子项,如清单 3 中的 getSubjects)进行签名。

正是由于这个原因,我们才在本教程中硬编码了 XPath 表达式,从而始终指向 SOAP body 元素的直接子项。XPath 的细节不在本教程的讨论范围内,因此对于希望了解 XPath 的读者,参考资料部分提供了多个链接,这些链接指向的文章讨论的无线应用程序中的 XPath。

不过,对于本教程,您并不需要了解 XPath。您所需要知道的就是,清单 19 中所使用的“//soap:Envelope/soap:Body/*”XPath 表达式指向 SOAP Body 元素的直接子项。




回页首


实例化 digestValue 数据成员

Reference 类的 DigestValue 数据成员保存基于被签名的数据计算的摘要值。请注意,摘要值包装在清单 3 的 SOAP 消息中的 DigestValue 元素中。这意味着,digestValue 数据成员的值将最终成为清单 3 中 DigestValue 元素的内容。

不过,在基于所签名的数据计算摘要值前,您需要按照讨论清单 18 时所说明的方式对 XML 数据进行规范化,以便进行签名。

因此,在清单 19 的步骤 3 中,Reference 构造函数将实例化一个名为 CanoincalAuthor 的类,并同时将 structure 和 content 参数传递给 CanoincalAuthor 构造函数。它将随后调用 CanonicalAuthor.authorCanonicalXML() 方法,此方法将返回要签名的 XML 数据的规范形式。

稍后在在 J2ME 中实现 SHA-1 摘要算法部分,我们将编写一个名为 SHA1DigestCalculator 的类,该类将接受要进行摘要的数据的规范形式,并基于此数据计算摘要值。

在步骤 4 中,Reference 构造函数实例化一个 SHA1DigestCalculator 对象,并同时将 XML 数据参数的规范化形式传递给 SHA1DigestCalculator 构造函数。

SHA1DigestCalculator 类具有一个名为 getValue() 的方法,该方法会将摘要值作为字节数组返回。

摘要值需要包装在 XML 元素中。Reference 构造函数使用 Base64Encoder 类(讨论清单 15 时曾提到过)来将二进制数据编码为可读的格式。

因此,在步骤 5 中,Reference 构造函数将实例化 Base64Encoder 对象,并随后调用其 getBase64EncodedValue() 方法,而此方法将返回摘要值的 Base 64 编码形式。

Reference 构造函数将 Base 64 编码的摘要值存储在 digestValue 数据成员中。Reference 类的 getDigestValue() 方法将返回此摘要值。

您现在已经了解了 Signature 相关类的增强版本中的所有数据成员。您已经看到了我们将在本教程剩下部分中进行构建的重要 Helper 类 SignatureCalculatorSHA1DigestCalculatorCanonicalAuthor 和 Base64Encoder

除了四个 Helper 类外,存根类的增强版本并不需要任何进一步的增强。源代码的增强版本可以在本教程的下载部分找到。

本教程剩下的部分将全面演示如何开发、测试和调试四个 Helper 类(CanonicalAuthorSHA1DigestCalculatorBase64Encoder 和 SignatureCalculator)。




回页首


构建基于 J2ME 的安全 SOAP 客户机: 第 2 部分:增强 J2ME 的 Web 服务 API (WSA) 中的存根类

实现安全算法

第 6 页,共 12 页


对本教程的评价

帮助我们改进这些内容


部分 6. 创建 XML 数据的规范形式

XML 规范化简介

规范化 XML 部分介绍了 CanonicalAuthor 类,其构造函数具有两个参数 structure 和 content。structure 和 content 参数是表示输入 XML 数据的输入对象。这种情况下的输入 XML 数据是出现在 SOAP Body 元素内的任意内容,如 清单 3 的 getSubjects 元素。

CanonicalAuthor 类具有一个名为 authorCanonicalXML() 的方法,该方法创建并返回输入 XML 数据的规范形式。

此部分将演示如何实现 W3C 定义的规范化算法。规范化流程主要处理以下所示类似的问题:

 

  1. 命名空间声明
  2. 空格的处理
  3. XML 内容中特殊字符的编码
  4. XML 属性的排序

 

我们将解决以上除属性排序外的所有问题。您并不需要处理 MXL 属性,因为如第 1 部分中的“WSA 的一个重要限制”部分中所提到的,WSA 并不包括对属性创建的支持。

请注意,使用了两个输入对象来创建 XML。第一个对象 (structure) 是 Element 类的实例,而第二个对象 (content) 是 Object 的实例。这两个对象之间的关系在实例化 SignedInfo 数据成员部分进行了说明。

在进行规范化工作时,您通常要处理一个输入 XML 文档。将获取 XML 文档,并对其进行规范化(或创建其规范形式)。但此处具有两个输入对象,分别包含输入 XML 数据的结构和内容。

因此,本部分创建 XML 数据的规范形式的工作就是创建 WSA 创建的 SOAP 请求的规范形式。

在本部分结束时,我们将通过执行以下步骤测试 CanonicalAuthor 类的性能。

 

  1. WSA 生成的 SOAP 请求
  2. 通过使用 IBM alphaWorks 提供的 XSS4J 规范化 SOAP 请求
  3. 将 SOAP 请求的规范形式与 CanonicalAuthor 的输出比较

 

首先,我们将了解 CanonicalAuthor.authorCanonicalXML() 如何使用两个输入对象创建规范的 XML。




回页首


authorCanonicalXML() 实现

authorCanonicalXML() 方法实现如清单 20 中所示。

清单 20. authorCanonicalXML() 方法实现

                    
public String authorCanonicalXML() {
    StringBuffer buffer = new StringBuffer();
    if (structure != null && content != null) {
        authorXMLFromElement (structure, content, buffer);
        return buffer.toString();
    }
    return null;
}

正如您在清单 20 中看到的,authorCanonicalXML() 方法实例化一个 StringBuffer 对象,此对象将保存 XML 数据的规范形式。

authorCanonicalXML() 方法将随后检查任何输入对象是否为空。如果没有输入对象为空,则将调用名为 authorXMLFromElement() 的私有 Helper 方法,该方法接受三个参数。前两个是输入对象,而第三个是 authorCanonicalXML() 方法刚刚创建的 StringBuffer 对象。

authorXMLFromElement() 方法对输入 XML 数据的规范形式的创建工作进行内部管理。下一部分将对如何进行此管理工作予以说明。




回页首


从 Element 对象创建 XML

您可以在清单 21 中看到 authorXMLFromElement() 方法实现。

清单 21. authorXMLFromElement() 方法实现

                    
private void authorXMLFromElement(
    Element elementStructure, 
    Object elementContent, 
    StringBuffer buffer) 
{
    /**** Step 1 ****/
    Type contentType = elementStructure.contentType;
    if (contentType.value < 8 ) 
    {  
        /**** Step 2****/  
        QName name = elementStructure.name;
        String localNameAndPrefix = name.getPrefix() +":"+ name.getLocalPart();  
        if (!namespaceURIDefined)
        {
            String completeQualifiedNameInStringForm = 
                localNameAndPrefix + 
                " xmlns:"+name.getPrefix()+"=/""+ name.getNamespaceURI() + "/"";    
            buffer.append ("<"+completeQualifiedNameInStringForm+">");
            namespaceURIDefined = true;
        } else
            buffer.append ("<"+localNameAndPrefix+">");            
        
        /**** Step 3****/
        encodeSpecialCharacters (elementContent.toString(), buffer);
        /**** Step 4****/                
        buffer.append("</"+ localNameAndPrefix +">");
    } else
        /**** Step 5****/  
        authorXMLFromComplexType(elementStructure, elementContent, buffer);
    
}//authorXMLFromElement

authorXMLFromElement() 方法的第一个参数是一个 Element 对象,该对象直接包装基元内容——整数或文本字符串——或其他 Element 对象。

对于 Element 包装其他 Element 对象的情况,它实际上是一个复杂类型。

authorXMLFromElement() 方法将以不同方法处理这两种情况(Element 包装基元数据或复杂类型内容)。

因此,在步骤 1 中,authorXMLFromElement() 方法将检查 Element 包装的内容类型——即基元类型数据或复杂类型。为此,它将检查 Element 对象的 contentType 属性。contentType 属性是 Type 对象,该对象在第 1 部分的“实例化简单电子邮件服务的元素”部分中进行了介绍。

Type 类具有名为 value 的字段,该字段说明内容类型是 0 到 8 之间的整数值。0 到 7 之间的值表示基元类型内容。

如果值为 8,则表示复杂类型内容。

因此在步骤 1 中,如果 authorXMLFromElement() 方法发现 value 字段小于 8,它将 Element 对象作为基元内容对待,将使用其基元内容创建此元素。

否则,如果 value 字段为 8,它将调用名为 authorXMLFromComplexType() 的私有 Helper 方法,该方法将创建复杂类型内容。

首先,我们将在接下来的三个部分中了解 authorXMLFromElement() 方法如何从基元内容创建元素。我们将随后讨论 authorXMLFromComplexType() 方法如何工作。




回页首


创建 start 标记的规范形式

请看清单 21 的步骤 2,其中 authorXMLFromElement() 方法将创建元素的 start 标记。为了创建 start 标记,您需要获取 XML 限定名称的三个组件:命名空间前缀、本地名称和命名空间 URI。authorXMLFromElement() 方法创建 start 标记的规范形式的方法如下:

Element 类具有一个 name 属性,其中包装 QName 对象。QName 对象表示元素的完全限定名称。可以参见第 1 部分的“存根类的数据成员”部分,其中介绍并说明了 QName 对象。

QName 对象具有 getLocalPart()getPrefix() 和 getNamespaceURI() 方法,这些方法分别返回限定名称的本地名称、命名空间前缀和命名空间 URI。

因此,在清单 21 的步骤 2 中,authorXMLFromElement() 方法将调用 QName 类的这些方法。authorXMLFromElement() 方法使用这些值来创建 start 标记,如 <tns:getSubjects xmlns:tns=" http://www.everydaywebservices.com/secureemailservice">。

请注意,您使用的前缀表示作为 SOAP 包装的一部分出现的命名空间 URI。您可以参考清单 3,WSA 在其中定义了 SOAP 信封中的命名空间 URI。

由于仅对 SOAP Body 元素的子项(属于 SOAP 消息的一部分)进行规范化,因此在信封中进行的命名空间声明将不会出现在规范形式中。

在此情况下,W3C 的 Exclusive XML Canonicalization Specification V1.0 要求在被规范化的部分中使用的命名空间声明应以规范形式出现。正是由于这个原因,我们才仅在 SOAP Body 的直接子项的 start 标记中声明 xmlns:tns="http://www.everydaywebservices.com/secureemailservice"。

规范形式并不允许在 start 标记中存在额外的空格。只在属性值对之间存在允许空格的 start 标记。

您将使本地名称和前缀继续保存在名为 localNameAndPrefix 的变量中。您稍后将在创建输入元素的 end 标记部分中创建 end 标记时再次使用此变量。




回页首


创建 XML 内容的规范形式

创建 start 标记后,接下来就要创建输入元素的内容。

步骤 1 中的 authorXMLFromElement() 方法确保 contentType 属性的 value 字段为 0 到 7 之间,这意味着输入元素包装基元类型内容。现在我们将根据其内容类型创建元素的内容。

表 1 显示了与 0 到 7 之间的不同值对应的内容类型。

表 1:基元类型内容的不同类型

类型标识符内容类型
0Boolean
1Byte
2Short
3Int
4Long
5Float
6Double
7String

现在,authorXMLFromElement() 方法将调用 content 对象的 toString() 方法,此方法将以 String 对象的形式返回元素内容。您可以将 String 对象包装为表 1 中所示的所有基元内容的所有类型(String 类型除外)的规范 XML 形式。

String 类型内容可以包含 XML 标记中使用的字符,如“小于”和“大于”字符。您需要将此类字符编码为 Exclusive XML Canonicalization Specification(请参见参考资料)的转义序列。

authorXMLFromElement() 方法调用名为 encodeSpecialCharacters() 的方法调用,该方法将特殊字符编码为转义序列。




回页首


创建输入元素的 end 标记

创建了输入元素的内容后,您将创建输入元素的 end 标记。

无论输入元素是否为空,XML 规范要求创建一对 start 和 end 标记。创建 end 标记时,您将需要获取与输入元素的 start 标记匹配的元素本地名称及其命名空间前缀。

清单 21 的步骤 2 中,您将输入元素的本地名称和前缀存储在名为 localNameAndPrefix 的 String对象中。在清单 21 的步骤 4 中,authorXMLFromElement() 使用来自步骤 2 的此 String 对象来为输入元素创建 end 标记。

您已经完成了具有基元类型作为内容的输入 Element 对象的规范 XML 的创建工作。现在我们将了解如何处理复杂类型内容。




回页首


从复杂内容创建 XML

如果输入元素包括复杂类型内容,authorXMLFromElement() 方法将调用 authorXMLFromComplexType() 方法(清单 21 的步骤 5 中),并随方法调用一起调用 structurecontent 和 buffer 参数。

authorXMLFromComplexType() 方法负责从复杂类型内容创建规范 XML。我们已经在清单 22 中显示了 authorXMLFromComplexType() 方法实现。

清单 22. authorXMLFromComplexType() 方法实现

                    
private void authorXMLFromComplexType(
    Element elementStructure, 
    Object elementContent, 
    StringBuffer buffer) 
{
    /***** Step 1 *****/
    QName name = elementStructure.name;
    String localNameAndPrefix = 
        name.getPrefix() +":"+ name.getLocalPart();  
    if (!namespaceURIDefined)
    {
        String completeQualifiedNameInStringForm = 
            localNameAndPrefix + 
                " xmlns:"+name.getPrefix()+
                "=/""+ name.getNamespaceURI() + "/"";
        buffer.append ("<"+completeQualifiedNameInStringForm+">");
        namespaceURIDefined = true;
    } else
        buffer.append ("<"+localNameAndPrefix+">");            
    /***** Step 2 *****/
    ComplexType complexType = 
        (ComplexType)elementStructure.contentType;
    Element[] elementsArray = complexType.elements;
    Object[] contentArray = {elementContent};
    /***** Step 3 *****/        
    if (elementContent instanceof Object[])
        contentArray =  (Object[]) elementContent;
    /***** Step 4 *****/         
    for (int j = 0; j < elementsArray.length; j++)
    {
        buffer.append("/n");        
        authorXMLFromElement(
            elementsArray[j], 
            contentArray[j], 
            buffer);
    }
    /***** Step 5 *****/
    buffer.append("/n");
    buffer.append("</"+ localNameAndPrefix +">");
}//authorXMLFromComplexType

创建简单类型和复杂类型的规范形式的基本区别在于内容参数的处理方法不同。因此,在清单 22 的步骤 1 和步骤 5 中,authorXMLFromComplexType() 方法将分别采用与已经进行了详细讨论的清单 21 中步骤 2 和步骤 4 类似的方式创建输入元素的 start 和 end 标记。

现在让我们讨论清单 22 的步骤 2 到步骤 4,其中 authorXMLFromComplexType() 方法将创建复杂类型内容。

清单 22 的步骤 2 中,authorXMLFromComplexType() 方法会将输入元素的 contentType 属性强制转换为 ComplexType 对象。此 ComplexType 对象保存复杂类型内容的结构。

复杂类型结构包含一系列 Element 对象,这些对象存储在 ComplexType 对象的 elements 属性。因此,在进行了到 ComplexType 对象的强制类型转换后,将立即获取其 elements 属性,此属性是一个 Element 对象数组。

现在您只需要一次从数组中获取一个元素,并调用每个 Element 对象的 authorXMLFromElement() 方法即可。

您很容易猜到,authorXMLFromElement() 和 authorXMLFromComplexType() 方法将彼此递归调用,直到创建了完整 XML 结构的规范形式为止。

请注意清单 22 中的另一个问题。我们使用了 0x10(或“n”)表示新行字符。在普通 XML 创建中,您可以使用 0x10 或 0x13(甚至两者一起使用)来表示新行字符。不过,Canonical XML Specification 仅允许使用 0x10。

您已经完成了 XML 数据的创建工作。在下一部分,我们将测试 CanonicalAuthor 类的性能。




回页首


测试 CanonicalAuthor 的性能

我们已经在本教程的下载部分提供的源代码中包含了一个名为 CanonicalizerTestingMIDlet的 MIDlet。此 MIDlet 执行测试 CanonicalAuthor 的性能所需的两个操作。

 

  1. 它将在 Sun Java Wireless Toolkit 的输出控制台上输出 CanonicalAuthor 的规范形式。
  2. 它将 WSA 创建的 SOAP 请求发送给“localhost:8090”

 

在本教程的下载部分提供的源代码中包含了名为 CanonicalVerifier 的 Java 应用程序。您可以使用以下命令行语句启动 CanonicalVerifierjava -classpath .;x:/xss4j.jar;x:/xalan.jar; CanonicalVerifier

CanonicalVerifier 充当服务器并侦听 localhost:8090 处的传入请求,将在此获取来自 CanonicalizerTestingMIDlet 的请求;它将进行以下工作:

 

  1. 使用 IBM alphaWorks 提供的 XSS4J 对 WSA 创建的 SOAP 请求进行规范化
  2. 它会在输出控制台输出规范形式

 

因此,CanonicalizerTestingMIDlet 和 CanonicalVerifier 的输出控制台上的数据应该彼此完全相同。

源代码下载中包括了两个文件,分别名为 outputOfCanonicalizerTestingMIDlet.txt 和 outputOfCanonicalVerifier.txt,其中包含可立即供您参考的两个控制台的输出。




回页首


构建基于 J2ME 的安全 SOAP 客户机: 第 2 部分:增强 J2ME 的 Web 服务 API (WSA) 中的存根类

实现安全算法

第 7 页,共 12 页


对本教程的评价

帮助我们改进这些内容


部分 7. 在 J2ME 中实现 SHA-1 摘要算法

SHA1 简介

此部分在前面实例化 digestValue 数据成员部分引入的 SHA1DigestCalculator 类中实现 SHA-1 摘要计算算法。

SHA-1 摘要算法由 Internet Society(请参见参考资料)定义。SHA-1 表示安全的散列算法 1 (Secure Hash Algorithm 1)。“散列”指找到数据的定长表示形式。

“安全”表示应该无法进行以下工作:

 

  1. 使用给定摘要值以推算方式计算消息
  2. 使用相同的摘要值以推算方式计算两个消息

 

这两个计算与摘要算法的“安全性”的关系如何?

通常,摘要算法将和签名算法一起使用。您首先将根据摘要计算算法对数据进行摘要操作。这就可以获得数据的定长表示形式,例如,如果使用 SHA-1 摘要算法,则为 20 个字节的长度。

现在将使用加密密钥对摘要值(而不是实际值)进行签名。

从计算角度而言,此技术比对实际数据进行签名开销更少。这是因为消息摘要的长度通常比实际消息更小。

如果黑客希望对您的消息进行编辑以让消息接收者仍然认为此消息是由您发出的,黑客将需要对消息进行编辑,使其摘要值不发生改变。如果黑客可以这样做,则即时对消息进行了编辑,签名将仍然保持不变。

正是由于这个原因,所以务必确保使用“安全”摘要算法,即不应出现可能使用给定摘要值计算消息的情况。

类似地,如果能够使用相同的摘要值计算两个消息,黑客就可以发送经过签名的消息,并稍后声明实际使用了匹配摘要值对其他消息进行了签名。




回页首


实现 SHA-1 的步骤

您可以通过参考参考资料部分中的链接来查找 SHA-1 算法的详细信息。此部分说明如何为 J2ME 实现 SHA-1。

存在各种 SHA-1 实现类型,参考资料部分提到了其中的一部分。不过,我们要演示需要较少内存的实现。尽管这意味着此算法执行时间将更长,但在 J2ME 中节约内存资源比加速算法执行更为重要。

清单 23 显示了 getValue() 方法,此方法属于 SHA1DigestCalculator 类。

清单 23. SHA1DigestCalculator.getValue() 方法

                    
public byte[] getValue(byte inputData[])
{
    /******** STEP 1 ********/ 
    initialize();
    /******** STEP 2 ********/ 
    padMessage( inputData );
    for (int i = 64; i <= inputData.length ; i += 64)
    {
       /******** STEP 3 ********/ 
        int startIndex = 0;
        byte[] blockOf64 = new byte[64];
        for (int j = 0; j < blockOf64.length; j ++)
            blockOf64[j] = inputData [startIndex + j];
        
           process ( blockOf64 );
        startIndex = i;
    }
    
    /******** STEP 4 ********/ 
    process ( lastBlock );
    
    if ( extraBlock != null )
        process ( extraBlock );
     
    //output digest bytes
    byte[] digestBytes = new byte [20];
    getDigestBytes ( digestBytes );
    return digestBytes;
}//getValue()

getValue() 方法仅接收一个参数,即要进行摘要操作的输入数据。

为了便于理解,我们已经在清单 23 中将 SHA-1 摘要计算算法划分为了四个步骤。我们将这些步骤称为“SHA-1 的四个步骤”。

清单 23 中的 SHA1DigestCalcualtor.getValue() 方法将首先调用 initialize() 方法。initialize() 方法实现了 SHA-1 的四个步骤中的第一步。initialize() 方法对一系列变量进行初始化。我们将在初始化摘要计算的值部分描述此初始化过程。

清单 23 的步骤 2 中,getValue() 通过调用 padMessage() 方法对输入数据进行垫长。padMessge() 方法实现 SHA-1 的四个步骤中的第二步。在此步骤中,您将检查要进行摘要操作的数据,并向输入数据垫长一定的位数,以使其长度成为 512 位(64 字节)的倍数。

我们稍后将在对传入数据进行垫长部分描述垫长过程。垫长过程可能产生 1 个或 2 个 64 字节的数据块。我们将这两个数据块称为“最末块”和“额外块”。padMessage() 方法将“最末块”和“额外块”存储在分别名为 lastBlock 和 extraBlock 的类级别数组中。

在步骤 3 中,getValue() 方法以一次获取一个 64 字节数据块的方式处理输入数据。为此,getValue() 方法定义了一个循环来以每次 64 个字节的方式从输入数据进行读取操作。它会将每个 64 字节数据块发送到名为 process() 的方法。

process() 方法根据 SHA-1 算法处理每个数据块,以生成 20 字节的摘要值。我们稍后将在 SHA-1 处理部分详细讨论 process() 方法。

循环将处理输入数据的所有可用 64 字节数据块,直到所剩数据小于 64 字节为止。这意味着循环不会处理输入数据的最末块。

这是因为最末块的垫长形式包含在前面在步骤 2 中准备的 lastBlock 和 extraBlock 数组中。因此,不会处理数据的最末块,而要处理清单 23 的步骤 4 中的垫长形式。

每个 64 字节数据块的处理都会产生 20 个字节,而这将成为下一个 64 字节数据块处理的输入。处理了最末块后,就获得了所需的摘要值了。

现在我们将详细对 initialize()padMessage() 和 process() 方法进行讨论。




回页首


初始化摘要计算的值

清单 24 显示了 initialize() 方法的实现,此方法通过初始化总共九个整数来实现 SHA-1 的四个步骤中的第一步。

清单 24. initialize() 方法实现

                    
public void initialize() {
    dynamicInteger1 = 0x67452301;
    dynamicInteger2 = 0xefcdab89;
    dynamicInteger3 = 0x98badcfe;
    dynamicInteger4 = 0x10325476;
    dynamicInteger5 = 0xc3d2e1f0;
    
    fixedInteger1   = 0x5a827999;
    fixedInteger2   = 0x6ed9eba1;
    fixedInteger3   = 0x8f1bbcdc;
    fixedInteger4   = 0xca62c1d6;
    lastBlock = null;
    extraBlock = null;
 }//initialize()

前五个整数依次命名为 dynamicInteger1 到 dynamicInteger5。这些变量用于保存在摘要计算过程中动态生成的值。SHA-1 规范为这五个动态整数提供了初始值。

这五个动态整数的初始值将在对 process() 方法的第一个调用中使用。process() 方法将在处理了第一个 64 字节数据块后为动态整数生成新值。新的动态值将用作第二个 process() 调用的输入。这将一直持续到使用了所有 64 字节数据的所有数据块为止。

在所有 process() 调用结束时五个整数变量中包含的值将形成所需的摘要值。每个整数包括四个字节,摘要值的长度将为 20 字节。

initialize() 方法还要初始化名为 fixedInteger1 到 fixedInteger4 的四个整数。按照 SHA-1 的定义,这些整数的值固定,并不会在摘要计算过程中变化。

此外,initialize() 方法会将 lastBlock 和 extraBlock 数组初始化为空。padMessage() 方法稍后将使用经过垫长操作的数据填充这些数组。




回页首


对传入数据进行垫长

SHA-1 以 64 字节(512 位)数据块为单位处理传入数据。您将首先把输入数据划分为 64 字节的数据块,然后仅对最末数据块进行垫长,以使其长度为 64 字节。

根据 SHA-1,垫长是将始终进行的重要过程,即使输入数据长度已经为 64 字节的倍数,仍然会进行此操作。

对数据的最末块进行垫长时,将在输入数据末端后紧跟一个硬编码的字节值 128(或 0x80)。

在硬编码的 0x80 字节后,您将放置一系列八字节长的零。长度字节指定实际输入数据中的总位数,而不会对垫长的字节进行计数。

为了适应八字节的长度,您必须考虑两种情况:

情况 1:数据的最末块的长度小于或等于五十五 (<=55) 字节。在这种情况下,您将把硬编码的 0x80 字节紧跟在块数据后,并在其中跟上足够的零,以凑够 56 个字节。这 56 字节后面将跟八个字节,以得到 64 字节的总数。您会将 64 字节垫长块存储在 lastBlock 数组中。

情况 2:数据的最末块的长度大于五十五 (> 55) 字节。在此情况下,您要将硬编码 0x80 字节紧跟在块数据末端后。0x80 字节后面将附加相应数量的零,以形成总共 64 个字节,您要将此块存储在 lastBlock 数组中。

在此情况下,您还将添加一个 64 字节的块,仅将其作为补充长度的字节。额外块的前 56 字节将都是零,其后是八个长度字节。您要将此额外块存储在 extraBlock 数组中。

可以在清单 25 中所示的 padMessage() 方法中看到此垫长过程的实现。

清单 25. 垫长过程的实现

                    
public void padMessage ( byte[] inputMessage) 
{
    int lastBlockLength = (inputMessage.length) % 64;
    /**** Case 1 ****/
    if ( lastBlockLength <= 55 )
    {
        lastBlock = new byte [64];
        populateBlock(
        (inputMessage.length - lastBlockLength),
        inputMessage, 
        lastBlock );
        lastBlock[lastBlockLength] = (byte) 128;
     
        //pad message array with zeros to complete 56 bytes.
        for (int i = lastBlockLength+1 ; i < 56 ; i++ ) 
            lastBlock[i] = (byte) 0;
            setLengthOfInputData(inputMessage.length, lastBlock);
        }
    /**** Case 2: ****/
    else if ( lastBlockLength > 55 && lastBlockLength < 64)
    {
       lastBlock  = new byte [64];
       extraBlock = new byte [64];
       populateBlock(
           (inputMessage.length - lastBlockLength),
           inputMessage, 
           lastBlock );
       lastBlock[lastBlockLength] = (byte) 128;
    
       //pad message array with zeros to complete 56 bytes.
       for (int i = lastBlockLength+1 ; i < 56 ; i++ ) 
           lastBlock[i] = (byte) 0;
            
        extraBlock = new byte [64];
        setLengthOfInputData (inputMessage.length, extraBlock);            
    }
}//padMessage





回页首


SHA-1 处理

请看清单 26 的 process() 方法,此方法负责输入数据的 64 字节块的 SHA-1 处理。

清单 26. process() 方法实现

                    
private void process (byte[] message) 
{
    /**** Step 1 ****/
    int temp1 = dynamicInteger1;
    int temp2 = dynamicInteger2;
    int temp3 = dynamicInteger3;
    int temp4 = dynamicInteger4;
    int temp5 = dynamicInteger5;
    int circularQueueMask = 0x0000000F;
    /**** Step 2 ****/
    for (int i = 0; i <= 79; i++) {
     int index = i & circularQueueMask;
     if (i >= 16) {
         /**** Step 3 ****/            
         int firstInt  = readFromByteArray (((index + 13) & circularQueueMask), message);
         int secondInt = readFromByteArray (((index + 8) & circularQueueMask), message);
         int thirdInt  = readFromByteArray (((index + 2) & circularQueueMask), message ;
         int fourthInt = readFromByteArray (index, message);
    
         /**** Step 4 ****/
         int intValue = (firstInt ^ secondInt ^ thirdInt ^ fourthInt);
         int semiProcessedData = intValue << 1 | intValue >>> 32 - 1;
         /**** Step 5 ****/
         writeToByteArray ( index, semiProcessedData, message);
      }
      /**** Step 6 ****/
      if (i >= 0 && i <= 19)  {
         int semiProcessedData = readFromByteArray(index, message);
         int processedTemp1 = (temp1 << 5 | temp1 >>> 32 - 5);
         int outputOfProcess1 = 
             processedTemp1 + 
             function1(temp2, temp3, temp4) + 
             temp5 + 
             semiProcessedData + 
             fixedInteger1;
         temp5 = temp4;
         temp4 = temp3;
         temp3 = (temp2 << 30 | temp2 >>> 32 - 30); 
         temp2 = temp1;
         temp1 = outputOfProcess1;
        }
      if (i >= 20 && i <= 39) { 
         int semiProcessedData = readFromByteArray(index, message);
         int processedTemp1 = (temp1 << 5 | temp1 >>> 32 - 5);
            
         int outputOfProcess2 = 
             processedTemp1 + 
             function2(temp2, temp3, temp4) + 
             temp5 + 
             semiProcessedData + 
             fixedInteger2;
          
         temp5 = temp4;
         temp4 = temp3;
         temp3 = (temp2 << 30 | temp2 >>> 32 - 30);
         temp2 = temp1;
         temp1 = outputOfProcess2;
     }
     if (i >= 40 && i <= 59)  {  
         int semiProcessedData = readFromByteArray(index, message);
         int processedTemp1 = (temp1 << 5 | temp1 >>> 32 - 5);
                                            
         int outputOfProcess3 = 
             processedTemp1 + 
             function3(temp2, temp3, temp4) + 
             temp5 + 
             semiProcessedData + 
             fixedInteger3; 
           
         temp5 = temp4;
         temp4 = temp3;
         temp3 = (temp2 << 30 | temp2 >>> 32 - 30);
         temp2 = temp1;
         temp1 = outputOfProcess3;
     }
     if (i >= 60 && i <= 79) {
         int semiProcessedData = readFromByteArray(index, message);
         int processedTemp1 = (temp1 << 5 | temp1 >>> 32 - 5);
       
         int outputOfProcess4 = 
             processedTemp1 + 
             function2(temp2, temp3, temp4) + 
             temp5 + 
             semiProcessedData +
             fixedInteger4;
        
         temp5 = temp4;
         temp4 = temp3;
         temp3 = (temp2 << 30 | temp2 >>> 32 - 30);
         temp2 = temp1;
         temp1 = outputOfProcess4;
     }
    }
    /**** Step 7 ****/
    dynamicInteger1 += temp1;
    dynamicInteger2 += temp2;
    dynamicInteger3 += temp3;
    dynamicInteger4 += temp4;
    dynamicInteger5 += temp5;
}
// Helper methods:
private int readFromByteArray(int index, byte[] msg) {
  int newIndex = index * 4;
  byte[] byteBuffer = new byte[4];
  for (int i= 0; i < 4; i++) {
     byteBuffer[i] = msg [newIndex];
     newIndex++; 
  }
  return getIntValue(byteBuffer);
}
private void writeToByteArray (int index, int value, byte[] msg){
  int newIndex = index * 4;
  int mask = 0x000000ff;
  byte[] intBytes = new byte[4];
  intBytes[0] = (byte) (value & mask);        
  intBytes[1] = (byte) ((value >> 8 ) & mask);
  intBytes[2] = (byte) ((value >> 16) & mask);
  intBytes[3] = (byte) ((value >> 24) & mask);
   
  for (int i = intBytes.length - 1 ; i >= 0; i --) {
     msg [newIndex] = intBytes [i];
     newIndex++;
  }
}
    
private int function1 (int val1, int val2, int val3){
  return val1 & val2 | ~val1 & val3;
}
private int function2 (int val1, int val2, int val3){
  return val1 ^ val2 ^ val3;
}
private int function3 (int val1, int val2, int val3){
  return val1 & val2 | val1 & val3 | val2 & val3;
}

process() 方法仅接受一个输入参数,名为 message 的字节数组。message 字节数组保存要处理输入数据。

为了处理输入数据,您需要进行以下七个步骤:

在步骤 1 中,您将定义五个局部变量,这些变量将作为在摘要计算流程期间生成的动态数据的临时占位符。处理结束时,process() 方法将把这些临时占位符的值复制到在初始化摘要计算的值部分中初始化的动态整数中。

SHA-1 算法指定输入数据的 64 字节块将处理 8 次。因此,在步骤 2 中,您将启动一个将重复本身八次的循环。

SHA-1 定义一系列要对输入数据进行执行的流程。每个流程从输入数据接受十六个字节。SHA-1 定义特定序列,将在其中从每个流程的 message 字节数组读取数据。

因此,在步骤 3 中,您将从输入数据以四个整数的形式读取十六个字节。

请注意,在清单 26 中,您已经编写了名为 readFromByteArray() 的方法来一次读取四个字节,并将其放入整数中。这仅是为了方便起见,因为处理整数比处理字节更为容易。

将十六个输入字节作为四个整数值存储后,您将在步骤 4 中处理这四个整数。根据 SHA-1,此过程的输出是单个整数值(四字节)。

现在,在步骤 5 中,您将把步骤 4 的输出整数值复制到输入 message 字节数组中。这意味着您在使用输入 message 字节数组存储半处理数据。这是为了节约内存资源的使用。

请注意,为了将整数复制到字节数组中,我们编写了一个名为 writeToByteArray() 的 Helper 方法。

在步骤 6 中,您将进一步对步骤 4 的输出进行处理。SHA-1 定义了对步骤 4 的输出进行不同的处理,具体取决于在步骤 2 中启动的循环计数器值。我们在清单 26 中将 SHA-1 流程作为 Helper 方法实现了。

在步骤 7 中,您要将 process() 方法的输出复制到在初始化摘要计算的值部分初始化的类级别动态整数变量中。这些动态整数变量将保存此 process() 方法调用的输出,并将其作为下一个 process() 调用的输入。对 process() 方法的最后一个调用将产生所需的摘要值。

这就结束了有关 SHA1DigestCalculator 类的所有方法的讨论。




回页首


测试 SHA1DigetstCalculator 的性能

下载部分包含一个名为 DigestTestingMIDlet 的 MIDlet。此 MIDlet 包含数个硬编码消息作为输入数据。它将使用 SHA1DigestCalculator 类计算输入数据的摘要,并在 Sun Java Wireless Toolkit 的输出控制台上输出摘要值。

本教程的源代码下载还包括一个名为 DigestVerifier 的 Java 应用程序。DigestVerifier 类包含与 DigestTestingMIDlet 相同的硬编码消息。

DigestVerifier 类使用随 Java Development Kit (JDK) V1.5 提供的 JCE 包来计算硬编码消息的摘要值。它将在输出控制台输出摘要值,因此可以对两个输出控制台进行比较。

您可以使用以下命令行语句运行 DigestVerifier

java -classpath . DigestVerifier 




回页首


构建基于 J2ME 的安全 SOAP 客户机: 第 2 部分:增强 J2ME 的 Web 服务 API (WSA) 中的存根类

实现安全算法

第 8 页,共 12 页


对本教程的评价

帮助我们改进这些内容


部分 8. 总结

在本教程的第 2 部分,您对安全电子邮件服务的所有存根类进行了增强。您还了解了每个增强内容的用途。

在增强存根类时,您遇到了四个 Helper 类,即 CanonicalAuthorSHA1DigestCalculatorBase64Encode 和 SignatureCalculator。这些类分别提供了对规范化、摘要计算、Base 64 编码和签名计算的支持。

您需要实现所有四个 Helper 类,从而为 Web 服务构建基于 J2ME 的安全客户机。

在第 2 部分,您已经实现了其中的两个 Helper 类(CanonicalAuthor 和 SHA1DigestCalculator)。

在本教程的第 3 部分和第 4 部分,您将实现 Base64Encode 和 SignatureCalculator 类。在第 3 部分,您还将构建能够对应用程序数据进行签名的 Java Card 应用程序。最后,您将这些内容组合起来形成存根增强器工具,该工具将进行构建安全 Web 服务客户机的大部分编程工作。




回页首

构建基于 J2ME 的安全 SOAP 客户机,第 3 部分: 安全 Web 服务 API 存根类

构建存根增强器工具

第 1 页,共 11 页


对本教程的评价

帮助我们改进这些内容


级别: 中级

Bilal Siddiqui (xml4java@yahoo.co.uk), 自由顾问, WaxSys

2007 年 6 月 25 日

了解如何构建基于 Java™ 2 Micro Edition (J2ME) 的安全 Web 服务客户机。本文是本系列的最后一部分,将对重要的 J2ME 安全算法进行讨论。文中将对前面两个部分开发的内容进行组合,并将提供用于测试安全 Web 服务客户机的机制。另外还将构建一个存根增强器工具,以大幅度地减少构建安全 Web 服务客户机所需的手动编程工作。

开始之前

关于本系列教程

本系列教程演示如何将基于 Java 2 Micro Edition (J2ME) 的无线访问中的安全机制集成到 Web 服务中。我们将在 J2ME MIDlet 中使用以下组件和技术:

  1. J2ME 的 Web 服务 API (WSA)
  2. 加密
  3. XML 数字签名(XML Digital Signature,XMLDS)
  4. Java Card

本系列的第 1 部分,您已经了解了 Web 服务 API (WSA) 存根类如何工作。第 2 部分说明了如何增强 WSA 存根类以及如何将其他技术组件(如加密和 XML 签名)集成到 WSA 存根类中。

第 3 部分将首先实现 Base64 编码和签名计算算法。第 3 部分还将给出一个全面的测试安排,可以用于测试基于 J2ME 的安全 Web 服务客户机。我们最后会将所有概念放入“存根增强器工具”来结束全文。此工具通过包含安全性功能来对 WSA 存根类的功能进行了增强。




回页首


关于本教程

在本系列教程的第 2 部分,我们对 WSA 存根类进行了增强。第 2 部分给出了四个 Helper 类,分别为 CanonicalAuthorSHA1DigestCalculatorBase64Encoder 和 SignatureCalculator。在第 2 部分还实现了其中的两个 Helper 类:CanonicalAuthor 和 SHA1DigestCalculator

在第 3 部分,我们将实现其余两个 Helper 类:Base64Encoder 和 SignatureCalculator。然后,我们将在本部分将所有存根和 Helper 类放入到测试安排中。通过这样,可以方便地对基于 J2ME 的安全 Web 服务客户机进行测试。

本教程最后将开发一个存根增强器工具。该工具用于承担增强存根类和生成 Helper 类所需的大部分手动编程工作。可以使用存根增强器工具来保存构建基于 J2ME 的安全 Web 服务客户机过程中的大部分工作。




回页首


先决条件

  • 阅读本系列教程的第 1 部分第 2 部分
  • 您需要对本系列教程讨论的各种技术组件有基本的了解。具体来说,假定了以下背景:
    • 您应该熟悉 Java 编程,并对 J2ME MIDlet 有基本了解。
    • WSA 使用 Web 服务描述语言(Web Services Definition Language,WSDL)和简单对象访问协议(Simple Object Access Protocol,SOAP)。因此,您需要知道 WSDL 接口如何映射到 SOAP 方法调用。
  • 具有一定的 XML 签名方法的背景也会有所帮助。

请参考参考资料部分,其中提供了几篇有关这些主题的非常优秀的 developerWorks 文章。




回页首


我是否应学习本教程?

本系列教程的主要目的是为了帮助您开发对 Web 服务的无线访问。主要的重点是安全性,但也可以使用此处提出的 WSA 概念来为您的 Web 服务开发任意类型的无线客户机。

本教程是本系列教程的第 3 部分,将说明如何在内存受限的无线设备中实现 Base64 编码算法。因此,本教程还可能帮助您在无线设备中实现类似的算法。

本部分还将介绍如何构建测试机制,可以在尝试通过无线方式访问 Web 服务时使用此机制。您可以使用本教程的测试机制,也可以为 Web 服务开发类似的测试机制。

本教程最后讨论的存根增强器工具演示了如何构建用于进行 WSA 存根增强的自动解决方案。可以使用此存根增强器工具来减少构建基于 J2ME 的安全 Web 服务客户机的时间。




回页首


教程主题

第 3 部分组织为以下七个部分:

  1. 教程介绍
  2. 演示如何采用 J2ME 实现 Base64 编码的算法
  3. 说明如何与 J2ME MIDlet 的 Java Card 应用程序通信
  4. 演示如何安装 Java Card 应用程序
  5. 演示如何构建能够计算加密签名值的 Java Card 应用程序
  6. 讨论如何构建存根增强器工具,以执行增强 WSA 存根类所需的大部分编程工作
  7. 总结




回页首


系统要求





回页首


构建基于 J2ME 的安全 SOAP 客户机,第 3 部分: 安全 Web 服务 API 存根类

构建存根增强器工具

第 2 页,共 11 页


对本教程的评价

帮助我们改进这些内容


二进制数据的 Base64 编码

Base64 编码

本系列教程的第 1 部分介绍了简单电子邮件服务和扩展的电子邮件服务,并说明了 J2ME 客户机如何使用 WSA 类来向远程 Web 服务发送 SOAP 消息。第 1 部分还解释了 WSA 类的工作原理。

第 2 部分说明了如何增强 WSA 存根类来为 Web 服务构建安全的 J2ME 客户机。在增强存根类时,我们讨论了四个 Helper J2ME 类:CanonicalAuthorSHA1DigestCalculatorBase64Encoder 和 SignatureCalculator。我们已经在第 2 部分的创建 XML 数据的规范形式在 J2ME 中实现 SHA-1 摘要算法实现了其中的两个 Helper 类。

我们在第 2 部分的计算签名值中对 Base64Encoder Helper 类做了介绍。在第 3 部分中,我将首先实现 Base64Encoder 类。

Base64Encoder 类有一个名为 getBase64EncodedValue() 的方法,该方法接受以字节数组的形式表示的二进制数据,并返回包装二进制值的 Base64 编码形式的 String 对象。

Base64 编码算法由因特网工程工作小组 (IETF) Base 64 Encoding 规范(请参见参考资料)定义。

您可以通过 Internet 找到很多 Base64 编码算法的实现。请参见参考资料部分,其中提供了指向开源 Base64 编码实现的链接。

本部分中所示的 Base64 编码实现经过了优化,可以减少计算 Base64 编码值时的内存 (RAM) 使用量。这在 J2ME 应用程序中将很有帮助,在此类应用程序中内存资源非常有限。

为了表示二进制数据,Base64 使用了美国信息交换标准码(American Standard Code for Information Interchange,ASCII)字符集中的 64 个字符来表示二进制数据。Base64 编码中的 64 个字符是从 A 到 Z(26 个字符)、从 a 到 z(26 个字符)、从 0 到 9(10 个字符)、+ 与 /(2 个字符)。

Base64 编码在 XML 应用程序中的优势在于,其中的任何字符在 XML 格式中都没有任何意义。因此,如果其中任何字符作为 XML 元素的一部分出现,都会将其作为简单的 XML 元素文本内容对待。

不过,这种编码技术也有一个小缺点。您要进行限制,用一个字节表示 64 个字符。这意味着您将仅仅使用字节的 8 位中的 6 位。当在网络上传递 Base64 编码数据时,这会带来 25% 的带宽浪费。

这意味着,当对包含 24 个字节的字节数组进行 Base64 编码时,其 Base64 编码值将多消耗 25% 的字节,其大小将为 30 个字节。正是这个原因,仅在无法直接发送无法编码的二进制数据时才使用 Base64 编码。




回页首


简单 Base64 编码的两个简单步骤

首先,我们将看看非常简单的 Base64 编码情况。假定以下是要转换为 Base64 编码形式的三字节数据: 0111 1001 1001 1100 0110 1110.

可以执行两个非常简单的步骤来对这三个字节的数据进行 Base64 编码。

步骤 1:以三个输入字节为基础创建四个字节

Base64 编码过程首先是在每个输入数据字节中插入两位,以形成三个输入字节的四字节形式: 00011110 00011001 00110001 00101110.

请注意,我们将实际的输入数据以粗体显示。如果仅考虑粗体数据位(忽略其他为零的数据位),可以看到完成步骤 1 之后,数据位与原始数据一样。

Base64 编码过程的步骤 1 在名为 getFourOutOfThreeBytes() 的方法中实现,如清单 1 中所示。
清单 1. getFourOutOfThreeBytes() 方法

                    
public byte[] getFourOutOfThreeBytes (byte[] inputData, short inputIndex) {
    byte[] fourBytes = new byte[4];
    byte mask = (byte) 0xFC;
    byte temp1 = (byte) (inputData [inputIndex] & mask);
    //Creating first of the four output bytes.
    temp1  = (byte) ( temp1 >>> 2 );
    mask   = (byte) 0x3F;
    temp1  = (byte) ( temp1 & mask );
    fourBytes[0] = temp1;
 
    //Creating second of the four output bytes.
    temp1  = 0;
    mask   = (byte) 0x3;
    temp1  = (byte) (inputData [inputIndex] & mask );
    temp1  = (byte) ( temp1 << 4 );
    byte temp2 = (byte) (inputData [inputIndex + 1] >>> 4 );
    mask   = 0xF;
    temp2  = (byte) (temp2 & mask);
    fourBytes[1] = (byte) (temp1 | temp2);
    //Creating third of the four output bytes.
    temp1  = 0;
    temp2  = 0;
    mask   = (byte) 0x3C;
    temp1  = (byte) (inputData [inputIndex + 1] << 2 );
    temp1  = (byte) ( temp1 & mask);
    mask   = (byte) 0x3;
    temp2  = (byte) (inputData [inputIndex + 2] >>> 6 );
    temp2  = (byte) ( temp2 & mask );
    fourBytes[2] = (byte) ( temp1 | temp2 );
    //Creating fourth of the four output bytes.
    temp1  = 0;
    temp2  = 0;
    mask   = (byte) 0x3F;
    temp1  = (byte) (inputData [inputIndex + 2] & mask );
    fourBytes[3] = temp1;
    
    return fourBytes;
}

以下说明了 getFourOutOfThreeBytes() 方法如何以三个字节为基础生成所需的四个字节:

getFourOutOfThreeBytes() 方法接受两个参数。第一个参数是名为 inputData 的字节数组,其中包含getFourOutOfThreeBytes() 方法将用于生成所需的四个字节的三个输入字节。第二个参数是一个 inputData 字节数组的索引值,指向三个输入字节的起始位置。

getFourOutOfThreeBytes() 方法首先实例化名为 fourBytes 的四字节数组,用于存储将要创建的四个字节。随后,该方法从三字节输入数据中提取六位,然后将这六位放在输出字节的最右端。然后会用零填充输出字节剩下的最左端两位。根据第一组六位数据创建了第一个输出字节后,getFourOutOfThreeBytes() 方法将使用输入字节中的下一个六位创建第二个输出字节。通过这样每次取六位的方式,将总共创建四个输出字节。

步骤 2:四字节的 Base64 编码

我们已经根据三个字节获得了对应的四字节。现在将修改每个字节,以使其在 ASCII 码的 A 到 Z、a 到 z、0 到 9、+ 以及 / 的范围内。四个字节中的每个字节的两个最高位均为零,因此每个字节的值都不会超过 63(即此值在十进制 0 到 63 的范围内,或从 0x00 到 0x3F 之间)。

另一方面,Base64 编码过程的结果是从 A 到 Z(十进制 65 到 91 或 0x41 到 0x5B)、从 a 到 z(十进制 97 到 122 或 0x61 到 0x7A)、0-9(十进制 48 到 57 或 0x30 到 0x39)、+(十进制 43 或 0x2B)和 /(十进制 47 或 0x2F)。

这意味着需要根据表 1 对步骤 1 的结果进行转换:
表 1. Base64 编码的输出要求

步骤 1 的结果Base64 编码值所需操作
十进制十六进制十进制十六进制
0 到 250x00 到 0x1965 到 910x41 到 0x5B在步骤 1 的输出上加上十进制数 65
26 到 510x1A 到 0x3397 到 1220x61 到 0x7A在步骤 1 的输出上加上十进制数 71
52 到 610x34 到 0x3D48 到 570x30 到 0x39从步骤 1 的输出上减去十进制数 4
620x3E430x2B从步骤 1 的输出上减去十进制数 19
630x3F470x2F从步骤 1 的输出上减去十进制数 16

清单 2 显示了 getEncodedFormOfFourBytes() 方法中步骤 2 的实现,该方法接受步骤 1 的四字节输出作为参数,并在根据表 1 对全部四个字节进行转换之后返回另一个字节数组:
清单 2. getEncodedFormOfFourBytes() 实现

                    
private byte[] getEncodedFormOfFourBytes(byte[] fourBytes) {
    
    byte[] encodedBytes = new byte[4];
    for (int i = 0; i < encodedBytes.length; i++)
        encodedBytes [i] = getEncodedFormOfOneByte (fourBytes[i]);
        return encodedBytes;
}
private byte getEncodedFormOfOneByte (byte value) {
    if (value >= 0 && value <= 25 )
        return (byte) (value + 65);
    if (value >= 26 && value <= 51 )
        return (byte) (value + 71);
    if (value >= 52 && value <= 61 )
        return (byte) (value - 4);
    if (value == 62)
        return (byte) 43;
    if (value == 63)
        return (byte) 47;
    
    return 0;    
}

请看清单 3 中所示的 getBase64EncodedValue() 方法,该方法将依次调用 getFourOutOfThreeBytes() 和 getEncodedFormOfFourBytes() 方法,以获得您提供的三字节数据的 Base64 编码形式。
清单 3. getBase64EncodedValue() 方法

                    
public String getBase64EncodedValue(byte inputData[])
{
    int length = inputData.length;
    if(length <= 0)
        return "";
    //Create an output byte array to hold Base64-encoded value.
    //Length of the output byte array is 25% more than the input bytes.
    int outputDataLength = 0;
    if (length % 3 == 0)
        outputDataLength = (length / 3) * 4;
    else
        outputDataLength = (length / 3) * 4 + 4;
    outputData[] = new byte[ outputDataLength ];
   
    //Declare index variables for input and output.
    int inputIndex = 0;
    int outputIndex = 0;
    
    //Encode inputData bytes, taking three bytes at a time.
    for (int i = length; i >= 3; i -= 3)
    {
        byte[] fourEncodedBytes = 
            getEncodedFormOfFourBytes (
                getFourOutOfThreeBytes(
                    inputData, (short)inputIndex));
        
        //Set encoded value in the output array.
        outputData [outputIndex ++] = fourEncodedBytes [0];
        outputData [outputIndex ++] = fourEncodedBytes [1];
        outputData [outputIndex ++] = fourEncodedBytes [2];
        outputData [outputIndex ++] = fourEncodedBytes [3];
        inputIndex += 3;
    }
    
   return new String (outputData);
}// getBase64EncodedValue

请注意,我们直接在清单 3 中将 getFourOutOfThreeBytes() 和 getEnhancedFormOfFourBytes() 方法放入循环中,以处理任何长度为 3 的倍数字节数据。对于输入二进制数据的大小不是 3 的倍数字节的情况,需要进行一些额外的处理,我们将在接下来的部分对此进行说明。




回页首


处理任意长度的数据

如果您的输入数据的长度为四字节,首先按照上面部分中所示的两步骤算法处理前三个字节。然后需要处理剩下的一个字节。

类似地,如果输入的是五个字节的二进制数据,则需要对剩下的两个字节进行特殊处理。如果能够实现处理多出一个和两个字节的这两种情况,就可以实现任意长度数据的 Base64 编码的算法。

例如,如果您的数据长度为 71 位,可以根据清单 3 对前面的 69(3 的倍数)字节进行 Base64 编码,而对最后两个字节进行特殊处理。

以下说明了如何处理多出的一个字节和两个字节的 Base64 编码。




回页首


多出一个字节的特殊处理

如果多出一个字节需要处理,可以直接以这个字节为基础创建四个字节。为此,可以执行以下步骤:

  1. 假定多出的要处理的那个字节为 10110101。将该字节的最左边(最高位)的六位 (101101) 移动到四个输出字节的第一个字节中。四个输出字节中的第 1 个字节将为以下所示:00101101(0x2D 或十进制 45)。 
  2. 将最右端(最低位)的两位 (01) 移动到四个输出字节中的第二个字节。最右端的位在第二个字节中的位置非常重要。要将该字节的最右两个字位放在该输出字节的第 5 和第 6 位。四个输出字节中的第 2 个字节将如以下所示:00100000(0x20 和十进制数 32)。
  3. 表 1 中的转换规则应用于步骤 1 和步骤 2 得到的两个输出。第一个输出字节将变成 0x74(十进制数 116),而第二个输出字节将变成 0x67(十进制数 103)。 
  4. 将四个输出字节中的第 3 和第 4 字节填入 = 字符(ASCII 中的 0x3D)。请注意,= 字符并不会出现在正常 Base64 编码过程的任何位置。它仅出现在对多余字节进行特殊处理期间的最后。

这四个步骤在清单 4 中实现为 handleOneExtraByte() 方法:
清单 4. handleOneExtraByte() 方法

                    
//Declare the byte array to hold output bytes.
byte[] outputData;
public void handleOneExtraByte ( 
    byte[] inputData, 
    int inputIndex, 
    int outputIndex) {
    
    //****Step 1***//
    byte mask = (byte) 0xFC;
    byte temp1 = (byte) (inputData [inputIndex] & mask );
    temp1 = (byte) (temp1 >>> 2);
    mask = (byte) 0x3F;
    temp1 = (byte) (temp1 & mask);
    //****Step 2***//
    mask = (byte) 0x3;
    byte temp2 = (byte) (inputData [inputIndex] & mask);
    //****Step 3***//
    outputData [outputIndex ++] = getEncodedFormOfOneByte (temp1);
    outputData [outputIndex ++] = getEncodedFormOfOneByte (temp2);
    //****Step 4***//
    outputData [outputIndex ++] = '=';
    outputData [outputIndex ++] = '=';                    
}//handleOneExtraByte





回页首


多出两个字节的特殊处理

处理两个多余字节比处理多于一个字节更为复杂一些。

假定两个多出来的字节是 10110110 和 10111101。可以通过使用以下步骤以这两个多余字节为基础创建四个输出字节:

  1. 将第一个多余字节的最左六位 (101101) 复制到第一个输出字节的最右端,用零填充其剩下的两位。四个输出字节中的第 1 个字节将为以下所示:00101101(0x2D 或 十进制数 45)。
  2. 将第一个多余字节的最右两位 (10) 及第二个多余字节的最左四位 (1011) 复制到第二个输出字节的最右位置。四个输出字节中的第 2 个字节将为以下所示:00101011(0x2B 或十进制数 43)。
  3. 将第二个多余字节的其余四位 (1101) 复制到四个输出字节中的第三个字节中。四个输出字节中的第 3 个字节将为以下所示:00110100(0x34 或十进制数 52)。
  4. 对三个输出字节应用表 1 中的转换规则。这三个字节将分别转换为 0x74、0x72 和 0x30。
  5. 在最后一个(第 4 个)输出字节中填充 = 字符。

我们已经在清单 5 所示的 handleTwoExtraBytes() 方法中实现了这五个步骤:
清单 5. handleTwoExtraBytes() 方法

                    
//Declare the byte array to hold output bytes.
byte[] outputData;
 
public void handleTwoExtraBytes ( 
    byte[] inputData, 
    int inputIndex, 
    int outputIndex) {
    /***Step 1***/
    byte mask = (byte) 0xFC;
    byte temp1 = (byte) (inputData [inputIndex] & mask);
    temp1 = (byte) (temp1 >>> 2);
    mask = (byte) 0x3F;
    temp1 = (byte) (temp1 & mask);
    /***Step 2***/
    mask = (byte) 0x3;
    byte temp2 = (byte) (inputData [inputIndex] & mask);
    temp2 = (byte) (temp2 << 4);
    byte temp3 = (byte) (inputData [inputIndex + 1] >>> 4);
    mask = 0xF;
    temp3 = (byte) (temp3 & mask);
    temp2 = (byte) (temp2 | temp3);
    /***Step 3***/
    mask = (byte) 0x3C;
    temp3 = (byte) (inputData [inputIndex + 1] << 2);
    temp3 = (byte) (temp3 & mask);
    /***Step 4***/
    outputData [outputIndex ++] = getEncodedFormOfOneByte (temp1);
    outputData [outputIndex ++] = getEncodedFormOfOneByte (temp2);
    outputData [outputIndex ++] = getEncodedFormOfOneByte (temp3);
    /***Step 5***/
    outputData [outputIndex ++] = '=';
}//handleTwoExtraBytes





回页首


测试 getBase64EncodedValue() 方法

现在请看清单 6,其中显示了前面在清单 3 中看到的 getBase64EncodedValue() 方法的完整形式:
清单 6. getBase64EncodedValue() 方法的完整形式

                    
//Declare the byte array to hold output bytes.
byte[] outputData;
public String getBase64EncodedValue(byte inputData[])
{
    int length = inputData.length;
    if(length <= 0)
        return "";
    //Create an output byte array to hold Base64-encoded value.
    //Length of the output byte array is 25% more than the input bytes.
    int outputDataLength = 0;
    if (length % 3 == 0)
        outputDataLength = (length / 3) * 4;
    else
        outputDataLength = (length / 3) * 4 + 4;
    outputData[] = new byte[ outputDataLength ];
   
    //Declare index variables for input and output.
    int inputIndex = 0;
    int outputIndex = 0;
    
    //Encode inputData bytes, taking three bytes at a time.
    for (int i = length; i >= 3; i -= 3)
    {
        byte[] fourEncodedBytes = 
            getEncodedFormOfFourBytes (
                getFourOutOfThreeBytes(
                    inputData, (short)inputIndex));
        
        //Set encoded value in the output array.
        outputData [outputIndex ++] = fourEncodedBytes [0];
        outputData [outputIndex ++] = fourEncodedBytes [1];
        outputData [outputIndex ++] = fourEncodedBytes [2];
        outputData [outputIndex ++] = fourEncodedBytes [3];
        inputIndex += 3;
    }
    
    //Handle extra bytes.  
    if((length - inputIndex) == 1)   
        handleOneExtraByte (inputData, inputIndex, outputIndex);
    else if((length - inputIndex) == 2)  
        handleTwoExtraBytes (inputData, inputIndex, outputIndex);
    return new String (outputData);
}//getBase64EncodedValue

请注意,清单 6 中的完整 getBase64EncodedValue() 方法使用了前面几个部分中开发的所有四个方法——getFourOutOfThreeBytes()getEncodedFormOfFourBytes()handleOneExtraBytes() 和 handleTwoExtraBytes()getBase64EncodedValue() 方法的实现已经完成了,接下来要对此方法进行测试,以验证是否能生成完全符合 Base64 编码形式的二进制数据。

在本教程的源代码下载中的 Base64Tester 文件夹下提供了名为 Base64TestingMIDlet 的 J2ME 应用程序(请参见下载部分)。

Base64TestingMIDlet 包含了多个硬编码字节数组,将其作为输入二进制数据。它将此字节数组传递给Base64Encoder.getBase64EncodedValue() 方法,此方法将返回输入二进制数据的 Base64 编码形式。该 MIDlet 会在 Sun Java Wireless Toolkit 的输出控制台打印原始二进制数据及其编码形式。

源代码下载中还提供了一个名为 Base64Tester 的 Java 应用程序,可以将其用于验证 Base64Encoder类的输出。可以使用以下命令行语句将 Base64Tester 作为普通 Java 应用程序运行:
java -classpath .;.;%JWSDP_HOME%/jwsdp-shared/lib/xmlsec.jar; Base64Tester

通过使用随 Java Web Services Developer Pack (Java WSDP) 提供的 Base64 编码实现(请参见参考资料),Base64Tester 对在 Base64TestingMIDlet 中进行编码的相同二进制数据进行编码。Base64Tester 在输出控制台打印 Base64 编码数据。

可以将您的 Base64TestingMIDlet 的 Base64 编码输出与 Base64Tester 的输出作一下比较。




回页首


将 CanonicalAuthor、DigestCalculator 和 Base64Encoder 集成到安全电子邮件服务中

请回顾一下在第 2 部分的增强 SignedInfo 类中提到的四个 Helper 类——CanonicalAuthorSHA1DigestCalculatorBase64Encoder 和 SignatureCalculator

我们在上面的部分中对 Base64Encoder 类进行了测试。我们在第 2 部分的测试 CanonicalAuthor 的性能中对规范化类 (CanonicalAuthor) 进行了测试。另外,我们还在第 2 部分的测试 SHA1DigetstCalculator 的性能中对摘要计算类 (SHA1DigestCalculator) 进行了测试。

唯一还需要构建的 J2ME 类是 SignatureCalculator,我们将在稍后接下来的部分中对其进行实现。不过,在此阶段,可以将已经进行了开发和测试的类(SHA1DigestCalculatorCanonicalAuthor 和 Base64Encoder)集成到 MIDlet 中(即SecureEverydayMIDlet),以用于访问安全电子邮件服务。

目前已经开发的类可以创建 SOAP 请求的规范形式、计算其摘要值并将摘要编码为 Base64 编码形式。唯一缺少的功能就是签名计算了。因此,您可以执行以下操作来对经过组合的可用功能进行测试:

  1. 使用 SHA1DigestCalculatorCanonicalAuthor 和 Base64Encoder 类的经过测试的形式编译和构建 SecureEverydayMIDletSecureEverydayMIDlet 的源代码和经过编译的形式均与这三个类一起置于源代码下载中的 Base64EncodedDigestTester 文件夹中(请参见下载部分)。
  2. 构建一个服务器端 Java 应用程序,以用于侦听 SecureEverydayMIDlet 发出的 SOAP 消息、分析请求以提取摘要值并对摘要值进行验证。源代码下载中包含一个名为 Based64EncodedDigestVerifier 的 Java 应用程序,可用于执行这些任务。

您可以使用以下命令行语句运行 Based64EncodedDigestVerifier
java -classpath .;%JWSDP_HOME%/jwsdp-shared/lib/xmlsec.jar;%JWSDP_HOME%/xmldsig/lib/xmldsig.jar;x:/xalan.jar; Based64EncodedDigestVerifier

运行了 Based64EncodedDigestVerifier 后,请运行 SecureEverydayMIDlet,从而向 Based64EncodedDigestVerifier 发送一个 SOAP 请求。接收到 SOAP 请求后,Based64EncodedDigestVerifier 将分析请求、验证摘要值并在输出控制台上显示验证结果。

在接下来的部分,我们将在 SignatureCalculator 类中构建签名创作支持。




回页首


构建基于 J2ME 的安全 SOAP 客户机,第 3 部分: 安全 Web 服务 API 存根类

构建存根增强器工具

第 3 页,共 11 页


对本教程的评价

帮助我们改进这些内容


构建签名创作支持

与 Java Card 应用程序通信

接下来说明如何将签名创作支持构建到 SecureEverydayMIDlet 中。

第 1 部分的使用 Java Card 技术中介绍了一个包含用户加密密钥的 Java Card 应用程序。该 Java Card 应用程序使用密钥来为用户数据计算签名值。

在第 2 部分 计算签名值中(第 2 部分清单 15 的第 3 步),我们遇到了 SignatureCalculator 的类。SignatureCalculator 类包装与 Java Card 应用程序的所有通信。SecureEverydayMIDlet 将使用 SignatureCalculator 类与 Java Card 应用程序通信,以获取签名值。

图 1 说明了 SignatureCalculator 类如何帮助 SecureEverydayMIDlet 与 Java Card 应用程序通信。
图 1. J2ME MIDlet 与 Java Card 应用程序间的通信组件 

XML error: The image is not displayed because the width is greater than the maximum of 500 pixels. Please decrease the image width.

图 1 显示了 J2ME 设备和 Java Card。请注意,Java Card 既可以实际位于 J2ME 设备中,也可以位于其外。例如,J2ME 手机中的用户身份识别模块(Subscriber Identification Module,SIM)就可以是 Java Card。在此情况下,Java Card 将实际位于 J2ME 设备内。信用卡也可以是 Java Card。在此情况下,Java Card 并不实际位于 J2ME 设备内。

无论 Java Card 位于 J2ME 设备内部还是外部,都可以通过多种物理方法与 Java Card 进行通信。通信的物理层不在本教程的讨论范围之内。(有关与 Java Card 应用程序的物理通信的信息,请参见参考资料中提供的链接。)

本教程的重点是说明 J2ME MIDlet(例如,图 1 中的 SecureEverydayMIDlet)可用于与任何类型的 Java Card 进行通信的应用层。J2ME 包含一个 API,称为安全性与信任服务 API(Security and Trust Services API,SATSA),可用于将 Java Card 应用程序集成到 J2ME MIDlet 中。

图 1 显示了 J2ME 设备中的 SignatureCalculator 类使用 SATSA 与 Java Card 应用程序通信。

所有 Java Card 应用程序都是 Java applet,可以像开发普通 applet 一样进行开发。可以看到,图 1 的 Java Card 包含一个名为 JavaCardSignatureCalculator 的 applet。我们将在下面的构建 JavaCardSignatureCalculator applet 部分中开发 JavaCardSignatureCalculator。现在我们将简单讨论一下使用 Java Card 技术来保护无线应用程序安全的目的。




回页首


使用 Java Card 技术的优势

Java Card 是一种特殊的智能卡(由 ISO 7816 定义;请参见参考资料),可以在其上安装和执行 Java applet。使用 Java applet 的主要优势在于,不同的服务提供企业可以在用户的 Java Card 安装各自的应用程序。例如,银行可以在其账户持有人的 Java Card 中安装一些移动支付应用程序。

类似的,我们将在“日常 Web 服务”用户的 Java Card 上安装 JavaCardSignatureCalculator applet。

这意味着与 Java Card 关联的有三种类型的实体:

  • Java Card 的所有者(例如,您的“日常 Web 服务”的用户)。
  • 服务提供者(例如,您将通过在“日常 Web 服务”用户的 Java Card 中安装 JavaCardSignatureCalculator applet 而成为服务提供者)。
  • Java Card 的发行者(即向用户发行 Java Card 的公司)。

请注意,Java Card 的发行者和服务提供者可以是(也可以不是)相同的企业。这就是使用 Java Card 的主要优势。服务提供者可以开发自己的 Java Card 应用程序并在其他公司发行的 Java Card 上安装其应用程序。(请参见参考资料部分,其中提供了指向 Java Card 发行者和服务提供者企业的链接。)

这一部分剩下的部分将说明 SignatureCalculator 类如何包装 J2ME MIDlet 和 Java Card applet 之间的所有通信。以下两部分将说明如何构建 JavaCardSignatureCalculator applet。




回页首


SignatureCalculator 类

清单 7 显示了 SignatureCalculator 类的构造函数。
清单 7. SignatureCalculator 构造函数

                    
public SignatureCalculator (
    GetSubjectsSignatureSignedInfo signedInfo, 
    String username, 
    String password)
{
    try {
        //**** Step 1 ****//
        String URL2SignatureApplet = 
                "jcrmi:0;AID=a0.0.0.0.62.3.1.c.8.2";
        //**** Step 2 ****//
        JavaCardRMIConnection rmiConnection = 
            (JavaCardRMIConnection)Connector.open(URL2SignatureApplet);
        //**** Step 3 ****//
        Remote remote = 
            rmiConnection.getInitialReference();
           
        //**** Step 4 ****//
        SignatureMethod signer = 
               (SignatureMethod) remote;
        //**** Step 5 ****//
        String base64EncodedDigest = 
            signedInfo.getReference().getDigestValue();
       
        //**** Step 6 ****
        signatureValue = 
            signer.sign( 
                username.getBytes(), 
                password.getBytes(), 
                base64EncodedDigest.getBytes());
    } 
    catch (java.io.IOException ie){ ie.printStackTrace(); }
    catch (javacard.framework.UserException ue){ ue.printStackTrace();}
 }

SignatureCalculator 构造函数接受三个参数。第一个参数是在第 2 部分的实例化 GetSubjectsSignatureSignedInfo 中进行了说明的 GetSubjectsSignatureSignedInfo 类的实例。此对象包装 SignedInfo 元素的内容(第 1 部分的清单 6),该元素中包含您的 XML 数据的规范化形式的摘要值。SignatureCalculator 类将使用此摘要值来计算加密签名。

其他两个参数为用户名和密码,SignatureCalculator 类需要使用这两个参数来访问包含签名计算所需的加密密钥的 Java Card applet。

SignatureCalculator 构造函数遵循六个步骤来使用 SATSA(如清单 7 中所示)。

接下来我们将对这六个步骤进行说明。




回页首


连接到 Java Card applet

使用 SATSA 的六个步骤如清单 7 的 SignatureCalculator 构造函数中所示。在开始几个步骤中,将连接到远程 Signer 对象,如下所述:

步骤 1

指定指向 JavaCardSignatureCalculator applet 的 URL。清单 7 的步骤 1 中的 URL 字符串 (jcrmi:0;AID=a0.0.0.0.62.3.1.c.8.2) 包含三个部分。

URL 字符串中的第一部分 (jcrmi:) 指定 URL 的协议。SATSA 使用 jcrmi 指定与 Java Card 应用程序通信的协议是远程方法调用(Remote Method Invocation,RMI)。

请注意,RMI 不是 SATSA 唯一可用于与 Java Card 应用程序通信的方法。不过,本教程将仅讨论使用 RMI 与 Java Card 应用程序进行通信。

Java Card 通常与 Java Card 读取设备一起使用(请参见参考资料)。Java Card 读取设备可能具有任意数量的物理插槽。每个物理插槽都能插入一张 Java Card。URL 字符串的第二部分 (0) 指定在其中插入 Java Card 的物理插槽。

Java Card 同时包含一系列 applet。Java Card 内的每个 applet 都有称为应用程序标识符(Application Identifier,AID)的唯一标识符。因此,Java Card 的 URL 需要标识其指向的 applet。第三部分 (AID=a0.0.0.0.62.3.1.c.8.2) 指定 applet 的 AID。

步骤 2

清单 7 的第二步中,将使用步骤 1 中的 URL 来连接到 JavaCardSignatureCalculator applet。为此,将需要以下两个类:javax.microedition.io.Connector 和 javax.microedition.jcrmi.JavaCardRMIConnectionConnector 类属于 J2ME 的输入/输出 (IO) 框架,而 JavaCardRMIConnection 类则属于 SATSA。

将调用静态 Connector.open() 方法,并同时向方法调用传递步骤 1 中所得的 URL。Connector.open() 方法与 JavaCardSignatureCalculator applet 进行通信,并返回 JavaCardRMIConnection 类的实例。

请注意,Connector.open() 方法是智能方法。它将根据 URL 中指定的协议返回对象。例如,如果向 Connector.open() 方法传递的是超文本传输协议(HyperText Transfer Protocol,HTTP)URL,它将返回另一个类 HttpConnection 的实例,J2ME 应用程序可使用此类进行 HTTP 通信。




回页首


使用远程 Java Card 对象

现在我们已经获得了 JavaCardRMIConnection 对象。JavaCardRMIConnection 类有一个名为getInitialReference() 的方法,该方法并不接受任何参数,将返回公开 java.rmi.Remote 接口的对象。Remote 接口属于 RMI 框架,用于表示驻留在 J2ME 应用程序之外的远程 Java 对象。

您可以在清单 7 的步骤 3 中看到对 getInitialReference() 方法的调用。我们稍后将对清单 7 的步骤 3 和后续步骤进行讨论,但首先我们将对三个问题进行说明:

  • Connector 类如何管理与 JavaCardSignatureCalculator applet 的通信?
  • 如何将远程 Java Card 对象表示为本地 J2ME 类?
  • 远程 Java Card 对象的本地表示形式如何在 J2ME 中工作?

接下来我们将对这三个问题进行详细的说明。




回页首


Connector.open() 如何工作?

Java Card 规范定义客户机应用程序(如 SecureEverydayMIDlet)与 Java Card applet(如 JavaCardSignatureCalculator)之间的通信数据格式。数据格式定义为数据单元的形式,称为应用程序协议数据单元(Application Protocol Data Unit,APDU)。

APDU 根据 Java Card 规范定义的格式包装二进制数据。有不同类型的 APDU。例如,名为 SELECT 的 APDU 用于选择驻留于 Java Card 内的特定 applet。类似地,名为 INVOKE 的 APDU 用于调用之前由SELECT APDU 选择的应用程序。

Connector.open() 方法会创建 SELECT APDU,将此 APDU 发送给 JavaCardSignatureCalculator applet,获取响应 APDU 并对其进行处理,以提取对驻留在 JavaCardSignatureCalculator applet 中的远程 Java Card 对象的引用。

提取了远程 Java Card 对象的引用后,它会将引用包装在名为 javax.microedition.jcrmi.RemoteRef的对象中。RemoteRef 属于 SATSA,用于保持远程 Java Card 对象的引用。

为了 J2ME 开发人员能更方便地使用远程 Java Card 对象的引用,SATSA 提供了名为 javax.microedition.jcrmi.RemoteStub 的类,该类实现了 Remote 接口(其实例由清单 7 的步骤 3 中的 getInitialReference() 方法返回)。我们将在后面说明 RemoteStub 类如何提供易用性(在SATSA 存根类如何工作中)。现在只需要注意的是,Connection.open() 方法将 RemoteRef 对象包装在 RemoteStub 对象中。

请注意,Connector.open() 属于 IO 框架的一部分。它将根据所使用的协议返回对象。IO 框架提供名为 javax.microedition.io.Connection 的接口,Connector.open() 方法将返回其协议特定的实例。

在本例中,将使用 SATSA 建立与远程 Java Card applet 通信。因此,Connector.open() 应该返回 Connection 接口特定于 SATSA 的实例。JavaCardRMIConnection 类(其实例由清单 7 的步骤 2 中的 Connector.open() 返回)实现 SATSA的 Connection 接口。因此,Connector.open() 将 RemoteStub对象包装在 JavaCardRMIConnection 对象中。

以上讨论可简要总结为以下三点:

  1. Connector.open() 获取远程 Java Card 对象的引用,并将引用包装在 RemoteRef 对象中。
  2. Connector.open() 将 RemoteRef object 包装在 RemoteStub 对象中。
  3. 最后,Connector.open() 将 RemoteStub 对象包装在 JavaCardRMIConnection 对象中,并在清单 7 的步骤 2 中返回后一个对象。

步骤 3

JavaCardRMIConnection.getInitialReference() 方法在清单 7 的步骤 3 中返回经过包装的 RemoteStub 对象。RemoteStub 对象包装对远程 Java Card 对象的引用,因此,从现在起我们将 RemoteStub 称为远程 Java Card 对象的本地表示形式

获得了远程 Java Card 对象的本地表示形式后,可以像使用本地对象一样使用此表示形式。




回页首


将远程 Java Card 对象表示为本地 J2ME 对象

稍后(在实现 Signer 类 中),我们将说明名为 Signer 的 Java Card 如何工作。Signer 类属于 JavaCardSignatureCalculator applet 的一部分,包含一个 sign() 方法,该方法接受应用程序数据作为参数并返回所需的签名值。

您将使用此 Signer 对象(一个远程 Java Card 对象)作为本地 J2ME 对象对用户的 XML 数据进行签名。

为了以本地方式使用 Signer 对象,需要为远程 Signer 对象创建本地存根。我们将此本地存根命名为SignatureMethod_Stub。此 SignatureMethod_Stub 类对 RemoteStub 进行了扩展,因此可作为远程 Signer 对象的本地表示形式使用。

要创建 SignatureMethod_Stub 类,需要遵循以下步骤:

 

  1. 编写一个接口,其公开远程 Signer 类中出现的相同方法且对 Remote 接口进行扩展。例如,我们在清单 8 中编写了名为 SignatureMethod 的接口: 

    清单 8. SignatureMethod 接口
                                    
    public interface SignatureMethod extends Remote {
        public static final short INVALID_PIN = (short)0x6002;
        
         public byte[] sign (  byte[] username,   byte[] password, byte[] dataToSign) 
             throws RemoteException, UserException;
    }
    
  2. 为了生成 SignatureMethod_Stub 类,需要使用 jcrmic 实用工具。不过,在本教程中用于开发SecureEverydayMIDlet 的 Sun Java Wireless Toolkit 中并不包含此实用工具。Sun 为 SATSA 1.0 for J2ME 提供了一个独立的参考实现(Reference Implementation,RI)(请参见参考资料)。jcrmic 工具属于此 SATSA RI。

    以下命令行使用 jcrmic 来生成 SignatureMethod_Stub 类:
    java -jar x:/satsa1.0/bin/jcrmic.jar -classpath .;x:/WTK23/lib/jsr177.jar; x:/WTK23/lib/midpapi20.jar; x:/WTK23/lib/cldcapi11.jar; -d . SignatureApplet.SignatureMethod

    此命令行语句将生成 SignatureMethod_Stub 类。我们将在 Sun Java Wireless Toolkit 对 SignatureMethod_Stub 类和其他类进行编译。

    请注意,上面所示的命令行语句将报告一个错误“Java compiler not found”。可以忽略此错误,因为并不需要 jcrmic 对 Java 文件进行编译。

 

为了方便起见,可以在本教程的代码下载中找到 SignatureMethod_Stub 类(请参见下载部分)。

步骤 4

此 SignatureMethod_Stub 类可以在 J2ME MIDlet 中表示远程 Signer 类。因此,在清单 7 的步骤 4 中,会将清单 7 的步骤 3 中的 getInitialReference() 方法返回的对象强制转换为 SignatureMethod_Stub 对象。

接下来我们将说明 SignatureMethod_Stub 类是如何工作的。




回页首


SATSA 存根类如何工作

现在我们将讨论 SignatureMethod_Stub 类如何包装 SATSA 功能。

SignatureMethod_Stub 类的主要目的是为了便于使用。请注意,SignatureMethod_Stub 类对于 SATSA 的用途与 WSA 的 WSA 存根类用途一样。(即,SignatureMethod_Stub 类按照与 WSA 存根类包装 WSA 功能类似的方式包装 SATSA 功能。)

图 2 演示了 SignatureMethod_Stub 的工作原理:
图 2. SignatureMethod_Stub 类的工作原理 
 

可以从图 2 中看到,SignatureMethod_Stub 类充当代理。J2ME MIDlet 可以调用 SignatureMethod_Stub 类所公开的方法。另一方面,SignatureMethod_Stub 类在内部使用 SATSA API 与 SignatureMethod_Stub 类表示的实际远程对象进行所有通信。

接下来,我们将说明 SignatureMethod_Stub 类如何为 J2ME MIDlet 提供易用性。清单 9 显示了 jcrmic 生成的 SignatureMethod_Stub 类实现。
清单 9. jcrmic 生成的 SignatureMethod_Stub 类

                    
public final class SignatureMethod_Stub
    extends javax.microedition.jcrmi.RemoteStub
    implements SignatureApplet.SignatureMethod, java.rmi.Remote {
    
    // constructor
    public SignatureMethod_Stub() {
    super();
    }
    
    public byte[] sign(
        byte[] param1, 
        byte[] param2, 
        byte[] param3) 
        throws java.rmi.RemoteException, 
        javacard.framework.UserException 
   {
    
    try {
        Object result = 
                ref.invoke(
                    "sign([B[B[B)[B", 
                    new java.lang.Object[] { param1, param2, param3 });
        return (byte[]) result;
        
    } catch (java.lang.RuntimeException e) {
        throw e;
    } catch (java.rmi.RemoteException e) {
        throw e;
    } catch (javacard.framework.UserException e) {
        throw e;
    } catch (java.lang.Exception e) {
        throw new java.rmi.RemoteException("undeclared checked exception", e);
    }
    }
}

请注意清单 9 中的 SignatureMethod_Stub 类的 sign() 方法,该方法接受三个参数:param1param2 和 param3param1 和 param2 分别包装用户名和密码。param3 中包含要进行签名的数据。

远程 Signer 对象的 sign() 方法也接受三个完全相同的参数。清单 9 的 sign() 方法直接将这三个参数传递给 RemoteRef 对象,而后者负责处理相关逻辑,以将这三个参数传递给远程 Signer 对象。




回页首


调用 sign() 方法

在 清单 7 的 SignatureCalculator 构造函数的步骤 4 中,将获得对远程 Signer 对象的本地引用(即 SignatureMethod_Stub 类),您需要调用此对象的 sign() 方法。

步骤 5

不过,在调用 sign() 方法之前,需要获得要进行签名的数据。GetSubjectsSignatureSignedInfo 对象(清单 7 中的 SignatureCalculator 构造函数的第一个参数)包含要进行签名的数据。因此,在清单 7 的步骤 5 中,我们将从 GetSubjectsSignatureSignedInfo 对象提取要进行签名的数据。

步骤 6

最后,在 SignatureCalculator 构造函数的步骤 6 中,将调用 SignatureMethod_Stub 类的 sign() 方法。sign() 方法接受三个参数:用户名、密码和要进行签名的数据。

SignatureMethod_Stub 的 sign() 方法将在内部管理与远程 Signer 对象的所有通信,并会以字节数组的形式返回所需的签名值。SignatureCalculator 构造函数将签名值存储在类级别的变量 signatureValue 中。

清单 7 中所示,SignatureCalculator 类具有一个名为 getSignatureValue() 的方法,该方法会向调用应用程序返回签名值。我们在第 2 部分的清单 15 的步骤 3 中曾使用 populateDataMembers()方法调用此 getSignatureValue() 方法来获取所需的签名值。

以上就是对 SignatureCalculator 类工作原理的讨论。接下来我们将说明 JavaCardSignatureCalculator applet 如何工作。




回页首


构建基于 J2ME 的安全 SOAP 客户机,第 3 部分: 安全 Web 服务 API 存根类

构建存根增强器工具

第 4 页,共 11 页


对本教程的评价

帮助我们改进这些内容


构建 applet

两种类型的 Java Card 代码

接下来我们将说明如何构建 JavaCardSignatureCalculator applet,通过该 applet,可以安全地存储用户加密密钥,并使用此密钥对用户的数据进行加密。

Faheem Khan 所撰写的一系列 developerWorks 文章讨论了 Java Card 应用程序的完整生命周期(请参见参考资料)。如果希望详细了解 Java Card 应用程序的体系结构和生命周期,可以参考这些文章。

在本教程中,我们重点讨论的是特定于 JavaCardSignatureCalculator applet 的 Java Card 功能。如果您只是要了解 JavaCardSignatureCalculator 如何工作,则无需阅读 Khan 的这些文章。

要实现任何 Java Card applet(如 JavaCardSignatureCalculator),都要编写两种类型的 Java 代码:

  • 在 applet 在 Java Card 上安装期间运行的代码。我们将其称为 JavaCardSignatureCalculator 安装代码。
  • 每次客户机访问 Java Card applet 时运行的代码。此代码实现 JavaCardSignatureCalculatorapplet 的业务逻辑。因此,我们将其称为 JavaCardSignatureCalculator applet 的业务逻辑。

我们将首先说明 JavaCardSignatureCalculator 安装代码,然后将说明如何实现 JavaCardSignatureCalculator 的业务逻辑。




回页首


安装代码

我们将使用 Sun 提供的 Java Card Development Kit (JCDK) V2.2.1 实现 JavaCardSignatureCalculator。JCDK 提供了一系列工具,可用于开发、安装和测试 Java Card applet。我们将稍后演示如何使用这些工具(在运行 JavaCardSignatureCalculator 中)。

目前只需要注意,所有 Java Card applet 都需要从名为 javacard.framework.Applet 类进行扩展,该类中包含了 applet 需要的 Helper 方法。Applet 类有一个名为 install() 的静态方法,会在对 Java Card 安装 applet 时自动调用。将需要在 install() 方法中编写安装代码,如清单 10 中所示:
清单 10. install() 方法

                    
public static void install(
         byte[] installationData, 
         short offset, 
         byte length) {
    new JavaCardSignatureCalculator(
        installationData, 
        offset, 
        length);
}//install()

install() 方法会在安装期间获取需要存储在 Java Card applet 中的所有数据。这些数据通过三个参数提供,即 installationDataoffset 和 length

installationData 是保存安装数据的字节数组。安装数据始终包含一个 AID 值。安装数据还包含其他数据部分。例如,对于 JavaCardSignatureCalculator applet,安装数据包含用户名、密码和加密密钥。我们将在稍后说明如何使用安装数据的这些部分。

offset 参数指定安装数据在 installationData 字节数组中的开始位置。length 参数指定安装数据的字节数。

每个 Java Card 都包含运行时环境,称为 Java Card Runtime Environment (JCRE)。安装 Java Card applet 时,JCRE 会调用 install() 静态方法。

清单 10 中的 install() 方法只需要实例化 JavaCardSignatureCalculator 类,并同时向 JavaCardSignatureCalculator 构造函数传递安装数据即可。JavaCardSignatureCalculator 构造函数将对安装数据执行所需的处理。




回页首


构造函数

清单 11 显示了 JavaCardSignatureCalculator 构造函数: 
清单 11. JavaCardSignatureCalculator 构造函数

                    
public JavaCardSignatureCalculator(byte[] installationData, short offset, byte length) {
    //**** Step1 *****//
    //The offset byte contains length of AID.
    byte aidLength = installationData[offset];
    byte[] appletAID = new byte[aidLength];
    Util.arrayCopy(
        appletAID, (short)0, installationData, (short)(offset+1), (short)aidLength);
    if (appletAID.length == 0)
        register();
    else
        register(appletAID, (short)0, aidLength);
    //**** Step2 *****//
    short kdOffset = (short)( offset + aidLength + 1 );
    byte[] username = new byte[USERNAME_LENGTH];
    byte[] password = new byte[PASSWORD_LENGTH];
    Util.arrayCopy(
        username, 
        (short)0, 
        installationData, 
        (short)(kdOffset), 
        (short) USERNAME_LENGTH);
    Util.arrayCopy(
        password, 
        (short)0, 
        installationData, 
        (short)(kdOffset + USERNAME_LENGTH), 
        (short) PASSWORD_LENGTH);
    AuthenticationService authenticationService = 
            new AuthenticationService(username, password);
    //**** Step3 *****//
    byte[] cryptogaphicKey = new byte[KEY_LENGTH];
    Util.arrayCopy(
        cryptogaphicKey, 
        (short)0, installationData, 
        (short)(kdOffset + USERNAME_LENGTH + PASSWORD_LENGTH), 
        (short) KEY_LENGTH);
    Signer signer = 
        new Signer(
            authenticationService, 
            cryptogaphicKey);
    //**** Step4 *****//
    RMIService rmiService = new RMIService(signer);
    //**** Step5 *****//
    disp = new Dispatcher( (short)2);
    disp.addService(secService, Dispatcher.PROCESS_COMMAND);
    disp.addService(rmiService, Dispatcher.PROCESS_COMMAND);
}

JavaCardSignatureCalculator 构造函数将在安装期间执行五个步骤,以处理安装数据。接下来我们将对这五个处理步骤进行说明。




回页首


步骤 1:向 applet 注册 AID

第一步是从安装数据提取 AID 值。AID 标识 JavaCardSignatureCalculator applet。因此,需要告知 JCRE 使用这个 AID 值标识 JavaCardSignatureCalculator applet。

为此,将在清单 11 的步骤 1 中调用名为 register() 方法,该方法在 Applet 类中定义。register() 方法接受 AID 值,并向 JCRE 进行注册。

现在使用 AID 值向 JCRE 注册 JavaCardSignatureCalculator。这意味着,JCRE 可以在接收到对 JavaCardSignatureCalculator 的请求时调用正确的 applet。这非常重要,因为同一个 JCRE 可以承载多个 applet。




回页首


步骤 2:创建身份验证服务

在步骤 2 中,将从安装数据提取用户名和密码。然后将使用用户名和密码实例化名为 AuthenticationService 的类。

AuthenticationService 类将保存用户名和密码,稍后将使用其对请求客户进行身份验证。将在实现 AuthenticationService 类中对 AuthenticationService 类进行说明。




回页首


步骤 3:创建签名类

在步骤 3 中,将实例化另一个类 Signer,该类的 sign() 方法会生成所需的签名值。正如您可以很容易猜到的,Signer 正是在清单 7 步骤 6 的 SecureEverydayMIDlet 中从远程调用其 sign() 方法的类。

为了实例化 Signer 类,将需要用户的加密密钥(例如,私钥)。将从安装数据中提取加密密钥。

Signer 构造函数接受两个参数:

  • 加密密钥。Signer 类使用此加密密钥来计算签名值。
  • 在步骤 2 中实例化的 AuthenticationService 对象。Signer 类使用 AuthenticationService 类来在计算签名值前检查用户的身份验证状态。

我们将稍后说明 Singer 构造函数如何工作(在实现 Signer 类中)。




回页首


步骤 4:创建 RMI 服务

清单 11 的步骤 4 中,JavaCardSignatureCalculator 构造函数对 RMIService 类进行实例化。此类是 Java Card 框架的一部分,用于管理远程应用程序(如 SecureEverydayMIDlet)对 Java Card 对象(如 Signer)的调用。

RMIService 类实现远程调用 Java Card 对象的逻辑。

RMIService 构造函数仅接受一个参数,即应该远程调用的 Signer 对象。RMIService 在内部管理关于以下方面的所有详细信息:如何调用 Signer.sign() 方法、如何将应用程序数据传递给 sign() 方法,以及如何将签名值返回给 SecureEverydayMIDlet




回页首


步骤 5:控制 Java Card 服务的调用

我们已经创建了两个服务类:步骤 2 中的 AuthenticationService 和步骤 4 中的 RMIService。现在需要指定将调用这两个服务类的顺序。

自然应该在 RMIService 前调用 AuthenticationService,以便 AuthenticationService 能够进行身份验证状态检查,以确定 RMIService 是否应该为客户机的请求提供服务。

为此,要在清单 11 的步骤 5 中创建 Dispatcher 对象。Dispatcher 也属于 Java Card 框架,用于处理 Java Card 应用程序提供的各种服务的调用。您只需要调用 Dispatcher 对象的 addService() 方法,并同时随方法调用传递服务提供者对象(如 AuthenticationService 和 RMIService)。Dispatcher 将在内部按照相同的顺序管理两个服务的调用。

请注意,需要调用 addService() 方法两次,一次为 AuthenticationService 进行调用,然后再为 RMIService 进行调用。而这正是在清单 11 的步骤 5 中完成的工作。

现在已经完成了处理安装数据所需的所有数据,因此已经完成了 Java Card 安装代码。现在需要了解如何执行刚刚所编写的安装代码。




回页首


执行 Java Card 安装代码

Sun 的 Java Wireless Toolkit 提供了多个工具,可用于对 Java Card applet 进行测试。可以通过两种方法进行此工作:

  • 第一种方法包含多个步骤,需要在此期间编译 Java Card applet 并创建其内存映像。内存映像按照与在 Java Card 中完全相同的方式表示 applet。最后,将使用随 Sun Java Wireless Toolkit 一起提供的模拟器工具运行 applet。正如前面提到的,Faheem Khan 的文章详细地对此方法进行了说明(请参见参考资料)。
  • 第二种方法很简单。此方法并不要求生成任何内存映像,而可以直接运行 Java Card applet。此方法适用于进行开发和调试。

我们将使用第二种方法来开发和调试 JavaCardSignatureCalculator applet。如果希望了解第种个方法,请参考 Khan 的文章。

不过,第二种方法并不允许向在前面清单 10 中看到的 install() 方法发送用户名、密码和加密密钥。相反,它将在不传递任何安装数据的情况下调用 install() 方法。

因此,在使用第二种方法时,将在 install() 方法中硬编码用户名、密码和加密密钥,如清单 12清单 10 的修改)中所示:
清单 12. 修改后的 install() 方法

                    
public static void install(
    byte[] installationData, 
    short offset, 
    byte length) 
{
    //Hard-coded username
    //ASCII for 'alice' 
    byte[] username = {
        (byte)0x61,
        (byte)0x6c,
        (byte)0x69,
        (byte)0x63,
        (byte)0x65
    };
    //Hard-coded password
    //ASCII for 'keyPass'
    byte[] password = {
        (byte)0x6b,
        (byte)0x65,
        (byte)0x79,
        (byte)0x50,
        (byte)0x61,
        (byte)0x73,
        (byte)0x73
    };
        
    //Hard-coded cryptographic key
    byte[] cryptographyKey = {(byte)0x45, (byte)0x09, (byte)0x36, (byte)0xDE, ...};
   
    //Copy bits of installation data into the installationData byte array. 
    installationData = putTogatherInstallationData (
        username, 
        password, 
        cryptographyKey
    ); 
    new JavaCardSignatureCalculator(
        installationData, 
        (short)0, 
        (byte)installationData.length);
}//install()

构建了 JavaCardSignatureCalculator 的业务逻辑之后,我们将稍后(在运行 JavaCardSignatureCalculator 中)演示如何使用第二种方法。




回页首


构建基于 J2ME 的安全 SOAP 客户机,第 3 部分: 安全 Web 服务 API 存根类

构建存根增强器工具

第 5 页,共 11 页


对本教程的评价

帮助我们改进这些内容


实现业务逻辑

调用 JavaCardSignatureCalculator

要讨论的下一个步骤是,如何实现 JavaCardSignatureCalculator applet 的业务逻辑。每次 SecureEverydayMIDlet 调用 JavaCardSignatureCalculator applet 时,都会运行业务逻辑。因此,我们需要首先说明远程调用 JavaCardSignatureCalculator 时的事件发生序列。

请查看图 3,其中显示了 SecureEverydayMIDlet 调用 JavaCardSignatureCalculator 时的事件序列:
图 3. SecureEverydayMIDlet 调用 JavaCardSignatureCalculator 时的事件发生序列 
 

可以在图 3 中找到以下事件:

  1. 第一个任务是选择 SecureEverydayMIDlet 希望调用的 applet。为此,SecureEverydayMIDlet将向 JCRE 发送一个 SELECT APDU。SELECT APDU 包含 JavaCardSignatureCalculator applet 的 AID。
  2. JCRE 将从 SELECT APDU 提取 AID 并根据 AID 标识 JavaCardSignatureCalculator applet。
  3. JCRE 发送 SELECT APDU 的响应,以建立与 SecureEverydayMIDlet 的通信会话。
  4. SecureEverydayMIDlet 通过向 JCRE 发送 INVOKE APDU 来调用 JavaCardSignatureCalculatorINVOKE APDU 包含应用程序数据,该数据由三个部分组成:请求客户的用户名和密码以及要进行签名的数据。将在稍后(步骤 8 中)使用用户名和密码来在进行数据签名前对请求客户进行身份验证。
  5. JCRE 调用 JavaCardSignatureCalculator applet。调用 Java Card applet 意味着调用 applet 的 process() 方法。JavaCardSignatureCalculator 的 process() 方法仅包含一行代码:
    public void process(APDU apdu) throws ISOException { disp.process(apdu); }
  6. 上面所示的 process() 方法要求 Dispatcher 开始调用在前面的清单 11 的步骤 5 的安装期间添加到 Dispatcher 的服务。
  7. Dispatcher 获得控制并调用身份验证服务。
  8. 身份验证服务执行用户身份验证,并将控制交回 Dispatcher
  9. Dispatcher 调用 RMI 服务。
  10. RMI 服务检查身份验证服务,以确定用户身份验证是成功还是失败。如果用户身份验证成功,RMI 服务将要求 Signer 类计算所需的签名值。
  11. RMI 服务将签名值发送回 JCRE。
  12. JCRE 将签名值发送给 SecureEverydayMIDlet

通过这 12 个事件可以发现,您只需要实现 AuthenticationService 和 Signer 类即可。Java Card 框架将处理其他的所有工作。因此,接下来我们将说明如何实现 AuthenticationService 和 Signer 类。




回页首


实现 AuthenticationService 类

前面在清单 11 的步骤 2 中,JavaCardSignatureCalculator 构造函数从安装数据提取用户名和密码,并将这些数据传递给 AuthenticationService 构造函数。AuthenticationService 构造函数(如清单 13 中所示)将用户名和密码作为字节数组存储在类级别的变量中。AuthenticationService 类使用这些字节数组来对请求客户进行身份验证。
清单 13. AuthenticationService 构造函数

                    
public class AuthenticationService extends BasicService implements SecurityService {
    //Class-level variables  
    private byte[] username = null;
    private byte[] password = null;    
    private byte authenticationResult;
    public AuthenticationService (byte[] username, byte[] password) {
        this.username = username;
        this.password = password;
        authenticationResult = (short) 0;
    }//AuthenticationService
    //Other methods of the AuthenticationService class
}

还要注意,在清单 13 中,AuthenticationService 构造函数初始化名为 authenticationResult 的类级别变量,用以表示身份验证过程的结果。authenticationResult 的值将根据身份验证过程的结果进行设置。

AuthenticationService 是一个服务提供者类,图 3 的步骤 7 中将由 Dispatcher 对其进行调用。为了调用 AuthenticationService 类,Dispatcher 将调用 AuthenticationService.processCommand()方法。

processCommand() 方法在名为 Service 的接口中定义。Service 接口属于 Java Card 框架,包含任何服务提供者类为了提供不同类型的服务而将要实现的方法。对于 AuthenticationService 的情况,只需要实现 processCommand() 方法(将在下面进行讨论)。

Java Card 框架还提供了名为 BasicService 的实用类,其中包含 Service 接口的所有方法的框架实现。因此,不要直接实现 Service 接口,更好的做法是让 AuthenticationService 扩展 BasicService 类,且仅重写 processCommand() 方法。

BasicService 类还提供多个实用类,AuthenticationService 将在身份验证期间使用这些实用类处理应用程序数据。

AuthenticationService 类还实现了名为 SecurityService 的接口。SecurityService 接口包括与安全相关的方法,安全服务需要实现这些方法。例如,AuthenticationService 实现名为 isAuthenticated() 的方法(如清单 14 中所示),该方法将返回 authenticationResult 变量的值。Signer 类调用isAuthenticated() 方法来在计算签名值之前检查身份验证结果。

AuthenticationService 类中定义的另一个方法名为 reset()。它会将 authenticationResult 重置为零。Signer 类会在计算签名值之后将 authenticationResult 重置为零。这可确保成功身份验证的结果仅用于一次签名计算。
清单 14. isAuthenticated() 和 reset() 方法

                    
public class AuthenticationService 
    extends BasicService implements SecurityService 
{
    //Constructor and data members of the AuthenticationService class
    public boolean isAuthenticated(short principal) throws ServiceException {
         return (authenticationResult == principal);
    }//isAuthenticated
    public void reset(){
        authenticationResult = (short) 0;
    }//reset()
    //Other methods of the AuthenticationService class
}//AuthenticationService

接下来我们将讨论 processCommand() 的实现,该方法提供 AuthenticationService 类的核心功能。




回页首


实现 processCommand() 方法

processCommand() 方法实现如清单 15 中所示:
清单 15. processCommand() 方法

                    
public class AuthenticationService 
   extends BasicService implements SecurityService 
{
   /***Constructor and data members will fit here.***/
   public boolean processCommand(APDU apdu) 
   {
      /**** Step 1 ****/
      if (getINS(apdu) == (byte)0x38) 
      {
         /**** Step 2 ****/
         receiveInData(apdu);
         /**** Step 3 ****/
         byte[] name2BAuthenticated = new byte[5];
         Util.arrayCopy(
            apdu.getBuffer(), (short)10, name2BAuthenticated, (short)0, (short)5);
         byte[] pass2BAuthenticated = new byte[7];
         Util.arrayCopy(
            apdu.getBuffer(), (short)15, pass2BAuthenticated, (short)0, (short)7);
         /**** Step 4 ****/
         if (Util.arrayCompare(
            name2BAuthenticated, (short)0, username, (short)0, (short) 5) == 0 
            && Util.arrayCompare(
            pass2BAuthenticated, (short)0, password, (short)0, (short)7) == 0)
            authenticationResult = PRINCIPAL_CARDHOLDER;
      }
      return false;
   }//processCommand
   /*** Other methods will fit here.***/
}//AuthenticationService

processCommand() 方法执行用户身份验证并将身份验证结果存储在 authenticationResult 变量中。

当 Dispatcher 调用图 3 步骤 7 中的 processCommand() 方法时,会随方法调用传递一个 APDU 对象作为输入参数。此 APDU 对象包含缓存字节数组,可以从中读取应用程序数据。(前面提到过,在清单 7 的步骤 6 中调用 sign() 方法时,SignatureCalculator 构造函数会发送应用程序数据。)

清单 15 中的 processCommand() 方法通过四个步骤对用户进行身份验证:

  1. processCommand() 方法检查输入 APDU 对象是否表示 INVOKE APDU。

    请注意,processCommand() 方法使用名为 getINS() 的实用类来检查输入 APDU 的类型。getINS() 方法属于 BasicService 类,接受 APDU 对象作为输入参数。它会将 APDU 对象的类型作为字节标识符返回。

    如果输入 APDU 不是 INVOKE APDU,processCommand() 方法将不进行任何处理。

  2. processCommand() 调用名为 receiveInData() 的方法,此方法接受输入 APDU 对象,并使用应用程序数据对其缓存字节数组进行填充。现在,APDU 对象将包装您在图 3 的步骤 4 中看到的所有三个应用程序数据部分。
  3. processCommand() 方法将从缓存字节数组中提取请求客户的用户名和密码(进行身份验证)。processCommand() 方法将用户名和密码分别存储在两个字节数组中,即 user2BAuthenticated 和 password2BAuthenticated
  4. 此步骤说明了实际的身份验证过程:processCommand() 方法将要进行身份验证的用户名和密码(在步骤 3 中提取)与安装期间用于在清单 11 的步骤 2 中实例化 AuthenticationService对象的用户名和密码进行比较。

    如果比较成功,processCommand() 方法会将 authenticationResult 变量的值设置为 PRINCIPAL_CARDHOLDER

您需要理解 PRINCIPAL_CARDHOLDER 的含义以及为何 processCommand() 方法使用它。Java Card 允许成功身份验证过程得出三个可能的结果,这意味着身份验证过程可能会发现请求客户属于以下情况之一:

  • 卡的所有者
  • 服务提供者
  • 卡的发行者

对于 AuthenticationService 类,只有第一种可能与其相关。即,身份验证服务类仅会验证请求客户是否是 Java Card 的所有者。正是由于这个原因,如果用户名与密码匹配,processCommand() 方法会将 authenticationResult 的值设置为 PRINCIPAL_CARDHOLDER。这意味着已确定请求客户是 Java Card 的所有者。

我们已经了解了身份验证过程。在前面图 3 的步骤 9 中成功进行身份验证后,Dispatcher 将调用 RMIServiceRMIService 会要求 Signer 类计算所需的签名值。接下来我们将说明 Signer 类如何执行签名计算。




回页首


实现 Signer 类

Signer 类(如清单 16 中所示)负责计算签名值。在前面清单 11 的步骤 3 中,JavaCardSignatureCalculator 构造函数实例化了 Signer 类,并同时向 Signer 构造函数传递加密密钥和 AuthenticationService 对象。Signer 构造函数将这两个对象作为类级别变量存储。
清单 16. Signer 类

                    
public class Signer 
   extends CardRemoteObject implements SignatureMethod
{
   
   private AuthenticationService authenticationService;
   byte[] cryptographicKey = null;
   
   public Signer(
      AuthenticationService authenticationService, 
      byte[] key)
   {
      this.authenticationService = authenticationService;
      cryptographicKey = key;
   }// Signer
   public byte[] sign(
       byte[] username, 
       byte[] password, 
       byte[] data2BSigned) throws RemoteException, UserException 
   {
       
      /*** Step 1***/ 
      if(!authenticationService.isAuthenticated(SecurityService.PRINCIPAL_CARDHOLDER)) {
         UserException.throwIt(ISO7816.SW_DATA_INVALID);
      }
      authenticationService.reset();
      
      /*** Step 2***/ 
      DSASignature signer = new DSASignature();
      /*** Step 3***/ 
      byte[] completeXMLStructure  = authorSignedInfo (data2BSigned);
      
      /*** Step 4***/ 
      byte[] signatureValue = 
         signer.getSignatureValue(
            cryptographicKey,
            completeXMLStructure
         );
      /*** Step 5***/ 
      return signatureValue;
      
   }//sign()
}//Signer

请注意,在清单 16 中,Signer 类对名为 CardRemoteObject 的类进行了扩展。CardRemoteObject 类属于 Java Card 框架,包含名为 export() 的方法。export() 方法将导出 CardRemoteObject 实例的引用,以便供远程应用程序(如 SecureEverydayMIDlet)访问。

因此,通过从 CardRemoteObject 扩展,就可以将 Signer 类提供给 SecureEverydayMIDlet 访问。请注意,您并不需要关心具体在何时以何种方式调用 export() 方法。RMIService 类将在内部处理 export() 方法的调用工作。而这正是在清单 11 的步骤 4 中将 Signer 对象传递给 RMIService 构造函数的原因。

Signer 类仅包含一个公共方法,名为 sign(),该方法计算签名值。SecureEverydayMIDlet 在清单 7 的步骤 6 中调用此 sign() 方法,并同时随方法调用传递用户名、密码和要进行签名的数据。接下来我们将说明 Signer.sign() 方法如何工作。




回页首


实现 sign() 方法

sign() 方法(如清单 16 中所示)接受字节数组形式的应用程序数据,并通过以下五个步骤根据应用程序数据计算签名值:

步骤 1

sign() 方法将通过调用 AuthenticationService.isAuthenticated() 方法(已在实现 AuthenticationService 类中进行了讨论)来检查请求客户的身份验证状态。

sign() 方法将 SecurityService.PRINCIPAL_CARDHOLDER 作为参数传递给 isAuthenticated() 方法。这意味着 sign() 方法希望验证请求客户是否为 Java Card 的所有者。如果 isAuthenticated() 返回false(表示请求客户不是 Java Card 的所有者),sign() 方法并不会进行任何处理,而且会引发异常。

如果 isAuthenticated() 返回 true(意味着请求客户是 Java Card 的所有者),sign() 方法会通过调用 AuthenticationService 类的 reset() 方法重置身份验证状态。这可确保将在每次发送签名请求时对客户进行身份验证。

步骤 2

sign() 方法实例化名为 DSASignature 的 Helper 类。DSASignature 类根据 DSA 签名算法计算签名值,此签名算法是由美国国家标准局(National Institute of Standards and Technology,NIST)制定的(请参见参考资料)。

我们在本教程中实现 DSA 签名算法,将算法实现包装在 DSASignature 类中。这样的做法具有扩展性优势。如果希望实现其他签名算法,可以实现自己的类,并将其集成到 sign() 方法中。

我们将稍后实现 DSASignature 类(在实现 DSASignature.getSignatureValue() 中)。DSASignature公开 getSignatureValue() 方法,该方法接受两个参数:

  • 要进行签名的数据
  • 计算签名值时使用的加密密钥

getSignatureValue() 方法计算并返回签名值。

步骤 3

sign() 方法将创建需要计算其签名值的 XML 结构,该结构是第 1 部分的清单 6 中的 SignedInfo 元素,为了方便您参考,我们在本教程的清单 17 中给出了对应的内容:
清单 17. SignedInfo 元素

                    
<SignedInfo>
   <CanonicalizationMethod
      Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">
   </CanonicalizationMethod>
   <SignatureMethod
      Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1">
   </SignatureMethod>
   <Reference URI="//soap:Envelope/soap:Body/*">
      <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod>
      <DigestValue>
         Xs4gwdg456sEsdaPf=
      </DigestValue>
   </Reference>
</SignedInfo>

请注意,您需要对清单 17 的 SignedInfo 元素进行签名。清单 17 的 SignedInfo 元素的 DigestValue 二级子项包装根据 XML 数据计算的摘要值(在第 2 部分的实例化 digestValue 数据成员中计算所得)。

因此,如果对完整的 SignedInfo 结构进行签名,将间接地对摘要值包装在 SignedInfo 元素内的 XML 数据进行签名。从计算方面而言,对摘要值进行签名的开销比对实际数据进行签名的开销更少。

不过请注意,要签名的数据(sign() 方法调用的第三个参数)并不包含完整的 SignedInfo 结构。相反,其中仅包含摘要值,以尽可能减少 SecureEverydayMIDlet 和 JavaCardSignatureCalculator 之间交换的数据大小。

因此,在清单 16 的步骤 3 中将创建清单 17 中的 SignedInfo 元素。

创建 SignedInfo 元素非常简单。表示 SignedInfo 结构的文本字符串中仅包含一个动态元素(即摘要值)。结构的其余部分都不是动态的,因此可以对其进行硬编码,如清单 18 的 authorSignedInfo() 私有 Helper 方法中所示:
清单 18. authorSignedInfo() 方法

                    
private byte[] authorSignedInfo(byte[] base64EncodedDigestValue){
   byte[] startSignedInfo = {(byte)0x3c, (byte)0x53, (byte)0x69,.. );
   byte[] endSignedInfo   = {(byte)0x65, (byte)0x74, (byte)0x68,.. );
   short length = 
      (short) (
         startSignedInfo.length + base64EncodedDigestValue.length + endSigndInfo.length);
      
   byte[] signedInfoStructure = new byte [length];
 
   for (short i = (short) 0; i < startSignedInfo.length ; i++)
      signedInfoStructure[i] = startSignedInfo [i];
   for (short j = (short) startSignedInfo.length ;  j < signedInfoStructure.length ; j++) 
   {
      if (j < (short) (startSignedInfo.length + base64EncodedDigestValue.length))
         signedInfoStructure[j] = 
            base64EncodedDigestValue [ (j - startSignedInfo.length)];
      else 
         signedInfoStructure[j] = 
            endSigndInfo [
               (j - (startSignedInfo.length + base64EncodedDigestValue.length))];
   }
      
   return signedInfoStructure;
}// authorSignedInfo()

现在已经得到了要进行签名的完整 SignedInfo 结构。Signer 类已经获得了加密密钥(已在清单 11 的步骤 3 传递给它了)。

步骤 4

因此在步骤 4 中,sign() 方法调用 DSASignature.getSignatureValue() 方法,并同时随 getSignatureValue() 方法调用传递要进行签名的完整 XML 结构以及用于计算签名值的加密密钥。getSignatureValue() 方法返回包含所需签名值的字节数组,我将稍后对此方法进行说明(在实现 DSASignature.getSignatureValue() 中)。sign() 方法将返回的字节数组存储在名为 signatureValue 的变量中。

步骤 5

最后,sign() 方法将 signatureValue 返回到 RMI 服务,以便 RMI 服务能继续执行图 3 的步骤 11(即将签名值发送回 JCRE)。




回页首


实现 DSASignature.getSignatureValue()

现在我们将在 DSASignature.getSignatureValue() 方法中实现 DSA 签名算法。

我们从 The Legion of the Bouncy 的开源 DSA 实现(请参见参考资料)借用了 DSASignature.getSignatureValue() 方法的大部分编程逻辑。

Bouncy Castle 提供了各种流行加密算法(包括 DSA)的 J2SE 与 J2ME 开源实现。将 Bouncy 的 J2ME 代码放入 Java Card applet 中运行非常简单。您只需要确保不在 Java Card 代码中使用以下 J2ME 数据类型即可:

  • String
  • Float
  • Double
  • Long

很容易猜到,这些 J2ME 数据类型在 Java Card 应用程序中不可用。本教程的源代码下载中包含名为 JavaCardSignatureCalculator 的文件夹,其中包含 JavaCardSignatureCalculator applet 的所有类,包括从 Bouncy Castle 借用的内容(请参见下载部分)。




回页首


生成加密密钥

请参见清单 12,其中在 install() 方法中硬编码了用户的加密密钥。现在我们将说明如何生成在清单 12 中硬编码的密钥。

可以使用名为 keytool 的 J2SE 工具来生成 DSA 加密密钥。以下命令行语句说明了如何进行此工作:
keytool -genkey -keyalg DSA -dname "CN=Alice, OU=EmailService, O=EverydayServices, C=PK" -alias alice -keypass keyPass -keystore keystore -storepass storePass

此命令为用户 alice 生成密钥。此密钥的密码将为 keyPass。密钥将存储在名为 keyStore 的密钥存储器中。密钥存储器的密码将为 storePass

尽管生成了密钥,但还需要将此密钥硬编码到 install() 方法中(请参考清单 12)。因此需要以字节数组的形式保存此密钥。

我们编写了一个简单的 J2SE 应用程序 StoreKeyAsBytes,可在本教程的源代码下载中找到此应用程序(请参见下载)。可以运行 StoreKeyAsBytes 来从密钥存储器以字节数组的形式获取加密密钥。

我们在文本文件 aliceKey.txt 提供了此字节数组,可以在源代码下载中找到此文件。我们使用 aliceKey.txt 文件中的字节数组来在清单 12 中对密钥值进行硬编码。




回页首


运行 JavaCardSignatureCalculator

现在可以使用前面介绍的第二种方法(在执行 Java Card 安装代码中)来测试 JavaCardSignatureCalculator applet。

第一步是像处理所有普通 Java 类一样编译所有 JavaCardSignatureCalculator 类。您的类路径中需要包含以下 JAR 文件:

  • api.jar
  • javacardframework.jar

可以在 JCDK 安装中找到这些 JAR 文件。

源代码下载中提供了所有 JavaCardSignatureCalculator 类的源代码和已编译形式(请参见参考资料)。

第二种方法要求使用 Java Card Workstation Development Environment (JCWDE) 工具,该工具随 JCDK 一起提供。为了在 JCWDE 上测试 JavaCardSignatureCalculator applet,您需要执行以下四个步骤:

  1. 将 JavaCardSignatureCalculator 应用程序的经过编译的类复制到 JCDK 安装目录的 classes 文件夹中。例如,如果在 X: 驱动器安装 JCDK,则可以在以下位置找到 classes 文件夹:X:/java_card_kit-2_2_1/samples/classes。
  2. 打开配置文件 jcwde_rmi.app,此配置文件位于 X:/java_card_kit-2_2_1/samples/src/demo/jcwde 目录中。需要对此文件进行编辑,以包括 JavaCardSignatureCalculator 应用程序的 AID。可以选择您的 Java Card applet 的 AID。AID 的长度在 5 到16 个字节之间。

    例如,我们选择 0xa0:0x0:0x0:0x0:0x62:0x3:0x1:0xc:0x8:0x2 作为 JavaCardSignatureCalculator 的 AID。源代码下载中提供了经过编辑的 jcwde_rmi.app 文件(请参见下载部分)。

  3. 将环境变量 JC_HOME 设置为指向您的 JCDK 安装的 bin 文件夹。例如,如果您在 X: 驱动器安装 JCDK,JC_HOME 的值将为 X:/java_card_kit-2_2_1/bin
  4. 现在可以使用以下命令行语句在 JCWDE 中运行 JavaCardSignatureCalculator
    jcwde jcwde_rmi.app

JavaCardSignatureCalculator 现在已启动,等待 SecureEverydayMIDlet 发出的签名请求。下一部分将说明如何使用 JavaCardSignatureCalculator 测试 SecureEverydayMIDlet 的最新形式。




回页首


运行签名应用程序

我们前面曾介绍过用于测试 SecureEverydayMIDlet 的测试机制(在测试 getBase64EncodedValue() 方法中)。您需要运行相同的机制来测试 SignatureCalculator 类的最新形式。

源代码下载在 Section5 文件夹中提供了所有 SecureEverydayMIDlet 类(源代码以及已编译形式)(请参见下载)。Section5 文件夹还包含了名为 SignatureVerifier 的 Java 应用程序,该应用程序侦听经过签名的 SOAP 消息(例如,来自无线客户机的此类消息)、处理经过签名的 SOAP 消息、验证摘要和签名值并在输出控制台打印验证结果。

运行此测试机制时,将发生以下事件序列:

  1. SecureEverydayMIDlet 根据 SOAP 消息计算摘要值。
  2. SecureEverydayMIDlet 与 JavaCardSignatureCalculator 通信,以获取 SOAP 消息的签名值。
  3. SecureEverydayMIDlet 创建 SOAP 消息的已签名形式,并将其发送给 SignatureVerifier
  4. SignatureVerifier 验证摘要值,并在输出控制台打印摘要验证结果。
  5. SignatureVerifier 验证签名值,并在输出控制台打印签名验证结果。

 

您的 SecureEverydayMIDlet 现在已经完成。而我们关于如何保护 WSA 存根类的安全的讨论也就到此结束了。

接下来将构建存根增强器工具,以执行到目前为止都是手动进行的大部分编程难点工作。




回页首


构建基于 J2ME 的安全 SOAP 客户机,第 3 部分: 安全 Web 服务 API 存根类

构建存根增强器工具

第 6 页,共 11 页


对本教程的评价

帮助我们改进这些内容


实现存根增强器工具

存根增强步骤

我们已经执行了很多步骤来增强存根类。现在将看到,之前手动执行的所有存根增强步骤都可以通过使用存根增强器工具 来自动实现。

在开始开发存根增强器工具前,让我们对之前手动进行的存根增强步骤进行一下简单的总结:

  1. 我们在第 2 部分的增强安全服务存根类对安全服务存根类(SecureMobileEmailService_PortType_Stub 类)进行了增强。增强安全服务存根类时,您对其构造函数、数据成员以及服务方法进行了增强。
  2. 我们在第 2 部分的增强 Signature 存根类对签名相关的存根类进行了增强。
  3. 实现了四个 Helper 类,分别为 CanonicalAuthorSHA1DigestCalculatorBase64Encoder 和SignatureCalculator

存根增强器工具将获取 WSA 存根生成器工具生成的安全服务存根类,并执行所有这些步骤。这意味着存根增强器工具将进行大部分难点工作,以保护基于 J2ME 的 SOAP 客户机。




回页首


存根增强器工具的体系结构

存根增强器工具包含名为 StubEnhancer 的类,其中有一个 main() 方法,如清单 19 中所示:
清单 19. StubEnhancer 类

                    
public class StubEnhancer {
 
    public static void main (String[] args) {
        /*** Step 1 ***/
        String secureServiceStubClassName = args[0];
        StubEnhancer enhancer = new StubEnhancer ();
        StringBuffer stringBuffer = 
            enhancer.readFileAsStringBufferObject (secureServiceStubClassName);
         /*** Step 2 ***/
        SecureServiceStubEnhancer secureServiceStub = 
            new SecureServiceStubEnhancer (stringBuffer);
         
        /*** Step 3 ***/
        secureServiceStub.writeFile();
         
        /*** Step 4 ***/
        String signatureClassName =  secureServiceStub.getSignatureClassName();
        String packageName        =  secureServiceStub.getPackageName();
        /*** Step 5 ***/
        stringBuffer = 
             enhancer.readFileAsStringBufferObject (signatureClassName);
            
        /*** Step 6 ***/
        SignatureRelatedEnhancer signatureEnhancer = 
            new SignatureRelatedEnhancer (
                stringBuffer,
                packageName,
                signatureClassName
            );
        /*** Step 7 ***/
        signatureEnhancer.writeFiles ();
        
        /*** Step 8 ***/
        HelperClassesGenerator helperClassesGenerator = 
            new HelperClassesGenerator (
                packageName,
                signatureEnhancer.getSignedInfoClassName()                    
        );  
        /*** Step 9 ***/        
        helperClassesGenerator.writeFiles();
     }//main() method
    /*** Other methods of StubEnhancer class will fit here.***/
}//StubEnhancer

可以通过使用以下命令行语句调用 StubEnhancer 类的 main() 方法:
java StubEnhancer SecureMobileEmailService_PortType_Stub.java

可以从上面的命令行语句中看到,StubEnhancer 类需要知道安全服务存根文件的名称。StubEnhancer遵循九个步骤来得到一组所需的增强存根类。

接下来我们将说明存根增强的九个步骤。




回页首


存根增强的步骤

清单 19 中的 main() 方法执行以下九个增强步骤:

步骤 1

main() 方法从命令行读取安全服务存根类的名称。随后将打开此文件,并将其读入到 StringBuffer对象。

步骤 2

main() 方法实例化名为 SecureServiceStubEnhancer 的类,并同时将步骤 1 中得到的 StringBuffer对象传递给 SecureServiceStubEnhancer 构造函数。SecureServiceStubEnhancer 类处理安全服务存根类,并生成其增强版本。SecureServiceStubEnhancer 类的工作方式在增强安全服务存根类中进行了介绍。

步骤 3

main() 方法调用 SecureSeviceStubEnhancer 类的 writeFile() 方法,该方法会在 EnhancedStubClasses 文件夹中以正确的文件名称保存安全服务存根的增强版本。

步骤 4

SecureServiceStubEnhancer 类还从安全服务存根类读取签名类的完全限定名称。您需要知道完全限定名称,以生成签名相关的存根类的增强版本。SecureServiceStubEnhancer 类公开 getSignatureClassName() 和 getSignaturePackageName() 方法,main() 方法将使用这两个方法来读取完全限定名称。

步骤 5

main() 方法使用步骤 4 中所得到的类名称读取签名类。它会将签名类加载到一个 StringBuffer 对象中。

步骤 6

main() 方法实例化另一个类 SignatureRelatedEnhancer,并同时将完全限定名称和签名类传递给 SignatureRelatedEnhancer 构造函数。SignatureRelatedEnhancer 类生成签名相关的类的增强版本。SignatureRelatedEnhancer 类在生成签名相关类与 Helper 类中进行了说明。

步骤 7

main() 方法调用 SignatureRelatedEnhancer 类的 writeFiles() 方法,其将使用正确的文件名称将所有签名相关的类保存在 EnhancedStubClasses 文件夹中。

步骤 8

main() 方法实例化 HelperClassesGenerator 类。HelperClassesGenerator(稍后在生成签名相关类和 Helper 类中讨论)将生成所有四个 Helper 类(CanonicalAuthorSHA1DigestCalculatorBase64Encoder 和 SignatureCalculator)。

步骤 9

main() 方法调用 HelperClassesGenerator 类的 writeFiles() 方法,该方法保存 Helper 类。

我们已经了解到,StubEnhancer 类使用三个类:SecureServiceStubEnhancerSignatureRelatedEnhancer 和 HelperClassesGenerator。下面我们将说明这三个类如何工作。




回页首


增强安全服务存根类

我们在本系列教程的第 2 部分执行了三个手动步骤来增强安全服务存根类:

  1. 我们在J2ME MIDlet 将如何使用 getSubjects() 中对存根构造函数进行了增强。
  2. 我们在增强安全服务存根类的数据成员中对存根类的数据成员进行了增强。
  3. 我们在增强 getSubjects() 方法实现中对存根类的服务方法进行了增强。

 

SecureServiceStubEnhancer 类具有三个方法:enhanceConstructor()enhanceDataMembers() 和 enhanceServiceMethods()SecureServiceStubEnhancer 构造函数将调用这三个方法,如清单 20 中所示:
清单 20. SecureServiceStubEnhancer 构造函数

                    
public SecureServiceStubEnhancer (StringBuffer secureServiceStub2BEnhanced) 
{
    if (secureServiceStubData != null) 
    {
        StringBuffer secureServiceStubWithEnhancedConstructor = 
            enhanceConstructor (secureServiceStub2BEnhanced);
        StringBuffer secureServiceStubWithEnhancedDataMembers = 
            enhanceDataMembers (secureServiceStubWithEnhancedConstructor);
        this.secureServiceStubData = 
            enhanceServiceMethods (secureServiceStubWithEnhancedDataMembers);            
    }
    else 
        showErrorMessage();
}//SecureServiceStubEnhancer

下面我们将说明 SecureServiceStubEnhancer 类的三个方法如何工作。




回页首


增强安全服务存根构造函数

清单 21 显示了 WSA 存根生成器工具生成的安全存根构造函数的原始形式:
清单 21. 安全服务存根构造函数的原始形式

                    
public SecureMobileEmailService_PortType_Stub() 
{
    _propertyNames = new String[] {ENDPOINT_ADDRESS_PROPERTY};
    _propertyValues = new Object[] {"http://localhost:8090"};
}

我们在第 2 部分清单 8 的步骤 1 中使用了存根构造函数的增强形式。其增强形式与清单 22 所示类似:
清单 22. 安全服务存根构造函数的增强形式

                    
public SecureMobileEmailService_PortType_Stub (
    String username, 
    String password) 
{
    this.username = username;
    this.password = password;
    _propertyNames = new String[] {ENDPOINT_ADDRESS_PROPERTY};
    _propertyValues = new Object[] {"http://localhost:8090"};
}

通过比较清单 21 和清单 22,可以发现只需要向构造函数添加用户名和密码参数就行了。

SecureServiceStubEnhancer 类的 enhanceConstructor() 方法(如清单 23 中所示)接受安全服务存根类作为参数,通过添加用户名和密码参数增强其构造函数,并返回该类的增强形式。
清单 23. enhanceConstructor() 方法

                    
private StringBuffer enhanceConstructor (StringBuffer secureServiceStub2BEnhanced) {
    
    String enhancedConstructorData = 
        createTwoParameterEnhancedConstructor(secureServiceStub2BEnhanced);
    int constructorIndex = 
        secureServiceStub2BEnhanced.indexOf("public "+stubClassName);
 
    return secureServiceStub2BEnhanced.insert(
        constructorIndex, 
        enhancedConstructorData);
}//enhanceConstructor





回页首


增强数据成员

在第 2 部分的增强安全服务存根类的数据成员中增强数据成员时,执行了两项任务:

  1. 增强 QName 对象定义。
  2. 了解了一些新的 Element 和 ComplexType 对象。

SecureServiceStubEnhancer 类有一个名为 enhanceDataMembers() 的方法(如清单 24 中所示),该方法执行以下两项任务:
清单 24. enhanceDataMembers() 方法

                    
private StringBuffer enhanceDataMembers (
    StringBuffer secureServiceStub2BEnhanced) 
{
    StringBuffer stubDataWithEnhancedQNameObjects = 
        fetchAndEnhanceQNameObjects (secureServiceStub2BEnhanced);
    return addNewComplexTypeAndElementObjects (stubDataWithEnhancedQNameObjects);
}//enhanceDataMembers

enhanceDataMembers() 方法将完整的安全服务存根类作为 StringBuffer 对象接受为参数。它使用两个分别名为 fetchAndEnhanceQNameObjects() 和 addNewComplexTypeAndElementObjects() 的 Helper 方法来增强安全服务存根类的安全成员部分。

fetchAndEnhanceQNameObjects() 增强 QName 对象定义,而 addNewComplexTypeAndElementstObjects() 方法将新的 ComplexType 和 Element 定义添加到安全服务存根类中。

接下来我们将说明两个 Helper 方法如何工作。




回页首


增强 QName 对象定义

安全服务存根类包含采用两个不同命名空间 URI 的 QName 对象。这两个命名空间 URI 是 http://www.w3.org/2000/09/xmldsig# 和 http://www.everydaywebservices.com/secureemailservice。第一个 URI 表示 XML 签名命名空间,而第二个 URI 表示所创建的安全服务。

安全服务类的原始形式在不使用任何命名空间声明的情况下创建了多个 QName 对象,如以下摘自第 2 部分清单 9 的代码所示:

protected static final QName 
    _qname_Signature = new QName(]
        "", 
        "Signature");

增强 QName 定义时,向签名相关的 QName 对象添加了签名命名空间定义。如以下摘自第 2 部分清单 10 的代码所示:

protected static final QName 
   _qname_Signature = new QName(
       "http://www.w3.org/2000/09/xmldsig#", 
       vSignature", 
       "ds");

可以将 QName 对象的原始形式与其增强形式进行一下比较。您会发现, QName 对象中的唯一增强是添加了签名命名空间 URI (http://www.w3.org/2000/09/xmldsig#) 和前缀 (ds)。

类似地,我们还在服务相关的 QName 对象中添加了安全电子邮件服务的命名空间 URI。例如,请看以下另一段摘自第 2 部分清单 10 的代码:

protected static final QName _qname_senderEmailAddress = 
    new QName(
        "http://www.everydaywebservices.com/secureemailservice",
        "senderEmailAddress", 
        "tns");

清单 25 实现了 fetchAndEnhanceQNameObjects() 方法,该方法将命名空间声明添加到 QName 对象:
清单 25. fetchAndEnhanceQNameObjects() 方法

                    
private StringBuffer fetchAndEnhanceQNameObjects (
    StringBuffer secureServiceStub2BEnhanced) 
{
    StringBuffer stubDataWithEnhancedSignatureRelatedQNameObjects =
        enhanceSignatureRelatedQNameObjects (secureServiceStub2BEnhanced);
    return enhanceServiceRelatedQNameObjects (
        stubDataWithEnhancedSignatureRelatedQNameObjects);
}//fetchAndEnhanceQNameObjects

fetchAndEnhanceQNameObjects() 方法接受安全服务存根类作为参数,将使用两个方法,即 enhanceSignatureRelatedQNameObjects() 和 enhanceServiceRelatedQNameObjects()enhanceSignatureRelatedQNameObjects() 增强签名相关的 QName 对象的定义。

类似地,enhanceServiceRelatedQNameObjects() 方法增强服务相关的 QName 对象的定义。




回页首


添加 ComplexType 和 Element 对象

现在让我们了解 addNewComplexTypeAndElementObjects() 方法(在前面的清单 24 中进行了调用)如何将新的 ComplexType 和 Element 对象添加到数据成员部分。

对于每个服务方法,WSA 存根生成器工具的数据成员部分的原始形式都包含一个对应的 ComplexType 对象。例如,清单 26 所示的名为 _complexType_getSubjects 的 ComplexType 对象:
清单 26. WSA 存根生成器生成的 ComplexType 对象

                    
ComplexType _complexType_getSubjects;
_complexType_getSubjects = new ComplexType();
_complexType_getSubjects.elements = new Element[2];
_complexType_getSubjects.elements[0] = _type_senderEmailAddress;
_complexType_getSubjects.elements[1] = _type_Signature;
_type_getSubjects = new Element(_qname_getSubjects, _complexType_getSubjects);

清单 26 中的 _complexType_getSubjects 对象包装第 2 部分清单 3 中所讨论的 getSubjects SOAP 请求中包含的元素。

在第 2 部分的增强安全服务存根类的数据成员中对数据成员部分进行增强时,在第 2 部分清单 10 的末尾处添加了另一个 ComplexType 对象 _complexType_getSubjects_enhanced,为了方便您参考,我们在此处的清单 27 中给出了相应的代码:
清单 27. 在第 2 部分清单 10 的末尾处添加的新 ComplexType 对象

                    
ComplexType _complexType_getSubjects_enhanced;
_complexType_getSubjects_enhanced = new ComplexType();
_complexType_getSubjects_enhanced.elements = new Element[1];
_complexType_getSubjects_enhanced.elements[0] = _type_senderEmailAddress;

可以看到,清单 26 的 _complexType_getSubjects 对象与清单 27 的 _complexType_getSubjects_enhanced 对象的唯一区别在于,Signature 元素的表示形式(如清单 26 的黑体所示)在清单 27 中没有。

您需要 _complexType_getSubjects_enhanced 对象(其中不包含 Signature 元素),因为并不希望在计算签名值时在 XML 数据中包含 Signature 元素。Signature 元素仅在根据实际 SOAP 消息计算了签名值之后才会出现在 SOAP 消息中。

还需要将 _complexType_getSubjects_enhanced 对象包装在 Element 对象内。可以在第 2 部分清单 10 的最后找到名为 _type_getSubjects_enhanced 的 Element 对象,为了方便您参考,我们在此处的清单 28 中给出了相应的代码。此 _type_getSubjects_enhanced Element 对象包装 _complexType_getSubjects_enhanced ComplexType 对象。
清单 28. _type_getSubjects_enhanced Element 对象

                    
Element _type_getSubjects_enhanced = 
    new Element(
       _qname_getSubjects, 
       _complexType_getSubjects_enhanced);

这意味着,在为安全服务存根类的每个服务方法添加一对新 ComplexType 和 Element 对象时,addNewComplexTypeAndElementObjects() 方法需要执行以下步骤:

  1. 提取安全服务存根类的数据成员部分。
  2. 对于安全服务存根类中的每个服务方法,找到 ComplexType 对象的原始形式(例如,getSubjects() 方法的 _complexType_getSubjects 对象)。
  3. 使用步骤 2 得到的 ComplexType 对象定义创建新 ComplexType 对象(即清单 27 的 _complexType_getSubjects_enhanced 对象),该对象并不包含 Signature 元素的表示形式。
  4. 将新 ComplexType 对象添加到数据成员部分。
  5. 创建新 Element 对象(即清单 28 中的 _type_getSubjects_enhanced 对象),以包装步骤 3 的新 ComplexType 对象。
  6. 将新 Element 对象添加到数据成员部分。

清单 29 显示了这六个步骤在 addNewComplexTypeAndElementObjects() 方法中的实现:
清单 29. addNewComplexTypeAndElementObjects() 方法

                    
private StringBuffer addNewComplexTypeAndElementObjects (
      StringBuffer secureServiceStubData) 
{
    /*** Step 1 ***/
    StringBuffer dataMembers = 
        new StringBuffer(fetchDataMembers(secureServiceStub2BEnhanced)); 
    for (int i = 0; i < fetchMethodNames (secureServiceStub2BEnhanced).size(); i ++)
    {
        String methodName = (String)methodNamesVector.get(i);
        /*** Step 2 ***/
        StringBuffer existingComplexType = 
            fetchComplexTypeDefinition(dataMembers, methodName);
        /*** Step 3 ***/
        StringBuffer newComplexType = 
            authorEnhancedComplexTypeDefinition (
                existingComplexType, 
                methodName
            );
        /*** Step 4 ***/ 
       addComplexTypeToDataMembers (
            dataMembers,
            newComplexType
        );
        /*** Step 5 ***/
        StringBuffer newElement = 
            authorEnhancedElementDefinition (
                methodName
            );
        /*** Step 6 ***/            
        addElementToDataMembers (
            dataMembers,
            newElement
        );
    }
    int startIndex = 
        secureServiceStub2BEnhanced.indexOf ("protected static final Element _type_");
    return  secureServiceStub2BEnhanced.replace (
               startIndex, 
               secureServiceStub2BEnhanced.length(), 
               dataMembers.toString()
            );
}//addNewComplexTypeAndElementObjects

我们对数据成员增强的讨论到此结束。接下来我们将讨论如何增强安全服务存根类的服务方法。




回页首


增强服务方法

我们在第 2 部分的增强 getSubjects() 方法实现中对安全服务存根类的 getSubjects() 方法进行了增强。接下来我们将了解如何在 SecureServiceStubEnhancer 类中实现相同的增强效果。

请看 enhanceServiceMethods()(如清单 30 中所示),该方法对安全服务存根类的所有服务方法进行增强(例如,在第 2 部分清单 6 中看到的 getSubjects() 方法)。
清单 30. enhanceServiceMethods() 方法

                    
private StringBuffer enhanceServiceMethods (
    StringBuffer secureServiceStub2BEnhanced) 
{
    StringBuffer enhancedStubData = 
        new StringBuffer(
            secureServiceStub2Benhanced.toString());
    for (int i = 0 ; i < fetchMethodNames(secureServiceStub2BEnhanced).size() ; i ++)
        enhancedStubData = 
            enhanceIndividualServiceMethod (
                (String)methodNamesVector.get(i), enhancedStubData)
           );
    return enhancedStubData;
}//enhanceServiceMethods

可以从清单 30 中看到,enhanceServiceMethods() 在循环中获取所有服务方法的名称,并随后为每个方法调用 enhanceIndividualServiceMethod() 方法。enhanceIndividualServiceMethod() 以一次一个的方式对服务进行增强。

接下来我们将说明 enhanceIndividualServiceMethod() 如何增强单个服务方法。




回页首


增强服务方法

我们以 getSubjects() 方法为例,以说明如何增强各个服务方法。

在第 2 部分的增强 getSubjects() 方法实现中对 getSubjects() 方法增强时,执行了三项任务:

  1. getSubjects() 方法的原始形式接受三个参数,其中之一是一个 Signature 对象。如清单 31 中所示:

    清单 31. getSubjects() 方法定义的原始形式
                                
    public java.lang.String[] getSubjects(
           java.lang.String senderEmailAddress
           secure.Signature signature ) 
           throws java.rmi.RemoteException 
    {
         // Method body
    }
    

    我们对 getSubjects() 方法定义进行了编辑,以使其不再接受 Signature 对象作为参数。getSubjects() 方法经过编辑的形式如清单 32 中所示:

    清单 32. 第 2 部分清单 11 中的 getSubjects() 方法的增强形式

                                
    public java.lang.String[] getSubjects(
        java.lang.String senderEmailAddress) 
        throws java.rmi.RemoteException {
        //Method body
      } 
    
  2. 如第 2 部分的清单 11 中的第一行增强代码所示,我们在 getSubjects() 方法内实例化了一个Signature 对象,为了方便您参考,我们在本教程的清单 33 中给出了对应的内容:

    清单 33. 在 getSubjects() 方法体内实例化 Signature 对象
                                
    public java.lang.String[] getSubjects(
        java.lang.String senderEmailAddress) 
        throws java.rmi.RemoteException {
        //First enhancement line of code
        secure.Signature signature = new secure.Signature (
            _type_getSubjects_enhanced,
            username,  
            password);
        //Rest of the getSubjects() method body
      }
    
  3. 如第 2 部分的清单 11 中的第二行增强代码所示,我们通过调用其 setApplicationData() 方法将应用程序数据传递给 Signature 对象,为了方便您参考,我们在本教程的清单 34 中给出了对应的内容:

    清单 34. 将应用程序数据传递给 Signature 对象
                                
    public java.lang.String[] getSubjects(
        java.lang.String senderEmailAddress) 
        throws java.rmi.RemoteException {
        //First enhancement line of code
        secure.Signature signature = new secure.Signature (
            _type_getSubjects_enhanced,
            username,  
            password);
        Object[] inputObject = new Object[2];
        inputObject[0] = senderEmailAddress;
        //Second enhancement line of code
        signature.setApplicationData(inputObject);
        //Rest of the getSubjects() method body
    }// getSubjects()
    

现在请看 enhanceIndividualServiceMethod()(如清单 35 中所示),该方法直接执行这三项任务:
清单 35. enhanceIndividualServiceMethod() 方法

                    
private StringBuffer enhanceIndividualServiceMethod (
    String methodName,
    StringBuffer secureServiceStub2BEnhanced) 
{
    
    //Tasks 1 and 2:
    StringBuffer enhancedStubData =
        enhanceMethodDefinitionAndAddSignatureObject (
             secureServiceStub2Benhanced, 
             methodName
        );
    //Task 3:
    return addSetApplicationDataMethodCall(
              enhancedStubData, 
              methodName
    );
}// enhanceIndividualServiceMethod

我们前面在增强安全服务存根类中已经看到了所有三个方法(enhanceConstructor()enhanceDataMembers() 和 enhanceServiceMethods())。我们对 SecureServiceStubEnhancer 类的讨论就到此结束了。可以在本教程的代码下载中找到 SecureServiceStubEnhancer 类的完整代码(请参见下载部分)。

在前面存根增强的步骤中,我们了解到存根增强器工具使用两个其他类,分别为 SignatureRelatedEnhancer 和 HelperClassesGenerator。接下来我们将讨论这两个类如何工作。




回页首


生成签名相关的 Helper 类

在第 2 部分的增强 Signature 存根类中,我们对 WSA 存根生成器工具生成的签名相关类(SignatureSignedInfo 和 Reference)进行了增强。签名相关类的增强形式中所编写的大部分代码并不依赖于所保护的 Web 服务。签名相关的类中只有很少一部分代码是特定于 Web 服务的。

例如,可以参考第 2 部分的清单 14,其中讨论了 Signature 类的增强形式。此处清单 36 提供了其相应内容: 
清单 36. Signature 类的增强形式

                    
public class Signature 
{
    private Element inputElement;
    private Object applicationData;
    private String username;
    private String password;
    protected secure.GetSubjectsSignatureSignedInfo signedInfo;
    protected java.lang.String signatureValue;
    protected secure.GetSubjectsSignatureKeyInfo keyInfo;
    public Signature (
        Element inputElement,
        String username,
        String password)
    {
        this.inputElement = inputElement;
        this.username = username;
        this.password = password;
    }
    public Signature ( 
        Element inputElement,
        Object inputObject,
        String username,
        String password)
    {
        this.inputElement = inputElement;
        this.applicationData = inputObject;
        this.username = username;
        this.password = password;
        populateDataMembers();
    }
    public void setApplicationData (Object inputObject) {
        this.applicationData = inputObject;
        populateDataMembers();
    }
    private void populateDataMembers() 
    {
        secure.GetSubjectsSignatureKeyInfo keyInfo =
            new secure.GetSubjectsSignatureKeyInfo (username);
        this.keyInfo = keyInfo;
        secure.GetSubjectsSignatureSignedInfo signedInfo =
            new secure.GetSubjectsSignatureSignedInfo (
                inputElement,
                applicationData);
        this.signedInfo = signedInfo;
 
        SignatureCalculator signatureCalculator =
            new SignatureCalculator(
                signedInfo, 
                username, 
                password);
        byte[] signatureValueInBinaryForm =
            signatureCalculator.getSignatureValue();
        Base64Encoder base64Encoder = new Base64Encoder();
        this.signatureValue =
            base64Encoder.getBase64EncodedValue(
               signatureValueInBinaryForm);
    }// populateDataMembers
    public secure.GetSubjectsSignatureSignedInfo getSignedInfo() {
        return signedInfo;
    }
    public secure.GetSubjectsSignatureKeyInfo getKeyInfo() {
        return keyInfo;
    }
    public java.lang.String getSignatureValue() {
        return signatureValue;
    }
}//Signature

可以看到清单 36 中的一些部分以粗体显示。这些是需要在存根增强过程中唯一需要动态生成的特定于 Web 服务的部分。Signature 类中的其他代码将始终不变(即不会因为 Web 服务不同而发生变化)。

因此,一种增强签名相关的类的简单策略是使用模板。模板包含签名相关类的所有增强代码,其中提供了 Web 服务特定代码的占位符。SignatureRelatedEnhancer 类可以直接将这些 Web 服务特定的代码填充到占位符中。

类似地,可以在 HelperClassesGenerator 类中使用同样的占位符与模板策略来生成所有四个 Helper 类(CanconicalAuthorSHA1DigestCalculatorBase64Encoder 和 SignatureCalculator)。

可以在本教程的源代码下载中找到 SignatureRelatedEnhancer 和 HelperClassesGenerator 类(请参见下载)。




回页首


测试存根增强器工具

现在要对刚刚构建的存根增强器工具进行测试。我们使用在第 1 部分的示例 Web 服务应用程序场景中介绍的酒店服务对存根增强器工具进行测试。

源代码下载中提供了酒店服务的 WSDL 文件。我们使用了 WSA 存根生成器工具来生成存根类的原始形式。可以在名为 HotelServiceOriginalStub 的文件夹中找到存根类的原始形式。

然后,我们使用存根增强器工具来生成酒店服务的存根类增强形式。可以在名为 HotelServiceEnhancedStub 的文件夹中找到存根类的增强形式。可以使用运行签名应用程序所述的测试安排来测试酒店服务的存根类增强形式。




回页首


构建基于 J2ME 的安全 SOAP 客户机,第 3 部分: 安全 Web 服务 API 存根类

构建存根增强器工具

第 7 页,共 11 页


对本教程的评价

帮助我们改进这些内容


总结

在本系列教程中,我们了解了 WSA 如何工作以及如何增强 WSA 存根类来建立 Web 服务的安全无线访问。另外还开发了一个 Java Card 应用程序并了解了如何使用 SATSA 来与 Java Card 应用程序通信。

此外,还了解了如何在 J2ME 中实现安全相关算法以及如何将安全算法集成到 WSA 中。本教程还介绍了一些测试安排的使用,可帮助您对 Web 服务客户机进行测试。

最后,我们开发了存根增强器工具,此工具可执行增强安全无线访问的 WSA 存根类所需的大部分难点工作。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值