Java Web 服务: Axis2 WS-Security 签名和加密

学习如何使用 Axis2 和 Rampart 对消息进行签名和加密

通过本文,您可以获得有关公开密匙加密基本原理的介绍,然后了解 WS-Security 如何通过结合公开-私有密匙对和秘密密匙,将这些原理应用于 SOAP 消息签名和加密。Dennis Sosnoski 将继续他的 Java Web 服务 系列,本期文章讨论 WS-Security 和 WS-SecurityPolicy 签名和加密特性,并提供了使用 Axis2 和 Rampart 的示例代码。

Dennis Sosnoski, 咨询师, Sosnoski Software Associates Ltd

Author photoDennis Sosnoski 是一名咨询师和培训师,专长是基于 Java 的 XML 和 Web 服务。他有 30 多年的专业软件开发经验,最近 10 年一直致力于服务器端 XML 和 Java 技术方面的工作。Dennis 是开源 JiBX XML Data Binding 框架及相关的 JiBX/WS Web 服务框架的首席开发人员,也是 Apache Axis2 Web 服务框架的提交者。他还是 JAX-WS 2.0 和 JAXB 2.0 规范的专家组成员之一。



2009 年 8 月 17 日

安全性对于使用 Web 服务交换业务数据至关重要。如果数据被第三方截取,或者欺骗性数据被当作有效数据接收,那么会引起严重的财务或法律后果。为 Web 服务(任何形式的数据交换)设计和实现应用程序的安全处理始终是可行的,但是这种方法比较冒险,因为即使是一个小错误和疏漏都会导致严重的安全漏洞。与其他更简单的数据交换相比,SOAP 的优势之一是它支持模块化扩展。自 SOAP 首次发布以来,安全性一直是扩展的主要关注点,促使了 WS-Security 和相关技术的标准化,允许针对每一个服务相应地配置安全性。

信息交换对安全性的需求通常包含三个方面:

  • 机密性:只有消息的目标接收者有权访问消息内容。
  • 完整性:接收到的消息没有发生任何修改。
  • 真实性:可以对消息的来源进行检验。

WS-Security 可以让您轻松地满足这三个方面的要求。在本文中,您将理解如何通过使用 Axis2 和 Rampart WS-Security 扩展实现这点。但是,我们首先将扼要概述一下公开密匙加密的原理 — 这是 WS-Security 的加密和签名特性的主要基础。

关于本系列

Web 服务构成了 Java™ 技术在企业计算应用中的关键部分。在 本系列文章 中,XML 和 Web 服务顾问 Dennis Sosnoski 介绍了对于使用 Web 服务的 Java 开发人员来说比较重要的主要框架和技术。通过跟随本系列的学习,您将了解到该领域的最新进展,并且知道如何使用它们来为您的编程项目提供帮助。

公开密匙加密

纵观历史,安全消息交换一直以来都基于某种形式的共享秘密。这个秘密可能采用代码的形式,交换的双方使用经过商定的内容替换词语或操作。或者可以是密码的形式,通过某种算法将某种文本转换为其他文本。秘密甚至可以采用其他形式,比如对于可能访问消息的其他方未知的语言。共享秘密可以使消息交换非常安全。但是,如果其他方发现了这个秘密,那么消息交换就会泄漏,并会为消息交换方带来潜在的破坏性结果。(比如,想一想二战时期 Enigma 和 German 的军事通信)。

公开密匙加密是一种与其他加密有着本质区别的安全方法,它不需要共享的秘密。它所基于的理念是数学上的 “trap-door” 函数,它可以沿着一个方向轻松地进行计算,但是很难从相反的方向进行计算。比如,可以很容易找到两个质数的乘积(如果使用电脑的话,甚至可以是非常大的质数),但是很难通过分析乘积来找到原始的两个因数。如果围绕某个函数的简单方向构造一个加密算法,那么想要破解加密的任何人都需要从相反的方向来解密。和任何精心挑选的算法一样,破解加密的尝试将非常困难,以至于无法在可以对消息交换产生威胁的时间期限内完成(至少要等到有人开发了可用的量子计算机,或真正有效的特异功能)。

使用了公开密匙加密后,希望接收已加密消息的一方将创建一对密匙值。每个密匙值都可以单独用于加密消息,但是不能用于解密由它本身加密的消息。相反,这对密匙值的另外一个密匙必须用于解密。只要密匙的所有者将其中的一个密匙保密,另一个密匙就可以公开给他人。任何访问这个公开密匙的人都可以用它加密消息,而只有密匙所有者才能够对消息解密。由于使用了不同的密匙进行加密和解密消息,因此这种形式的加密被称为不对称加密

消息签名

当使用您的公开密匙对消息进行加密,那么只有您(即私有密匙的持有者)才能够解密消息。这可以确保机密性,满足了安全消息交换三个方面的其中一个方面。但是也可以使用您的私有 密匙来加密消息,这样做之后,任何具有您的公开 密匙的人都可以解密这个消息。乍看上去这似乎没什么用 — 任何人都可以读取的加密消息有什么用?— 但是这种方法可以很好地充当一种检验真实性的机制。据称从您那里收到已加密消息的人可以使用您的公共密匙来解密消息并与预期值进行比较。如果值匹配的话,那么他们就知道是 创建了这条消息。

在实际中,消息签名的过程不仅仅是使用私有密匙加密消息。首先,您需要使用某种方法为解密消息确立预期值。这通常使用另一种经过变化的数学 trap-door 函数完成:这是一个散列(hash)函数,它易于计算但难以复制(即很难在不修改该消息的散列值的情况下对消息进行修改,或者很难找到具有相同散列值的另一个消息作为提供的消息)。使用这种散列函数,可以为希望签名的消息生成散列值(在安全讨论中通常称为一个摘要(digest)),然后使用私有密匙加密该摘要并使用消息发送已加密的摘要值。任何接收此消息的人都可以对该消息使用相同的散列算法,然后使用您的公开密匙解密附带的已加密摘要,并比较两者的值。如果值匹配的话,那么接收方可以确定(在当前技术的范围内,并认为您一直对您的私有密匙保密)该消息是由您发送的。

在处理 XML 时,消息签名过程还涉及到另一个步骤。XML 消息以文本形式表示,但是文本表示的某些方面被 XML 认为是无关的(比如元素上的属性的顺序,或元素开始和结束标记内使用的空白)。由于这个与文本表示有关的问题,因此 W3C(XML 规范的所有者)决定在计算摘要值之前,将 XML 消息转换为一个标准的文本形式(见 参考资料)。一些标准化算法也得到定义,可以用于 XML 消息。只要消息交换的双方都同意使用相同的算法,那么具体使用哪种算法并不会产生什么影响。

使用经过签名的消息摘要可以同时保证消息完整性(因为对消息的修改将会改变摘要值)和真实性(因为您的私有密匙被用于加密摘要)。由于使用公开密匙的加密可以确保消息的私密性,因此消息交换安全性的所有主要方面都可以通过使用一个公开-私有密匙对涵盖。当然,对于一个密匙对,消息交换中只有一方是安全的 — 但是如果交换的另外一方也具有自己的公开/私有密匙对,那么就可以为消息交换的双方提供充分的安全性。

证书

公开-私有密匙对可以同时用于对双方之间交换的信息进行加密和签名,假设交换双方都可以访问对方的公开密匙。这产生了一个问题,如何在保证安全性的情况下获得公开密匙。实现此目的的方法有许多种,其中最常用的是对公开密匙使用一个或多个可靠的第三方证明。数字证书 就是一种以第三方证明的形式提供公开密匙的机制。

数字证书实质上就是一个包装公开密匙的包装器,其中包含了用于密匙所有方的识别信息。然后由一个可信的第三方签名被封装的内容,并且签名被包括到证书中。可信的第三方通过使用其自己的签名发出证书,为公开密匙和识别信息做证明。当然,这又摆出了如何确立可信第三方的身份的自举问题。通常的解决方法是将某些称为发证机关(issuing authority)的可信第三方的证书硬编码到软件(比如 JVM)中。

除了这里描述的基本内容外,数字证书还涉及到很多其他内容,包括由于发生错误或泄漏私有密匙而需要撤回发出的证书,证书的有效期限,以及指定证书既定用途的扩展。参见 参考资料 获得有关数字证书和公开密匙加密的更多信息的链接。还可以查阅文档,获得 JDK 安装中附带的 keytool 安全工具。keytool 文档很好地介绍了证书结构和处理,以及 keystores(稍后讨论)和其他安全性内容。您还将在本文后面看到一个使用 keytool 的示例。

Keystores

大多数 Java 安全代码都使用 keystore 来处理私有密匙和数字证书。一个 keystore 实际上就是一个以加密形式包含密匙和证书条目的文件。需要使用密码才能访问 keystore。keystore 中的每个私有密匙会被再一次加密,需要额外的密码才能保持密匙的安全性。使用 keystore 和私有密匙的任何软件都需要在运行时保持 keystore 和私有密匙密码可用。这限制了由这些密码提供的安全性(因为任何访问源代码的人都可以了解到密码的载入方式)。因此您需要维护在其中托管了软件以及任何系统备份的系统的物理安全性。并且,要保护私有密匙的安全性,只能将 keystore 和密码放到该系统和系统备份中。

秘密密匙和对称加密

尽管使用非对称加密的公开密匙加密构成了许多有用 WS-Security 特性的基础,但是老式的秘密密匙加密仍然发挥着重要的作用。要实现相同程度的保护,非对称加密算法往往要比基于秘密密匙(同一个密匙同时用于加密和解密,意味着这个密匙必须始终保密)的对称算法涉及更多的计算量。由于这个原因,经常将这两种技术结合使用:高开销的非对称加密用于保障秘密密匙交换的安全性,秘密密匙然后又可用于低开销的对称加密。本文后面介绍 WS-Security 消息加密时,您将看到一个这样的示例。


设置

本文使用了与 “Axis2 WS-Security 基础” 相同的示例应用程序,该应用程序展现了如何使用 Axis2 和 Rampart 实现 WS-Security UsernameToken。但是,需要作出一些修改来支持使用 WS-Security 公开密匙加密特性,因此本文附带了一个单独的代码包(见 下载 )。

示例代码的根目录为 jws05code。该目录包含了以下内容:

  • 一个 Ant build.xml 文件
  • 一个 Ant build.properties 文件,它配置了示例应用程序的操作
  • library.wsdl 文件为示例应用程序提供了服务定义
  • log4j.properties 文件用于配置客户端登录
  • 一些属性定义 XML 文件(所有名为 XXX-policy-client.xml 或 XXX-policy-server.xml 的文件)

在开始使用示例代码前,需要完成以下操作:

  1. 编辑 build.properties 文件,设置 Axis2 安装的路径。
  2. 确保 Axis2 安装已经通过 Rampart 代码进行了更新,如 “Axis2 WS-Security 基础” 的 安装 Rampart 一节所述。(一种好方法就是查看 repository/modules 目录中的 rampart-x.y.mar 模块文件)。
  3. org.bouncycastle.jce.provider.BouncyCastleProvider Bouncy Castle 安全提供者(用于示例代码中使用的公开密匙加密特性)添加到 JVM 安全配置中(lib/security/java.security 文件)(见 参考资料)。
  4. 将 Bouncy Castle JAR(名为 bcprov-jdkxx-vvv.jar,其中 xx 表示 Java 版本,vvv 表示 Bouncy Castle 代码版本)同时 添加到 Axis2 安装的 lib 目录和 Axis2 服务器应用程序的 WEB-INF/lib 目录。

现在,您已经准备好构建示例应用程序并尝试下一节将介绍的安全示例。


对消息进行签名

签名要比 “Axis2 WS-Security 基础” 中的 UsernameToken 示例用到更多规范。您需要:

  • 识别用于在每个方向上创建签名的私有/公开密匙对,提供用于访问 keystore 和私有密匙的密码。
  • 指定用于 XML 标准化、摘要生成和实际签名的算法集。
  • 指定将消息的哪些部分包含到签名中。

这些信息中的一部分被作为配置数据处理,被嵌入到服务的 WS-SecurityPolicy 文档中。其他部分被包含到运行时消息交换中。

清单 1 展示了一个 WS-Policy 文档,用于将 Axis2 客户机配置为对消息使用签名。(清单 1 进行了编辑,以符合页面的宽度。在示例代码中可以查看 sign-policy-client.xml 的完整文本)。

清单 1. 用于签名(客户机)的 WS-Policy/WS-SecurityPolicy
<!-- Client policy for signing all messages, with certificates included in each
 message -->
<wsp:Policy wsu:Id="SignOnly"
    xmlns:wsu="http://.../oasis-200401-wss-wssecurity-utility-1.0.xsd"
    xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
  <wsp:ExactlyOne>
    <wsp:All>
      <sp:AsymmetricBinding
          xmlns:sp="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702">
        <wsp:Policy>
          <sp:InitiatorToken>
            <wsp:Policy>
              <sp:X509Token
                  sp:IncludeToken="http://.../IncludeToken/AlwaysToRecipient"/>
            </wsp:Policy>
          </sp:InitiatorToken>
          <sp:RecipientToken>
            <wsp:Policy>
              <sp:X509Token
                  sp:IncludeToken="http://.../IncludeToken/AlwaysToInitiator"/>
            </wsp:Policy>
          </sp:RecipientToken>
          <sp:AlgorithmSuite>
            <wsp:Policy>
              <sp:TripleDesRsa15/>
            </wsp:Policy>
          </sp:AlgorithmSuite>
          <sp:Layout>
            <wsp:Policy>
              <sp:Strict/>
            </wsp:Policy>
          </sp:Layout>
          <sp:IncludeTimestamp/>
          <sp:OnlySignEntireHeadersAndBody/>
        </wsp:Policy>
      </sp:AsymmetricBinding>
      <sp:SignedParts xmlns:sp="http://.../ws-securitypolicy/200702">
        <sp:Body/>
      </sp:SignedParts>

      <ramp:RampartConfig xmlns:ramp="http://ws.apache.org/rampart/policy">
        <ramp:user>clientkey</ramp:user>
        <ramp:passwordCallbackClass
            >com.sosnoski.ws.library.adb.PWCBHandler</ramp:passwordCallbackClass>

        <ramp:signatureCrypto>
          <ramp:crypto provider="org.apache.ws.security.components.crypto.Merlin">
            <ramp:property name="org.apache.ws.security.crypto.merlin.keystore.type"
                >JKS</ramp:property>
            <ramp:property name="org.apache.ws.security.crypto.merlin.file"
                >client.keystore</ramp:property>
            <ramp:property
                name="org.apache.ws.security.crypto.merlin.keystore.password"
                >nosecret</ramp:property>
          </ramp:crypto>
        </ramp:signatureCrypto>

      </ramp:RampartConfig>

    </wsp:All>
  </wsp:ExactlyOne>
</wsp:Policy>

清单 1 中,策略中的 <sp:AsymmetricBinding> 元素给出了在消息交换中使用公开密匙加密的基本配置信息。在这个元素内,使用了一些嵌套元素来提供配置的细节。<sp:InitiatorToken> 识别出用于对由客户机(发起者)发往服务器(接收者)的消息进行签名的密匙对,在本例中指定这个公开密匙将采用 X.509 证书的形式并连同来自客户机的消息一起发送(sp:IncludeToken=".../AlwaysToRecipient")。<sp:RecipientToken> 识别用于对从服务器返回给客户机的消息进行签名的密匙对,同样使用的是 X.509 证书,并且证书被包含在来自服务器的消息中(sp:IncludeToken=".../AlwaysToInitiator")。

<sp:AlgorithmSuite> 元素识别用于进行签名的一组算法。<sp:IncludeTimestamp> 提供了用于每个消息的时间戳(用于保护服务免受重播式攻击,在这种攻击下,消息在传输中被捕获并被重新提交,从而扰乱或中断服务)。<sp:OnlySignEntireHeadersAndBody> 元素指定签名将在消息的整个头部或消息体中完成,而不是在某些嵌套的元素(另一种安全措施,防止发生重写消息的攻击类型)中完成。<sp:SignedParts> 元素识别将要被签名的消息部分 — 在本例中为 SOAP 消息 Body

清单 1 中的 WS-Policy 文档的最后一部分提供了特定于 Rampart 的配置信息。这是 “Axis2 WS-Security 基础” 中使用的 Rampart 配置的更加复杂的版本,这一次包括了一个 <ramp:user> 元素来识别将用于消息签名的密匙,以及一个 <ramp:signatureCrypto> 元素,用于配置包含客户机私有密匙和服务器证书的 keystore。被引用的 keystore 文件必须在运行时提供给类路径。在示例应用程序中,keystore 文件在构建期间被复制到 client/bin 目录中。

密码回调类与 “Axis2 WS-Security 基础” 中使用的类稍有不同。在那篇文章中,只需要在服务器使用密码回调,并且只需要检验(用于明文式 UsernameToken)或设置(用于散列式 UsernameToken)与特定用户名匹配的密码。对于本文使用的公开密匙加密,回调必须提供用于保护 keystore 内的私有密匙的密码。并且需要对客户机(为客户机私有密匙提供密码)和服务器(为服务器私有密匙提供密码)使用单独的回调。清单 2 展示了回调的客户端版本:

清单 2. 客户机密码回调
/**
 * Simple password callback handler. This just checks if the password for the private key
 * is being requested, and if so sets that value.
 */
public class PWCBHandler implements CallbackHandler
{
    public void handle(Callback[] callbacks) throws IOException {
        for (int i = 0; i < callbacks.length; i++) {
            WSPasswordCallback pwcb = (WSPasswordCallback)callbacks[i];
            String id = pwcb.getIdentifer();
            int usage = pwcb.getUsage();
            if (usage == WSPasswordCallback.DECRYPT ||
                usage == WSPasswordCallback.SIGNATURE) {

                // used to retrieve password for private key
                if ("clientkey".equals(id)) {
                    pwcb.setPassword("clientpass");
                }

            }
        }
    }
}

清单 2 中的回调旨在支持使用同一个公开-私有密匙对进行签名和解密,因此将在两种情况下进行检查并对任意一种情况返回相同的密码。

清单 1 中的 WS-Policy 用于客户机,与此相对应的是用于服务器的 WS-Policy(示例代码中的 sign-policy-server.xml),两者只在 Rampart 配置的细节方面略有不同。同样,清单 2 密码回调类的服务器版本只在标识符值和返回的密码方面有所差异。

运行示例应用程序

build.properties 文件引用了将由示例应用程序使用的 client.policyserver.policy 文件。在文件的补充版中分别被设置为 sign-policy-client.xml 和 sign-policy-server.xml,因此您应当只需要构建应用程序。通过将控制台打开到 jws05code 目录并输入 ant,您可以使用 Ant 完成任务。如果所有内容均配置正确,那么应当在 jws05code 目录中得到一个 library-signencr.aar Axis2 服务归档文件。通过使用 Axis2 Administration 页面上传 .aar 文件,将该服务部署到您的 Axis2 服务器安装中,然后通过在控制台输入 ant run 来试用客户机。如果所有内容都正确设置,那么应当看到如图 1 所示的输出:

图 1. 运行应用程序的控制台输出
运行应用程序的控制台输出

要查看消息中的实际 WS-Security 信息,需要使用 TCPMon 之类的工具(见 参考资料)。首先设置 TCPMon 并在一个端口上接受来自客户机的连接,该连接随后转发给运行在另一个端口上的服务器(或另一个主机)。随后可以编辑 build.properties 文件并将 host-port 值修改为 TCPMon 的侦听端口。如果在控制台中再一次输入 ant run,应当会看到被交换的消息。清单 3 展示了一个样例客户机消息捕捉:

清单 3. 从客户机发送给服务器的第一条消息
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Header>
    <wsse:Security xmlns:wsse=".../oasis-200401-wss-wssecurity-secext-1.0.xsd"
        soapenv:mustUnderstand="1">
      <wsu:Timestamp xmlns:wsu=".../oasis-200401-wss-wssecurity-utility-1.0.xsd"
          wsu:Id="Timestamp-3753023">
        <wsu:Created>2009-04-18T19:26:14.779Z</wsu:Created>
        <wsu:Expires>2009-04-18T19:31:14.779Z</wsu:Expires>
      </wsu:Timestamp>
      <wsse:BinarySecurityToken
          xmlns:wsu=".../oasis-200401-wss-wssecurity-utility-1.0.xsd"
          EncodingType=".../oasis-200401-wss-soap-message-security-1.0#Base64Binary"
          ValueType=".../oasis-200401-wss-x509-token-profile-1.0#X509v1"
          wsu:Id="CertId-2650016">MIICoDC...0n33w==</wsse:BinarySecurityToken>
      <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
          Id="Signature-29086271">
        <ds:SignedInfo>
          <ds:CanonicalizationMethod
              Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
          <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
          <ds:Reference URI="#Id-14306161">
            <ds:Transforms>
              <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            </ds:Transforms>
            <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
            <ds:DigestValue>SiU8LTnBL10/mDCPTFETs+ZNL3c=</ds:DigestValue>
          </ds:Reference>
          <ds:Reference URI="#Timestamp-3753023">
            <ds:Transforms>
              <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            </ds:Transforms>
            <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
            <ds:DigestValue>2YopLipLgBFJi5Xdgz+harM9hO0=</ds:DigestValue>
          </ds:Reference>
        </ds:SignedInfo>
        <ds:SignatureValue>TnUQtz...VUpZcm3Nk=</ds:SignatureValue>
        <ds:KeyInfo Id="KeyId-3932167">
          <wsse:SecurityTokenReference
              xmlns:wsu=".../oasis-200401-wss-wssecurity-utility-1.0.xsd"
              wsu:Id="STRId-25616143">
            <wsse:Reference URI="#CertId-2650016"
              ValueType=".../oasis-200401-wss-x509-token-profile-1.0#X509v1"/>
          </wsse:SecurityTokenReference>
        </ds:KeyInfo>
      </ds:Signature>
    </wsse:Security>
  </soapenv:Header>
  <soapenv:Body
      xmlns:wsu=".../oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="Id-14306161">
    <ns2:getBook xmlns:ns2="http://ws.sosnoski.com/library/wsdl">
      <ns2:isbn>0061020052</ns2:isbn>
    </ns2:getBook>
  </soapenv:Body>
</soapenv:Envelope>

SOAP 消息中的 <wsse:Security> 头部保存有所有运行时安全配置信息和签名数据。第一个项是 <wsu:Timestamp>,在 WS-SecurityPolicy 配置中被请求。时间戳包含了两个时间值:创建时间和过期时间。这两个值在本例中为 5 分钟的时间间隔,这是 Rampart 的默认设置。(可以在 Rampart 策略配置中修改这两个值)。这两个值之间的时间量稍微有些随意,但是 5 分钟是一个比较合理的值 — 足够处理客户机与服务器之间存在的适当的时钟偏移(系统时钟之间的差异),但是同时也能够限制利用消息进行重播攻击。在时间戳之后,安全头部的下一个项是 <wsse:BinarySecurityToken>。这个安全令牌是一个客户机证书,采用 base64 二进制编码形式。

安全头部中的第三个项是一个 <ds:Signature> 块,其中包含三个子元素。第一个子元素 <ds:SignedInfo> 是直接进行签名的消息部分。<ds:SignedInfo> 中的第一个子元素标识用于自身标准化和签名的算法。后面是一个 <ds:Reference> 子元素,针对签名中包含的每个消息组件。每个子 <ds:Reference> 元素根据标识符引用一个特定消息组件并提供应用到该组件的标准化和摘要算法,以及生成的摘要值。<ds:SignedInfo> 的其余子元素提供了实际的签名值和一个用于检验签名的公开密匙的引用(在本例中为此前在头部中的 <wsse:BinarySecurityToken> 中包含的证书,由 wsu:Id="CertId-2650016" 标识)。


加密和签名消息

向已签名的消息进行加密非常简单,只需要向策略添加一个 <sp:EncryptedParts> 元素来标识将被加密的组件,以及添加一些额外的 Rampart 配置信息。清单 4 展示了针对这一用途的策略的客户机版本(为了适合页面宽度再次进行了编辑 — 查看示例代码中的 signencr-policy-client.xml 文件获得完整文本),其中在 清单 1 策略的基础上新添的内容以粗体显示:

清单 4. 用于签名和加密(客户机)的 WS-Policy/WS-SecurityPolicy
<!-- Client policy for first signing and then encrypting all messages, with the
 certificate included in the message from client to server but only a thumbprint
 on messages from the server to the client. -->
<wsp:Policy wsu:Id="SignEncr"
    xmlns:wsu="http://.../oasis-200401-wss-wssecurity-utility-1.0.xsd"
    xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
  <wsp:ExactlyOne>
    <wsp:All>
      <sp:AsymmetricBinding
          xmlns:sp="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702">
        <wsp:Policy>
          <sp:InitiatorToken>
            <wsp:Policy>
              <sp:X509Token
                  sp:IncludeToken="http://.../IncludeToken/AlwaysToRecipient"/>
            </wsp:Policy>
          </sp:InitiatorToken>
          <sp:RecipientToken>
            <wsp:Policy>
              <sp:X509Token
                  sp:IncludeToken="http://.../IncludeToken/Never">
                <wsp:Policy>
                  <sp:RequireThumbprintReference/>
                </wsp:Policy>
              </sp:X509Token>
            </wsp:Policy>
          </sp:RecipientToken>
          <sp:AlgorithmSuite>
            <wsp:Policy>
              <sp:TripleDesRsa15/>
            </wsp:Policy>
          </sp:AlgorithmSuite>
          <sp:Layout>
            <wsp:Policy>
              <sp:Strict/>
            </wsp:Policy>
          </sp:Layout>
          <sp:IncludeTimestamp/>
          <sp:OnlySignEntireHeadersAndBody/>
        </wsp:Policy>
      </sp:AsymmetricBinding>
      <sp:SignedParts xmlns:sp="http://.../200702">
        <sp:Body/>
      </sp:SignedParts>
      <sp:EncryptedParts xmlns:sp="http://.../200702">
        <sp:Body/>
      </sp:EncryptedParts>

      <ramp:RampartConfig xmlns:ramp="http://ws.apache.org/rampart/policy">
        <ramp:user>clientkey</ramp:user>
        <ramp:encryptionUser>serverkey</ramp:encryptionUser>
        <ramp:passwordCallbackClass
            >com.sosnoski.ws.library.adb.PWCBHandler</ramp:passwordCallbackClass>

        <ramp:signatureCrypto>
          <ramp:crypto provider="org.apache.ws.security.components.crypto.Merlin">
            <ramp:property name="org.apache.ws.security.crypto.merlin.keystore.type"
                >JKS</ramp:property>
            <ramp:property name="org.apache.ws.security.crypto.merlin.file"
                >client.keystore</ramp:property>
            <ramp:property
                name="org.apache.ws.security.crypto.merlin.keystore.password"
                >nosecret</ramp:property>
          </ramp:crypto>
        </ramp:signatureCrypto>

        <ramp:encryptionCrypto>
          <ramp:crypto provider="org.apache.ws.security.components.crypto.Merlin">
            <ramp:property name="org.apache.ws.security.crypto.merlin.keystore.type"
                >JKS</ramp:property>
            <ramp:property name="org.apache.ws.security.crypto.merlin.file"
                >client.keystore</ramp:property>
            <ramp:property
                name="org.apache.ws.security.crypto.merlin.keystore.password"
                >nosecret</ramp:property>
          </ramp:crypto>
        </ramp:encryptionCrypto>

      </ramp:RampartConfig>

    </wsp:All>
  </wsp:ExactlyOne>
</wsp:Policy>

仅使用加密不行吗?

如果可以提供一个仅对 Axis2 和 Rampart 使用加密的示例会很不错,但是这个功能在 Axis2 1.4(及更早版本)中被去掉了。1.5 发行版提供了一个代码补丁。如果您使用的是 Axis2 1.5 或更高版本(以及相应的 Rampart 发行版),那么可以尝试使用 encr-policy-client.xml 和 encr-policy-server.xml 策略文件来在不使用任何签名的情况下对每个消息体进行加密。

使用加密不需要 用到 清单 4 策略中的第一处修改,但这是个好主意。在使用加密时,客户机在发送初始请求时需要用到服务器证书(因为要使用证书中的服务器公开密匙进行加密)。由于客户机需要具有服务器证书,并不表示要将服务器证书发送给客户机。清单 4 策略中经过修改的 <sp:RecipientToken> 反映了这个用例,表示不应发送证书(sp:IncludeToken=".../Never"),而应当使用 thumbprint reference(实际上就是一个证书散列)。thumbprint reference 要比完整的证书更加简洁,因此使用引用可以减小消息的大小和处理开销。

实际实现加密的修改是新添的 <sp:EncryptedParts> 元素。这个元素表示将要使用加密,而内容 <sp:Body> 元素表示需要对消息中的 SOAP 消息体进行加密。

清单 4 中新添的 Rampart 配置信息包含一个 <ramp:encryptionUser> 元素和一个 <ramp:encryptionCrypto> 元素,前者为用于加密信息的公开密匙(即证书)分配一个别名,后者指出如何访问包含证书的 keystore。在示例应用程序中,对用于签名的私有密匙和用于加密的公开密匙使用了相同的 keystore,因此 <ramp:encryptionCrypto> 元素只不过是对现有的 <ramp:signatureCrypto> 元素进行重命名后的副本。

在运行时,Rampart 需要获得用于保护私有密匙的密码,这个私有密匙用于对已加密数据进行解密。前面的密码回调用于获得私有密匙密码(用于实现签名),如 清单 2 所示,还提供了用于解密的密码,因此不需要做任何修改。

运行示例应用程序

要尝试运行使用了签名和加密的示例应用程序,首先需要编辑 build.properties 文件。将客户机策略行修改为 client.policy=signencr-policy-client.xml,将服务器策略修改为 server-policy=signencr-policy-server.xml。随后可以通过运行 ant 重新构建应用程序,将生成的 library-signencr.aar 文件部署到 Axis2 安装中,然后运行 ant run 来进行尝试。

清单 5 显示了依次使用签名和加密后的请求-消息捕捉,与 清单 3 中以粗体显示的服务器版本有着明显的不同:

清单 5. 使用了签名和加密的消息
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
  <soapenv:Header>
    <wsse:Security xmlns:wsse=".../oasis-200401-wss-wssecurity-secext-1.0.xsd"
        soapenv:mustUnderstand="1">
      <wsu:Timestamp xmlns:wsu=".../oasis-200401-wss-wssecurity-utility-1.0.xsd"
          wsu:Id="Timestamp-4067003">
        <wsu:Created>2009-04-21T23:15:47.701Z</wsu:Created>
        <wsu:Expires>2009-04-21T23:20:47.701Z</wsu:Expires>
      </wsu:Timestamp>
      <xenc:EncryptedKey Id="EncKeyId-urn:uuid:6E12E251E439C034FA12403557497352">
        <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
          <wsse:SecurityTokenReference>
            <wsse:KeyIdentifier
                EncodingType="http://...-wss-soap-message-security-1.0#Base64Binary"
                ValueType="http://.../oasis-wss-soap-message-security-1.1#ThumbprintSHA1"
                >uYn3PK2wXheN2lLZr4n2mJjoWE0=</wsse:KeyIdentifier>
          </wsse:SecurityTokenReference>
        </ds:KeyInfo>
        <xenc:CipherData>
          <xenc:CipherValue>OBUcMI...OIPQEUQaxkZps=</xenc:CipherValue>
        </xenc:CipherData>
        <xenc:ReferenceList>
          <xenc:DataReference URI="#EncDataId-28290629"/>
        </xenc:ReferenceList>
      </xenc:EncryptedKey>
      <wsse:BinarySecurityToken
          xmlns:wsu="http://.../oasis-200401-wss-wssecurity-utility-1.0.xsd"
          EncodingType="http://...-wss-soap-message-security-1.0#Base64Binary"
          ValueType="http://.../oasis-200401-wss-x509-token-profile-1.0#X509v1"
          wsu:Id="CertId-2650016">MIICo...QUBCPx+m8/0n33w==</wsse:BinarySecurityToken>
      <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
          Id="Signature-12818976">
        <ds:SignedInfo>
          <ds:CanonicalizationMethod
              Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
          <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
          <ds:Reference URI="#Id-28290629">
            <ds:Transforms>
              <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            </ds:Transforms>
            <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
            <ds:DigestValue>5RQy7La+tL2kyz/ae1Z8Eqw2qiI=</ds:DigestValue>
          </ds:Reference>
          <ds:Reference URI="#Timestamp-4067003">
            <ds:Transforms>
              <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            </ds:Transforms>
            <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
            <ds:DigestValue>GAt/gC/4mPbnKcfahUW0aWE43Y0=</ds:DigestValue>
          </ds:Reference>
        </ds:SignedInfo>
        <ds:SignatureValue>DhamMx...+Umrnims=</ds:SignatureValue>
        <ds:KeyInfo Id="KeyId-31999426">
          <wsse:SecurityTokenReference
              xmlns:wsu=".../oasis-200401-wss-wssecurity-utility-1.0.xsd"
              wsu:Id="STRId-19267322">
            <wsse:Reference URI="#CertId-2650016"
                ValueType=".../oasis-200401-wss-x509-token-profile-1.0#X509v1"/>
          </wsse:SecurityTokenReference>
        </ds:KeyInfo>
      </ds:Signature>
    </wsse:Security>
  </soapenv:Header>
  <soapenv:Body
      xmlns:wsu=".../oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="Id-28290629">
    <xenc:EncryptedData Id="EncDataId-28290629"
        Type="http://www.w3.org/2001/04/xmlenc#Content">
      <xenc:EncryptionMethod
          Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc"/>
      <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <wsse:SecurityTokenReference
            xmlns:wsse="http://.../oasis-200401-wss-wssecurity-secext-1.0.xsd">
          <wsse:Reference URI="#EncKeyId-urn:uuid:6E12E251E439C034FA12403557497352"/>
        </wsse:SecurityTokenReference>
      </ds:KeyInfo>
      <xenc:CipherData>
        <xenc:CipherValue>k9IzAEG...3jBmonCsk=</xenc:CipherValue>
      </xenc:CipherData>
    </xenc:EncryptedData>
  </soapenv:Body>
</soapenv:Envelope>

第一个不同是在安全头部中出现了 <xenc:EncryptedKey> 元素。这个元素的内容提供了一个加密了的秘密密匙,使用了服务器的公开密匙执行加密。第二个不同之处在于实际的 SOAP Body 内容,它被替换为 <xenc:EncryptedData> 元素。这个已加密的数据从安全头部引用 <xenc:EncryptedKey> 值作为在 Body 内容中用于对称加密的密匙。


使用自己的自签名证书

要获得由权威认证机构签名的官方数字证书,需要生成一个公开-私有密匙对并使用公开密匙生成证书请求。然后将证书请求发送给您选择的机构并支付费用。证书机构将验证您的身份(保证整个过程的完整性的重要一步,但是可能会出现错误),并发出具有其签名的证书。

如果用于测试或内部用途,可以生成您自己的自签名证书。本文的示例代码使用了两个这种自签名证书,一个用于客户机,一个用于服务器。用于客户端的 client.keystore keystore 包含了客户机的私有密匙和证书,以及服务器证书(必须存储在客户机,这样在用于签名时,无需证书机构的签名就可以被作为有效证书接受,并且也可以直接用于加密)。用于服务器端的 server.keystore keystore 包含了服务器的私有密匙和证书,以及客户机证书(这样证书就可以作为有效证书被接受)。

您可以生成自己的私有密匙和自签名证书,并使用自己生成的密匙-证书对替换下载中的相应内容。要使用 JDK 附带的 keytool 程序实现替换,请打开控制台并输入以下命令代码(在这里将代码分解成多个行以适应页宽;您在输入时必须作为单一行输入):

keytool -genkey -alias serverkey -keypass serverpass -keyalg RSA -sigalg SHA1withRSA 
   -keystore server.keystore -storepass nosecret

上面的命令生成了别名为 serverkey 的服务器密匙和证书,保存在名为 server.keystore 的新 keystore 中。(如果在此目录中已经有一个 server.keystore,那么首先需要删除使用该别名的现有密匙对)。keytool 提示了许多用于生成认证的信息项(这些信息都不会影响测试使用)并要求您确认这些信息。通过输入 yes 确认后,keytool 将使用私有密匙和证书创建 keystore,随后退出该程序。

接下来,再次运行这个程序来生成客户机密匙对和 keystore,这一次使用以下的命令(以单行形式输入):

keytool -genkey -alias clientkey -keypass clientpass -keyalg RSA -sigalg SHA1withRSA 
   -keystore client.keystore -storepass nosecret

下一步是从服务器 storekey 导出证书,并将证书导入到客户机 keystore。要执行导出,使用下面的命令(为适应页面宽度而进行了分解;您必须以单行形式输入):

keytool -export -alias serverkey -keystore server.keystore -storepass nosecret 
   -file servercert.cer

导出创建了一个名为 servercert.cer 的证书文件,您可以将它导入到客户机 keystore 中,使用以下命令(以单行形式输入):

keytool -import -alias serverkey -keystore client.keystore -storepass nosecret 
   -file servercert.cer

运行导入命令时,keytool 输出了证书的细节并询问您是否信任该证书。当您通过输入 yes 接受密匙后,它将把证书添加到 keystore 并退出。

对于最后一步,导出客户机证书并将其导入到服务器 server keystore,输入以下命令(以单行形式输入):

keytool -export -alias clientkey -keystore client.keystore -storepass nosecret 
   -file clientcert.cer

然后运行以下命令(为适应页面宽度而分行;您必须以单行形式输入):

keytool -import -alias clientkey -keystore server.keystore -storepass nosecret 
   -file clientcert.cer

为什么要导出/导入两种证书?

文本要求您为双方导出一个证书,然后将证书导入到另一方的 keystore。如果您使用了加密,那么需要为服务器证书执行这一操作,即使证书是由权威机构签名,因为加密需要访问另一方的公开密匙。另一方面,对于客户机证书,只需要将证书导入服务器 keystore,因为证书是自签名的并且无法通过其他方式验证。通过将证书导入到 keystore,您已经实现对其进行了确认,因此不需要再通过权威机构进行检验。

可以采用相同的办法处理多个使用自签名证书的客户机,只需要将每个客户机的证书导入到服务器 keystore。作为一种替代选择,与使用自签名证书不同的是,可以运行您自己的证书机构(使用 OpenSSL 之类的工具)并要求每个客户机获得由该机构签名的证书。通过这个方式,您可以向服务器 keystore 添加证书机构,并且任何具有由该机构签名的证书的客户机将被接受。或者通过支付费用来使用由权威机构签名的官方证书。

要利用新的密匙和证书,在运行客户机构建之前,必须将 client.keystore 文件复制到示例代码的 client/src 目录(或者只需将其复制到 client/bin 目录以立即生效),并在运行服务器构建之前将 server.keystore 文件复制到示例代码的 server/src 目录。

本节中的样例 keytool 命令使用了与附带的示例代码相同的文件名和密码。当您生成自己的密匙和证书时,您也可以修改这些值,但是那样做的话还需要修改示例代码以便匹配。在消息传递双方的策略文件中,keystore 密码和文件名都是 RampartConfig 中的参数。客户机密匙密码被硬编码到 com.sosnoski.ws.library.adb.PWCBHandler 类的客户机版本中,而服务器密匙密码则位于同一个类的服务器版本中。

结束语

在本文中,您了解了如何使用 Axis2 和 Rampart 实现基于策略的 WS-Security 加密和签名。这些强大的安全特性对于许多业务数据交换都至关重要,但是它们在处理开销方面会增加成本。在下一期 Java Web 服务 文章中,我们将介绍各种安全方法的相关性能开销情况,使您能够更好地判断如何在自己的应用程序中使用安全性。


下载

描述名字大小
本文源代码j-jws5.zip36KB

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, SOA and web services, Open source
ArticleID=421081
ArticleTitle=Java Web 服务: Axis2 WS-Security 签名和加密
publish-date=08172009