Java 中文官方教程 2022 版(四十二)

原文:docs.oracle.com/javase/tutorial/reallybigindex.html

设置策略文件以授予所需的权限。

原文:docs.oracle.com/javase/tutorial/security/toolsign/rstep3.html

接下来,您将使用策略工具创建一个名为exampleraypolicy的策略文件,并在其中授予来自已签名 JAR 文件的代码权限。

JAR 文件必须使用与上一步中导入到 Ray 的密钥库(exampleraystore)中的公钥对应的私钥进行签名。包含公钥的证书在密钥库中被别名为susan。我们将授予此类代码权限以读取C:\TestData\目录中的任何文件。

步骤如下:

  1. 启动策略工具

  2. 指定密钥库

  3. 使用 SignedBy 别名添加策略条目

  4. 保存策略文件

启动策略工具

原文:docs.oracle.com/javase/tutorial/security/toolsign/wstep1.html

要启动策略工具,只需在命令行中键入以下内容:

policytool

这将打开策略工具窗口。每次启动策略工具时,它会尝试从通常称为“用户策略文件”的文件中填充此窗口中的策略信息,默认情况下,该文件名为.java.policy,位于您的主目录中。如果策略工具找不到用户策略文件,它会报告这种情况并显示一个空白的策略工具窗口(即,一个带有标题和按钮但没有数据的窗口,如下图所示。

一个空白的策略工具窗口

由于本教程的课程不需要对您的官方用户策略文件进行修改,因此您将创建并使用一个与用户策略文件不同的策略文件。

假设您看到了空白的策略工具窗口(如果没有,请在文件菜单中选择新建),您可以立即开始创建一个新的策略文件。

指定密钥库

原文:docs.oracle.com/javase/tutorial/security/toolsign/wstep2.html

对于本课程,您将授予别名为 susan 的 JAR 文件中的所有代码对C:\TestData\目录中所有文件的读取访问权限。您需要

  1. 指定包含别名为 susan 的证书信息的密钥库

  2. 创建授予权限的策略条目

密钥库是在将证书导入为受信任的证书步骤中创建的名为exampleraystore的密钥库。

要指定密钥库,请在主策略工具窗口的编辑菜单中选择更改密钥库命令。这将弹出一个对话框,您可以在其中指定密钥库 URL 和密钥库类型。

要指定名为exampleraystore的密钥库,位于C:驱动器上的Test目录中,请在标记为“New KeyStore URL”的文本框中键入以下file URL

file:/C:/Test/exampleraystore

如果密钥库类型是默认类型,可以将标记为“New KeyStore Type”的文本框留空,如安全属性文件中所指定的那样。您的密钥库将是默认类型,因此请将文本框留空。


注意: “New KeyStore URL”值是一个 URL,因此应始终使用斜杠(而不是反斜杠)作为目录分隔符。


当您完成指定密钥库 URL 后,请选择确定。标记为密钥库的文本框现在填入了 URL。

接下来,您需要指定新的策略条目。

添加带有 SignedBy 别名的策略条目

原文:docs.oracle.com/javase/tutorial/security/toolsign/wstep3.html

要授予由susan签名的代码权限以读取C:\TestData目录中的任何文件,您需要创建一个授予此权限的策略条目。请注意,“由susan签名的代码”是指“包含在 JAR 文件中的类文件中的代码,其中 JAR 文件是使用与出现在别名为susan的 keystore 证书中的公钥对应的私钥签名的。”

在主策略工具窗口中选择添加策略条目按钮。这将打开策略条目对话框:

策略条目对话框

在此对话框中,将以下别名键入到SignedBy文本框中:

susan

CodeBase文本框留空,以授予由susan签名的所有代码权限,无论其来源。


注意: 如果您想将权限限制为仅来自C:\Test\目录的susan签名代码,您将在CodeBase文本框中键入以下 URL:

file:/C:/Test/*


要添加权限,请选择添加权限按钮。这将打开权限对话框。

权限对话框

执行以下操作。

  1. 从权限下拉列表中选择文件权限。完整的权限类型名称(java.io.FilePermission)现在显示在下拉列表右侧的文本框中。

  2. 在标记为目标名称的列表右侧的文本框中键入以下内容,以指定C:\TestData\目录中的所有文件:

    C:\TestData\*
    
    
  3. 通过从操作下拉列表中选择读取选项来指定读取权限。

现在权限对话框看起来像下面这样。

填写字段的权限对话框

选择确定按钮。新的权限出现在策略条目对话框中的一行中,如下所示。

新的权限出现在策略条目对话框中


注意: 你在文件路径中键入的每个反斜杠都已替换为两个反斜杠,以方便您使用。策略文件中的字符串由一个标记器处理,允许使用 \ 作为转义字符(例如,\n 表示换行),因此策略文件需要两个反斜杠来表示一个反斜杠。如果您使用单个反斜杠作为目录分隔符,策略工具会自动将其转换为双反斜杠。


现在已经完成指定此策略条目的操作,请在策略条目对话框中选择完成按钮。策略工具窗口现在包含表示策略条目的一行,显示SignedBy值。

保存策略文件

原文:docs.oracle.com/javase/tutorial/security/toolsign/wstep4.html

要保存你正在创建的新策略文件,请从文件菜单中选择另存为命令。这将弹出另存为对话框。

浏览目录结构,找到要保存策略文件的目录:C:驱动器上的Test目录。输入文件名。

exampleraypolicy

然后选择保存按钮。策略文件现在已保存,其名称和路径显示在标有策略文件的文本框中。

然后通过从文件菜单中选择退出命令退出策略工具。

查看策略文件效果

原文:docs.oracle.com/javase/tutorial/security/toolsign/rstep4.html

在之前的步骤中,你在exampleraypolicy策略文件中创建了一个条目,授予由susan签名的代码从C:\TestData\目录(或者如果你在 UNIX 上工作,则是你的主目录中的testdata目录)读取文件的权限。现在,你应该能够成功地执行Count程序,从指定目录中读取文件并计算字符数,即使在使用安全管理器运行应用程序时也是如此。

如在创建策略文件课程的结尾所述,有两种可能的方式可以使exampleraypolicy文件作为整体策略的一部分被考虑,除了在安全属性文件中指定的策略文件之外。第一种方法是在传递给运行时系统的属性中指定额外的策略文件。第二种方法是在安全属性文件中添加一行指定额外的策略文件。

方法 1

你可以使用-Djava.security.policy命令行参数来指定一个策略文件,该文件应该被用来补充或替代安全属性文件中指定的文件。

要运行Count应用程序并包含exampleraypolicy策略文件,请在包含sCount.jarexampleraypolicy文件的目录中键入以下内容:

java -Djava.security.manager
    -Djava.security.policy=exampleraypolicy
    -cp sCount.jar Count C:\TestData\data

注意:在一行上键入命令,-D-cp之前加上一个空格。

程序应该报告指定文件中的字符数。

如果仍然报错,那么策略文件中可能存在问题。使用策略工具检查你在上一步中创建的权限,并更正任何拼写错误或其他错误。

方法 2

你可以在安全属性文件中的policy.url.n属性中指定多个 URL,包括形如"http://"的 URL,所有指定的策略文件都将被加载。

因此,让解释器考虑你的exampleraypolicy文件的策略条目的一种方法是在安全属性文件中添加指示该文件的条目。


重要提示:如果你正在运行自己的 JDK 副本,你可以轻松编辑你的安全属性文件。如果你正在运行与他人共享的版本,只有在你有写入权限或在适当时向系统管理员请求修改文件时,你才能修改系统范围的安全属性文件。然而,在本教程测试中,对于你来说可能不适合修改系统范围的策略文件;我们建议你只是阅读以下内容以了解如何操作,或者安装你自己的私人版本的 JDK 以供教程课程使用。


安全属性文件位于

  • Windows*java.home*\lib\security\java.security

  • UNIX*java.home*/lib/security/java.security

java.home部分表示 JRE 安装的目录。

要修改安全属性文件,请在适合编辑 ASCII 文本文件的编辑器中打开它。然后在以policy.url.2开头的行后添加以下行:

  • Windows**policy.url.3=file:/C:/Test/exampleraypolicy**

  • UNIX**policy.url.3=file:${user.home}/test/exampleraypolicy**

在 UNIX 系统上,您还可以显式指定您的主目录,如

policy.url.3=file:/home/susanj/test/exampleraypolicy

接下来,在您的命令窗口中,转到包含sCount.jar文件的目录,即C:\Test~/test目录。在一行上键入以下命令:

java -Djava.security.manager
        -cp sCount.jar Count C:\TestData\data

与第一种方法一样,如果程序仍然报告错误,则可能是策略文件出现问题。使用策略工具检查您在上一步中创建的权限,并更正任何拼写错误或其他错误。


重要提示:在继续之前,您可能希望删除您刚刚在安全属性文件中添加的行(或将其注释掉),因为您可能不希望在不运行教程课程时包含exampleraypolicy文件。


课程:文件交换

原文:docs.oracle.com/javase/tutorial/security/toolfilex/index.html

如果您想将重要文件(如合同)电子发送给他人,最好对文件进行数字“签名”,以便您的接收方可以检查文件确实来自您,并在传输过程中未被更改。

本课程向您展示如何使用安全工具交换重要文件,本例中为合同。

首先,您假装自己是合同发送方,斯坦·史密斯。本课程展示了斯坦将使用的步骤,将合同放入 JAR 文件中,签名并导出与用于签署 JAR 文件的私钥对应的公钥证书。

然后,你假装自己是鲁思,已经收到签名的 JAR 文件和证书。你将使用keytool将证书导入鲁思的密钥库中,别名为stan,并使用jarsigner工具验证签名。

有关数字签名、证书、密钥库和工具的更多信息,请参阅用于安全代码和文件交换的 API 和工具使用课程。


注意:本课程假设您从同一目录中执行所有命令。


以下是步骤:

  • 发送方操作步骤

  • 接收方操作步骤

合同发送方的步骤

原文:docs.oracle.com/javase/tutorial/security/toolfilex/sender.html

此处为合同发送方概述的步骤与签署代码并授予权限课程中列出的代码签署者的步骤基本相同。然而,在这里,您假装是 Stan Smith 而不是 Susan Jones,并且在要签名的 JAR 文件中存储数据文件而不是类文件。

作为合同发送方,您需要执行以下步骤。

  1. 创建包含合同的 JAR 文件,使用jar工具。

  2. 生成密钥(如果尚未存在),使用keytool-genkey命令。

    可选步骤:为公钥证书生成证书签名请求(CSR),并导入认证机构的响应。为简单起见,由于您只是假装是 Stan Smith,因此省略了此步骤。有关更多信息,请参见为公钥证书生成证书签名请求(CSR)。

  3. 对 JAR 文件进行签名,使用jarsigner工具和第 2 步生成的私钥。

  4. 导出公钥证书,使用keytool-export命令。然后将签名的 JAR 文件和证书提供给接收方 Ruth。

合同发送方的步骤

创建包含合同的 JAR 文件

原文:docs.oracle.com/javase/tutorial/security/toolfilex/step1.html

你需要的第一件事是一个合同文件。你可以下载并使用这个非常基本的示例文件,命名为contract。或者你也可以使用其他任何你喜欢的文件。只需确保将文件命名为contract,这样它就可以与本课程中指定的命令一起使用。

一旦你有了合同文件,将其放入一个 JAR 文件中。在你的命令窗口中输入以下内容:

jar cvf Contract.jar contract

这个命令创建一个名为Contract.jar的 JAR 文件,并将contract文件放入其中。

生成密钥

原文:docs.oracle.com/javase/tutorial/security/toolfilex/step2.html

在签署包含contract文件的Contract.jar JAR 文件之前,如果您尚未拥有合适的密钥,则需要生成密钥。您需要使用私钥对 JAR 文件进行签名,您的接收方需要您相应的公钥来验证您的签名。

本课程假设您尚未拥有密钥对。您将创建一个名为examplestanstore的密钥库,并创建一个具有新生成的公钥/私钥对的条目(其中公钥在证书中)。

现在假设你是 Stan Smith,并且在 Example2 公司的法律部门工作。在命令窗口中输入以下内容,创建一个名为examplestanstore的密钥库,并为 Stan Smith 生成密钥:

keytool -genkey -alias signLegal -keystore examplestanstore

密钥库工具会提示您输入密钥库密码、专有名称信息和密钥密码。以下是提示信息;粗体表示您应该输入的内容。

Enter keystore password:   *<password>*
What is your first and last name?
  [Unknown]:  Stan Smith 
What is the name of your organizational unit?
  [Unknown]:  Legal 
What is the name of your organization?
  [Unknown]:  Example2 
What is the name of your City or Locality?
  [Unknown]:  New York
What is the name of your State or Province?
  [Unknown]:  NY 
What is the two-letter country code for this unit?
  [Unknown]:  US 
Is <CN=Stan Smith, OU=Legal, O=Example2, L=New York, ST=NY, C=US> correct?
  [no]:  y 

Enter key password for <signLegal>
        (RETURN if same as keystore password):

前面的keytool命令在执行命令的同一目录中(假设指定的密钥库尚不存在)创建名为examplestanstore的密钥库,并为具有Stan Smith通用名称和Legal组织单位的实体生成公钥/私钥对。

你刚刚创建的自签名证书包括公钥和专有名称信息。(自签名证书是由与证书中公钥对应的私钥签名的证书。)该证书有效期为 90 天。如果不指定* -validity*选项,则默认有效期为 90 天。该证书与别名为signLegal的密钥库条目中的私钥相关联。私钥分配了输入的密码。

自签名证书对于开发和测试应用程序非常有用。但是,用户会收到警告,应用程序是使用不受信任的证书签名的,并询问他们是否要运行该应用程序。为了让用户更有信心运行您的应用程序,请使用由认可的证书颁发机构颁发的证书。

签署 JAR 文件

原文:docs.oracle.com/javase/tutorial/security/toolfilex/step3.html

现在,您已经准备好签署 JAR 文件了。

在命令窗口中一行输入以下内容,以使用密钥库条目中别名为signLegal的私钥签署 JAR 文件Contract.jar,并将生成的签名附加到命名为sContract.jar的结果签名的 JAR 文件中:

jarsigner -keystore examplestanstore
    -signedjar sContract.jar
    Contract.jar signLegal

系统会提示您输入存储密码和私钥密码。

jarsigner工具从别名为signLegal的密钥库条目中提取证书,并将其附加到已签名 JAR 文件的生成签名中。

导出公钥证书

原文:docs.oracle.com/javase/tutorial/security/toolfilex/step4.html

您现在拥有一个已签名的 JAR 文件sContract.jar。想要使用此文件的接收方也希望验证您的签名。为此,他们需要与您用于生成签名的私钥对应的公钥。您可以通过将包含您的公钥的证书副本发送给他们来提供您的公钥。通过以下方式,将该证书从密钥库examplestanstore复制到名为StanSmith.cer的文件中:

keytool -export -keystore examplestanstore
-alias signLegal -file StanSmith.cer

系统会提示您输入存储密码。

一旦他们获得了该证书和签名的 JAR 文件,您的接收方可以使用jarsigner工具来验证您的签名。请参阅合同接收方的步骤。

合同接收方的步骤

docs.oracle.com/javase/tutorial/security/toolfilex/receiver.html

现在扮演接收来自 Stan 的签名 JAR 文件和证书文件的 Ruth,执行以下步骤。

  1. 使用keytool-import命令将证书导入为受信任的证书。

  2. 使用jarsigner工具验证 JAR 文件签名。

将证书导入为受信任的证书

原文:docs.oracle.com/javase/tutorial/security/toolfilex/rstep1.html

假设您是 Ruth,并已从 Stan Smith 那里收到

  • 签名的 JAR 文件sContract.jar包含一个合同

  • 文件StanSmith.cer包含与用于签署 JAR 文件的私钥对应的公钥证书

在您可以使用jarsigner工具检查 JAR 文件签名的真实性之前,您需要将 Stan 的证书导入您的密钥库。

即使您(扮演 Stan)创建了这些文件,它们实际上还没有被传输到任何地方,您可以模拟成除创建者和发送者 Stan 之外的其他人。作为 Ruth,输入以下命令创建一个名为exampleruthstore的密钥库,并将证书导入到别名为stan的条目中。

keytool -import -alias stan -file StanSmith.cer -keystore exampleruthstore

由于密钥库尚不存在,keytool将为您创建它。它会提示您输入密钥库密码。

keytool打印证书信息并要求您验证它;例如,通过将显示的证书指纹与从另一个(受信任的)信息源获得的指纹进行比较。(每个指纹是一个相对较短的数字,可以唯一且可靠地识别证书。)例如,在现实世界中,您可以打电话给 Stan 并询问他应该是什么指纹。他可以通过执行命令获取他创建的StanSmith.cer文件的指纹

keytool -printcert -file StanSmith.cer

如果他看到的指纹与keytool向您报告的指纹相同,则您都可以假定证书在传输过程中未被修改。您可以放心地让keytool继续将一个“受信任的证书”条目放入您的密钥库中。该条目包含来自文件StanSmith.cer的公钥证书数据。keytool为这个新条目分配别名stan

验证 JAR 文件签名。

docs.oracle.com/javase/tutorial/security/toolfilex/rstep2.html

作为 Ruth,您现在已将 Stan 的公钥证书导入到exampleruthstore密钥库中作为“受信任的证书”。您现在可以使用jarsigner工具来验证 JAR 文件签名的真实性。

当您验证已签名的 JAR 文件时,您验证签名是否有效,以及 JAR 文件是否未被篡改。您可以通过以下命令对sContract.jar文件进行此操作:

jarsigner -verify -verbose -keystore exampleruthstore sContract.jar 

您应该看到类似以下内容:

       183 Fri Jul 31 10:49:54 PDT 1998 META-INF/SIGNLEGAL.SF
       1542 Fri Jul 31 10:49:54 PDT 1998 META-INF/SIGNLEGAL.DSA
       0 Fri Jul 31 10:49:18 PDT 1998 META-INF/
smk    1147 Wed Jul 29 16:06:12 PDT 1998 contract

 s = signature was verified 
 m = entry is listed in manifest
 k = at least one certificate was found in keystore
 i = at least one certificate was found in identity scope

jar verified.

请务必使用-verbose选项运行命令,以获取足够的信息以确保以下内容:

  • 合同文件是 JAR 文件中的文件之一,已签名并验证其签名(这就是s的意思)。

  • 用于验证签名的公钥位于指定的密钥库中,因此您信任它(这就是k的意思)。

课程:生成和验证签名

原文:docs.oracle.com/javase/tutorial/security/apisign/index.html

本课程将指导您使用 JDK 安全 API 为数据生成数字签名并验证签名的步骤。本课程适用于希望将安全功能纳入其程序中的开发人员,包括密码服务。

本课程演示了使用 JDK 安全 API 签署文档。该课程展示了一个程序,由拥有原始文档的人执行,用于生成密钥、使用私钥为文档生成数字签名,并将公钥和签名导出到文件。

然后展示了另一个程序的示例,由文档、签名和公钥的接收者执行。展示了程序如何导入公钥并验证签名的真实性。该课程还讨论并演示了可能的替代方法和提供和导入密钥的方法,包括在证书中。

欲了解有关概念和术语(数字签名、证书、密钥库)的更多信息,请参阅用于安全代码和文件交换的 API 和工具使用课程。

在本课程中,您将创建两个基本应用程序,一个用于数字签名生成,另一个用于验证。接着讨论和演示了潜在的增强功能。本课程包含三个部分。

  • 生成数字签名展示了使用 API 生成数据的密钥和数字签名,使用私钥并将公钥和签名导出到文件。应用程序从命令行获取数据文件名。

  • 验证数字签名展示了使用 API 导入一个公钥和一个据称是指定数据文件签名的签名,并验证签名的真实性。数据、公钥和签名文件名在命令行中指定。

  • 弱点和替代方案讨论了基本程序使用的方法可能存在的弱点。然后介绍并演示了可能的替代方法和提供和导入密钥的方法,包括使用包含编码密钥字节的文件和使用包含公钥的证书。

生成数字签名

原文:docs.oracle.com/javase/tutorial/security/apisign/gensig.html

即将创建的GenSig程序将使用 JDK 安全 API 生成密钥和使用私钥为数据生成数字签名,并将公钥和签名导出到文件中。应用程序从命令行获取数据文件名。

以下步骤创建GenSig示例程序。

  1. 准备初始程序结构

    创建一个名为GenSig.java的文本文件。输入初始程序结构(导入语句、类名、main方法等)。

  2. 生成公钥和私钥

    生成一对密钥(公钥和私钥)。私钥用于对数据进行签名。公钥将被VerSig程序用于验证签名。

  3. 对数据进行签名

    获取一个Signature对象并初始化以进行签名。提供要签名的数据,并生成签名。

  4. 保存签名和公钥到文件中

    将签名字节保存在一个文件中,将公钥字节保存在另一个文件中。

  5. 编译并运行程序

准备初始程序结构

原文:docs.oracle.com/javase/tutorial/security/apisign/step1.html

这是GenSig程序的基本结构。将其放在名为GenSig.java的文件中。

import java.io.*;
import java.security.*;

class GenSig {

    public static void main(String[] args) {

        /* Generate a DSA signature */

        if (args.length != 1) {
            System.out.println("Usage: GenSig nameOfFileToSign");
        }
        else try {

        // the rest of the code goes here

        } catch (Exception e) {
            System.err.println("Caught exception " + e.toString());
        }
    }
}

注意:

  • 签署数据的方法位于java.security包中,因此程序从该包中导入所有内容。程序还导入了java.io包,其中包含输入要签名的文件数据所需的方法。

  • 期望提供一个参数,指定要签名的数据文件。

  • 后续步骤中编写的代码将放在trycatch块之间。

生成公钥和私钥

原文:docs.oracle.com/javase/tutorial/security/apisign/step2.html

要能够创建数字签名,您需要一个私钥。(为了验证签名的真实性,还需要相应的公钥。)

在某些情况下,密钥对(私钥和相应的公钥)已经存在于文件中。在这种情况下,程序可以导入并使用私钥进行签名,如 Weaknesses and Alternatives 中所示。

在其他情况下,程序需要生成密钥对。通过使用KeyPairGenerator类生成密钥对。

在此示例中,您将为数字签名算法(DSA)生成公钥/私钥对。您将生成长度为 1024 位的密钥。

生成密钥对需要几个步骤:

创建密钥对生成器

第一步是获取用于生成 DSA 签名算法密钥的密钥对生成器对象。

与所有引擎类一样,获取特定类型算法的KeyPairGenerator对象的方法是在KeyPairGenerator类上调用getInstance静态工厂方法。该方法有两种形式,都有一个String algorithm作为第一个参数;其中一种形式还有一个String provider作为第二个参数。

调用者可以选择指定提供程序的名称,这将确保所请求的算法实现来自指定的提供程序。本课程的示例代码始终指定内置于 JDK 中的默认 SUN 提供程序。

在上述声明之后放置

else try {

在上一步创建的文件中的行,Prepare Initial Program Structure:

KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DSA", "SUN");

初始化密钥对生成器

下一步是初始化密钥对生成器。所有密钥对生成器都共享密钥大小和随机源的概念。KeyPairGenerator类有一个initialize方法,接受这两种类型的参数。

DSA 密钥生成器的密钥大小是密钥长度(以位为单位),您将设置为 1024。

随机源必须是SecureRandom类的实例,提供一个密码学强随机数生成器(RNG)。有关SecureRandom的更多信息,请参阅SecureRandom API SpecificationJava Cryptography Architecture Reference Guide

以下示例请求一个使用内置 SUN 提供程序提供的 SHA1PRNG 算法的SecureRandom实例。然后将此SecureRandom实例传递给密钥对生成器初始化方法。

SecureRandom random = SecureRandom.getInstance("SHA1PRNG", "SUN");
keyGen.initialize(1024, random);

有些情况需要强随机值,比如创建高价值和长期存在的秘密,如 RSA 公钥和私钥。为了帮助应用程序选择合适的强SecureRandom实现,从 JDK 8 开始,Java 发行版在java.security.Security类的securerandom.strongAlgorithms属性中包含了已知的强SecureRandom实现列表。当您创建这样的数据时,应考虑使用SecureRandom.getInstanceStrong(),因为它获取已知强算法的实例。

生成密钥对

最后一步是生成密钥对,并将密钥存储在PrivateKeyPublicKey对象中。

KeyPair pair = keyGen.generateKeyPair();
PrivateKey priv = pair.getPrivate();
PublicKey pub = pair.getPublic();

对数据进行签名

原文:docs.oracle.com/javase/tutorial/security/apisign/step3.html

现在您已经创建了公钥和私钥,可以准备对数据进行签名。在此示例中,您将对文件中包含的数据进行签名。GenSig 从命令行获取文件名。使用 Signature 类的实例创建数字签名。

对数据进行签名,生成该数据的数字签名,需要执行以下步骤。

获取签名对象:以下获取一个 Signature 对象,用于使用 DSA 算法生成或验证签名,该算法与程序在上一步中生成密钥的算法相同,生成公钥和私钥。

Signature dsa = Signature.getInstance("SHA1withDSA", "SUN"); 

注意:在指定签名算法名称时,还应包括签名算法使用的消息摘要算法的名称。SHA1withDSA 是指定 DSA 签名算法的一种方式,使用 SHA-1 消息摘要算法。

初始化签名对象

Signature 对象用于签名或验证之前,必须进行初始化。签名的初始化方法需要一个私钥。使用前一步中放入名为 privPrivateKey 对象中的私钥。

dsa.initSign(priv);

向签名对象提供要签名的数据 该程序将使用作为第一个(也是唯一的)命令行参数指定的文件名中的数据。程序将逐个缓冲区读取数据,并通过调用 update 方法将其提供给 Signature 对象。

FileInputStream fis = new FileInputStream(args[0]);
BufferedInputStream bufin = new BufferedInputStream(fis);
byte[] buffer = new byte[1024];
int len;
while ((len = bufin.read(buffer)) >= 0) {
    dsa.update(buffer, 0, len);
};
bufin.close();

生成签名

一旦所有数据都已提供给 Signature 对象,就可以生成该数据的数字签名。

byte[] realSig = dsa.sign();

将签名和公钥保存在文件中

原文:docs.oracle.com/javase/tutorial/security/apisign/step4.html

现在您已经为某些数据生成了签名,您需要将签名字节保存在一个文件中,将公钥字节保存在另一个文件中,这样您就可以通过调制解调器、软盘、邮件等方式将其发送给其他人。

  • 生成签名的数据,

  • 签名,

  • 公钥

接收方可以通过运行您将在接下来的验证数字签名步骤中生成的VerSig程序来验证数据是否来自您,并且在传输过程中没有被修改。该程序使用公钥来验证接收到的签名是否是接收到的数据的真实签名。

回想一下,签名是放在一个名为realSig的字节数组中的。您可以通过以下方式将签名字节保存在名为sig的文件中。

/* save the signature in a file */
FileOutputStream sigfos = new FileOutputStream("sig");
sigfos.write(realSig);
sigfos.close();

从生成公钥和私钥步骤中回想一下,公钥是放在一个名为pub的 PublicKey 对象中的。您可以通过调用getEncoded方法获取编码后的密钥字节,然后将编码后的字节存储在一个文件中。您可以随意命名文件。例如,如果您的名字是 Susan,您可以将其命名为suepk(代表"Sue 的公钥"),如下所示:

/* save the public key in a file */
byte[] key = pub.getEncoded();
FileOutputStream keyfos = new FileOutputStream("suepk");
keyfos.write(key);
keyfos.close();

编译并运行程序

原文:docs.oracle.com/javase/tutorial/security/apisign/step5.html

这里GenSig.java程序的完整源代码,添加了一些注释。编译并运行它。请记住,您需要指定要签名的文件名,如

java GenSig data

您可以下载并使用名为data的示例文件或您喜欢的任何其他文件。该文件不会被修改。它将被读取,以便为其生成签名。

执行程序后,您应该看到保存的suepk(公钥)和sig(签名)文件。

验证数字签名

原文:docs.oracle.com/javase/tutorial/security/apisign/versig.html

如果您有生成数字签名的数据,您可以验证签名的真实性。为此,您需要

  • 数据

  • 签名

  • 用于签署数据的私钥对应的公钥

在这个例子中,您编写一个VerSig程序来验证由GenSig程序生成的签名。这演示了验证据称签名真实性所需的步骤。

VerSig导入一个公钥和一个据称是指定数据文件签名的签名,然后验证签名的真实性。公钥、签名和数据文件名在命令行中指定。

创建VerSig示例程序以导入文件并验证签名的步骤如下。

  1. 准备初始程序结构

    创建一个名为VerSig.java的文本文件。输入初始程序结构(导入语句、类名、main方法等)。

  2. 输入并转换编码的公钥字节

    从指定为第一个命令行参数的文件中导入编码的公钥字节,并将其转换为PublicKey

  3. 输入签名字节

    从指定为第二个命令行参数的文件中输入签名字节。

  4. 验证签名

    获取一个Signature对象并用于验证签名的公钥进行初始化。提供要验证签名的数据(来自指定为第三个命令行参数的文件),并验证签名。

  5. 编译和运行程序

准备初始程序结构

原文:docs.oracle.com/javase/tutorial/security/apisign/vstep1.html

这是在本课程后续部分创建的VerSig程序的基本结构。将此程序结构放在名为VerSig.java的文件中。

import java.io.*;
import java.security.*;
import java.security.spec.*;

class VerSig {

    public static void main(String[] args) {

        /* Verify a DSA signature */

        if (args.length != 3) {
            System.out.println("Usage: VerSig " +
                "publickeyfile signaturefile " + "datafile");
        }
        else try {

        // the rest of the code goes here

        } catch (Exception e) {
            System.err.println("Caught exception " + e.toString());
        }
    }

}

注意:

  • 用于验证数据的方法位于java.security包中,因此程序从该包中导入所有内容。程序还从java.io包中导入所需的用于输入要签名的文件数据的方法,以及从java.security.spec包中导入包含X509EncodedKeySpec类的内容。

  • 期望有三个参数,分别指定公钥、签名和数据文件。

  • 在本课程后续步骤中编写的代码将放在trycatch块之间。

输入并转换编码的公钥字节

原文:docs.oracle.com/javase/tutorial/security/apisign/vstep2.html

接下来,VerSig需要从指定为第一个命令行参数的文件中导入编码的公钥字节,并将其转换为PublicKey。需要一个PublicKey,因为SignatureinitVerify方法需要它来初始化用于验证的Signature对象。

首先,读取编码的公钥字节。

FileInputStream keyfis = new FileInputStream(args[0]);
byte[] encKey = new byte[keyfis.available()];  
keyfis.read(encKey);

keyfis.close();

现在字节数组encKey包含了编码的公钥字节。

你可以使用KeyFactory类来实例化一个 DSA 公钥,从其编码中。KeyFactory类提供了不透明密钥(类型为Key)和密钥规范之间的转换,密钥规范是底层密钥材料的透明表示。通过不透明密钥,你可以获取算法名称、格式名称和编码的密钥字节,但不能获取密钥材料,例如,可能包括密钥本身和用于计算密钥的算法参数。 (请注意,PublicKey,因为它扩展了Key,本身也是一个Key。)

所以,首先你需要一个密钥规范。假设密钥是根据 X.509 标准编码的,你可以通过以下方式获取一个,例如,如果密钥是使用 SUN 提供的内置 DSA 密钥对生成器生成的:

X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(encKey);

现在你需要一个KeyFactory对象来进行转换。该对象必须是一个可以处理 DSA 密钥的对象。

KeyFactory keyFactory = KeyFactory.getInstance("DSA", "SUN");

最后,你可以使用KeyFactory对象从密钥规范生成一个PublicKey

PublicKey pubKey =
    keyFactory.generatePublic(pubKeySpec);

输入签名字节

原文:docs.oracle.com/javase/tutorial/security/apisign/vstep3.html

下一步,输入作为第二个命令行参数指定的文件中的签名字节。

FileInputStream sigfis = new FileInputStream(args[1]);
byte[] sigToVerify = new byte[sigfis.available()]; 
sigfis.read(sigToVerify);
sigfis.close();

现在字节数组sigToVerify包含了签名字节。

验证签名

原文:docs.oracle.com/javase/tutorial/security/apisign/vstep4.html

您已经向VerSig程序添加了代码

  • 输入编码的密钥字节并将其转换为名为pubKeyPublicKey

  • 将签名字节输入到名为sigToVerify的字节数组中

您现在可以继续进行验证。

初始化用于验证的 Signature 对象

与生成签名一样,签名是通过使用Signature类的实例来验证的。您需要创建一个使用与生成签名相同的签名算法的Signature对象。GenSig程序使用的算法是来自 SUN 提供程序的 SHA1withDSA 算法。

Signature sig = Signature.getInstance("SHA1withDSA", "SUN");

接下来,您需要初始化Signature对象。验证的初始化方法需要公钥。

sig.initVerify(pubKey);

向签名对象提供要验证的数据 现在,您需要向Signature对象提供生成签名的数据。这些数据位于以第三个命令行参数指定的文件中。与签名时一样,逐个缓冲区读取数据,并通过调用update方法将其提供给Signature对象。

FileInputStream datafis = new FileInputStream(args[2]);
BufferedInputStream bufin = new BufferedInputStream(datafis);

byte[] buffer = new byte[1024];
int len;
while (bufin.available() != 0) {
    len = bufin.read(buffer);
    sig.update(buffer, 0, len);
};

bufin.close();

验证签名

一旦您向Signature对象提供了所有数据,您可以验证该数据的数字签名并报告结果。请记住,所谓的签名已读入名为sigToVerify的字节数组。

boolean verifies = sig.verify(sigToVerify);

System.out.println("signature verifies: " + verifies);

如果所谓的签名(sigToVerify)是由与公钥pubKey对应的私钥生成的指定数据文件的实际签名,则verifies值将为true

编译并运行程序

原文:docs.oracle.com/javase/tutorial/security/apisign/vstep5.html

这里VerSig.java程序的完整源代码,附加了一些注释。

编译并运行程序。请记住,您需要在命令行上指定三个参数:

  • 包含编码的公钥字节的文件的名称

  • 包含签名字节的文件的名称

  • 数据文件的名称(生成签名的文件)

由于您将测试GenSig程序的输出,您应该使用的文件名是

  • suepk

  • sig

  • data

这是一个示例运行;粗体表示您需要键入的内容。

%java VerSig suepk sig data
signature verifies: true

弱点和替代方案

原文:docs.oracle.com/javase/tutorial/security/apisign/enhancements.html

本课程中的GenSigVerSig程序演示了使用 JDK 安全 API 生成数据的数字签名以及验证签名的用法。然而,这些程序描绘的实际场景,即发送方使用 JDK 安全 API 生成新的公钥/私钥对,发送方将编码的公钥字节存储在文件中,接收方读取密钥字节,这并不一定是现实的,并且存在一个潜在的重大缺陷。

在许多情况下,密钥不需要生成;它们已经存在,要么作为文件中的编码密钥,要么作为密钥库中的条目。

潜在的重大缺陷在于没有任何保证接收方收到的公钥的真实性,而VerSig程序只有在提供的公钥本身是真实的情况下才能正确验证签名的真实性!

使用编码的密钥字节

有时,编码的密钥字节已经存在于用于签名和验证的密钥对的文件中。如果是这种情况,GenSig程序可以导入编码的私钥字节,并将其转换为签名所需的PrivateKey,通过以下方式,假设包含私钥字节的文件名在privkeyfile字符串中,并且字节代表已使用 PKCS #8 标准编码的 DSA 密钥。

FileInputStream keyfis = new FileInputStream(privkeyfile);
byte[] encKey = new byte[keyfis.available()];
keyfis.read(encKey);
keyfis.close();

PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(encKey);

KeyFactory keyFactory = KeyFactory.getInstance("DSA");
PrivateKey privKey = keyFactory.generatePrivate(privKeySpec);

GenSig不再需要将公钥字节保存在文件中,因为它们已经在一个文件中。

在这种情况下,发送方发送接收方

  • 包含编码的公钥字节的已存在文件(除非接收方已经拥有此文件)和

  • GenSig导出的数据文件和签名文件。

VerSig程序保持不变,因为它已经期望文件中存在编码的公钥字节。

但是,如果有恶意用户拦截文件并以一种无法检测到其更换的方式替换它们,会出现什么潜在问题呢?在某些情况下,这不是问题,因为人们已经通过面对面或通过信任的第三方进行了公钥交换。之后,可以远程进行多次文件和签名交换(即在不同位置的两个人之间),并且可以使用公钥来验证其真实性。如果有恶意用户尝试更改数据或签名,这将被VerSig检测到。

如果无法进行面对面的密钥交换,您可以尝试其他方法来增加正确接收的可能性。例如,您可以在随后的数据和签名文件交换之前,通过尽可能安全的方法发送您的公钥,也许使用不太安全的媒介。

一般来说,将数据和签名与你的公钥分开发送大大降低了攻击的可能性。除非所有三个文件都被更改,并且以下一段讨论的特定方式,否则VerSig将检测到任何篡改。

如果所有三个文件(数据文档、公钥和签名)被恶意用户拦截,那个人可以用私钥替换文档并签名,然后将替换后的文档、新签名和用于生成新签名的私钥对应的公钥转发给你。然后VerSig会报告验证成功,你会认为文档来自原始发送者。因此,你应该采取措施确保至少公钥完整接收(VerSig检测到其他文件的任何篡改),或者可以使用证书来促进公钥的认证,如下一节所述。

使用证书

在密码学中,更常见的是交换包含公钥的证书,而不是公钥本身。

一个好处是,证书由一个实体(颁发者)签名,以验证所包含的公钥是另一个实体(主体所有者)的实际公钥。通常,一个受信任的第三方认证机构(CA)验证主体的身份,然后通过签署证书来担保其为公钥所有者。

使用证书的另一个好处是,你可以通过使用颁发者(签名者)的公钥验证其数字签名来检查你收到的证书的有效性,该公钥本身可能存储在一个证书中,其签名可以通过使用该证书颁发者的公钥验证;该公钥本身可能存储在一个证书中,依此类推,直到达到你已经信任的公钥。

如果你无法建立信任链(也许因为所需的颁发者证书对你不可用),可以计算证书的指纹。每个指纹是一个相对较短的数字,可以唯一可靠地识别证书。(从技术上讲,它是证书信息的哈希值,使用消息摘要,也称为单向哈希函数。)你可以联系证书所有者,比较你收到的证书的指纹与发送的指纹。如果它们相同,证书也相同。

对于GenSig来说,更安全的做法是创建包含公钥的证书,然后让VerSig导入证书并提取公钥。然而,JDK 没有公共证书 API,允许你从公钥创建证书,因此GenSig程序无法从生成的公钥创建证书。(尽管有从证书中提取公钥的公共 API。)

如果您愿意,您可以使用各种安全工具,而不是 API,对您的重要文档进行签名,并与密钥库中的证书一起使用,就像在文件交换课程中所做的那样。

或者,您可以使用 API 修改您的程序以使用来自密钥库的已存在私钥和相应的公钥(在证书中)。首先,修改GenSig程序以从密钥库中提取私钥而不是生成新密钥。首先,让我们假设以下内容:

  • 密钥库名称在String``ksName

  • 密钥库类型为"JKS",这是来自 Oracle 的专有类型。

  • 密钥库密码在字符数组spass

  • 包含私钥和公钥证书的密钥库条目的别名在String``alias

  • 私钥密码在字符数组kpass

然后,您可以通过以下方式从密钥库中提取私钥。

KeyStore ks = KeyStore.getInstance("JKS");
FileInputStream ksfis = new FileInputStream(ksName); 
BufferedInputStream ksbufin = new BufferedInputStream(ksfis);

ks.load(ksbufin, spass);
PrivateKey priv = (PrivateKey) ks.getKey(alias, kpass);

您可以从密钥库中提取公钥证书,并将其编码字节保存到名为suecert的文件中,通过以下方式。

java.security.cert.Certificate cert = ks.getCertificate(alias);
byte[] encodedCert = cert.getEncoded();

// Save the certificate in a file named "suecert" 

FileOutputStream certfos = new FileOutputStream("suecert");
certfos.write(encodedCert);
certfos.close();

然后,您将数据文件、签名和证书发送给接收者。接收者通过首先使用keytool -printcert命令获取证书的指纹来验证证书的真实性。

keytool -printcert -file suecert
Owner: CN=Susan Jones, OU=Purchasing, O=ABC, L=Cupertino, ST=CA, C=US
Issuer: CN=Susan Jones, OU=Purchasing, O=ABC, L=Cupertino, ST=CA, C=US
Serial number: 35aaed17
Valid from: Mon Jul 13 22:31:03 PDT 1998 until:
Sun Oct 11 22:31:03 PDT 1998
Certificate fingerprints:
MD5:  1E:B8:04:59:86:7A:78:6B:40:AC:64:89:2C:0F:DD:13
SHA1: 1C:79:BD:26:A1:34:C0:0A:30:63:11:6A:F2:B9:67:DF:E5:8D:7B:5E

然后接收者验证指纹,可能通过给发送者打电话并将其与发送者的证书进行比较,或者通过在公共存储库中查找它们来进行验证。

接收者的验证程序(修改后的VerSig)然后可以通过以下方式导入证书并从中提取公钥,假设证书文件名(例如,suecert)在String``certName中。

FileInputStream certfis = new FileInputStream(certName);
java.security.cert.CertificateFactory cf =
    java.security.cert.CertificateFactory.getInstance("X.509");
java.security.cert.Certificate cert =  cf.generateCertificate(certfis);
PublicKey pub = cert.getPublicKey();

确保数据机密性

假设您希望保持数据的内容机密性,以便在传输过程中(或在您自己的计算机或磁盘上)无意或恶意尝试查看数据的人无法这样做。为了保持数据的机密性,您应该对其进行加密,仅存储和发送加密结果(称为ciphertext)。接收者可以解密密文以获得原始数据的副本。

课程:实现您自己的权限

原文:docs.oracle.com/javase/tutorial/security/userperm/index.html

本课程演示了如何编写一个定义自己特殊权限的类。本课程的基本组件包括:

  1. 一个名为ExampleGame的示例游戏。

  2. 一个名为HighScore的类,被ExampleGame用来存储用户最新的高分。

  3. 一个名为HighScorePermission的类,用于保护对用户存储的高分值的访问。

  4. 用户的安全策略文件,授予ExampleGame更新他/她的高分的权限。

基本场景如下:

  1. 用户玩ExampleGame

  2. 如果用户达到新的高分,ExampleGame使用HighScore类来保存这个新值。

  3. HighScore类查看用户的安全策略,以检查ExampleGame是否有权限更新用户的高分值。

  4. 如果ExampleGame有权限更新高分,则 HighScore 类更新该值。

我们描述每个基本组件的关键点,然后展示一个示例运行:

  • ExampleGame

  • 高分类

  • 高分权限类

  • 一个示例策略文件

  • 将所有内容整合在一起

ExampleGame

原文:docs.oracle.com/javase/tutorial/security/userperm/game.html

下面是ExampleGame的源代码。为简单起见,ExampleGame实际上并不包含玩游戏的代码。它只是检索或更新用户的最高分。

要查看用户当前的最高分值,您可以运行:

java ExampleGame get

要为用户设置新的最高分值,您可以运行:

java ExampleGame set *score* 

要检索用户当前的最高分,ExampleGame只需实例化一个HighScore对象并调用其getHighScore方法。要为用户设置新的最高分,ExampleGame实例化一个HighScore对象并调用setHighScore,将用户的新最高分传递给它。

这里是ExampleGame的源代码,ExampleGame.java:


package com.gamedev.games;

import java.io.*;
import java.security.*;
import java.util.Hashtable;
import com.scoredev.scores.*;

public class ExampleGame
{
    public static void main(String args[])
	throws Exception 
    {
	HighScore hs = new HighScore("ExampleGame");

	if (args.length == 0)
	    usage();

	if (args[0].equals("set")) {
	    hs.setHighScore(Integer.parseInt(args[1]));
	} else if (args[0].equals("get")) {
	    System.out.println("score = "+ hs.getHighScore());
	} else {
	    usage();
	}
    }

    public static void usage()
    {
	System.out.println("ExampleGame get");
	System.out.println("ExampleGame set <score>");
	System.exit(1);
    }
}

高分类

原文:docs.oracle.com/javase/tutorial/security/userperm/highscore.html

HighScore类存储并保护用户在ExampleGame(以及调用它的任何其他游戏)中的高分值的访问。为简单起见,该类将高分值保存到名为.highscore的文件中,该文件位于用户的主目录中。但是,在允许ExampleGame检索或更新用户的高分值之前,该类会检查用户是否已在其安全策略文件中授予ExampleGame访问高分的权限。

检查ExampleGame是否具有HighScorePermission

要检查ExampleGame是否具有访问用户高分值的权限,HighScore类必须:

  1. 调用System.getSecurityManager()以获取当前安装的安全管理器。

  2. 如果结果不为空(也就是说,存在一个安全管理器,而不是调用者是一个无限制的应用程序),那么

    1. 构造一个HighScorePermission对象,并

    2. 调用安全管理器的checkPermission方法,并传递新构造的HighScorePermission对象。

这是代码:

SecurityManager sm = System.getSecurityManager();
if (sm != null) {
    sm.checkPermission(
        new HighScorePermission(gameName));
}

checkPermission方法本质上是询问安全管理器是否ExampleGame具有指定的HighScorePermission。换句话说,它询问安全管理器是否ExampleGame有权限更新指定游戏(ExampleGame)的用户高分值。底层安全框架将查阅用户的安全策略,以查看ExampleGame是否确实具有此权限。

高分代码

这里HighScore类的完整源代码。

注意:doPrivileged方法调用用于使HighScore能够临时访问对其可用但对调用它的代码(ExampleGame)不可用的资源。例如,预期策略文件将授予HighScore访问用户主目录中的.highscore文件的权限,但不会授予这些权限给游戏,如ExampleGame

HighScorePermission 类

原文:docs.oracle.com/javase/tutorial/security/userperm/perm.html

HighScorePermission 类定义了 ExampleGame 需要更新用户高分的权限。

所有权限类都应该从 java.security.Permissionjava.security.BasicPermission 中的一个子类化。两者之间的基本区别在于,java.security.Permission 定义了需要名称和操作的更复杂的权限。例如,java.io.FilePermission 扩展自 java.security.Permission,并需要一个名称(文件名)以及该文件允许的操作(读/写/删除)。

相比之下,java.security.BasicPermission 定义了只需要名称的更简单的权限。例如,java.lang.RuntimePermission 扩展自 java.security.BasicPermission,只需要一个名称(如 "exitVM"),允许程序退出 Java 虚拟机。

我们的 HighScorePermission 是一个简单的权限,因此可以从 java.security.BasicPermission 扩展。

通常,BasicPermission 类中的方法实现本身不需要被其子类重写。这就是我们的 HighScorePermission 的情况,所以我们只需要实现构造函数,它们只是调用超类的构造函数,如以下所示:


package com.scoredev.scores;

import java.security.*;

public final class HighScorePermission extends BasicPermission {

    public HighScorePermission(String name)
    {
	super(name);
    }

    // note that actions is ignored and not used,
    // but this constructor is still needed
    public HighScorePermission(String name, String actions) 
    {
	super(name, actions);
    }
}

一个示例策略文件

原文:docs.oracle.com/javase/tutorial/security/userperm/policy.html

以下是一个完整的策略文件,供用户运行ExampleGame使用。

这里不描述策略文件的语法;如果您感兴趣,请参阅默认策略实现和策略文件语法页面。

你不需要了解语法;你可以随时使用策略工具创建策略文件,如创建策略文件,控制应用程序的快速导览,以及签署代码并授予权限课程中所示。

以下是示例策略文件,后面是各个条目的描述。假设

  • 策略文件位于 Kim 的计算机上,Kim 的密钥库命名为kim.keystore

  • ExampleGame已由游戏创建者 Terry 的私钥签名,相应的公钥在别名为"terry"的密钥库条目中。

  • HighScoreHighScorePermissions类是由实现它们的人(Chris)的私钥签名的,相应的公钥在别名为"chris"的密钥库条目中。

这是策略文件:kim.policy

keystore "kim.keystore";

// Here is the permission ExampleGame needs.
// It grants code signed by "terry" the
// HighScorePermission, if the
// HighScorePermission was signed by "chris"
grant SignedBy "terry" {
  permission
    com.scoredev.scores.HighScorePermission
      "ExampleGame", signedBy "chris";
};

// Here is the set of permissions the HighScore
// class needs:
grant SignedBy "chris" {
  // The HighScore class needs permission to read
  // "user.home" to find the location of the
  // highscore file

  permission java.util.PropertyPermission
    "user.home", "read";

  // It needs permission to read and write the
  // high score file itself

  permission java.io.FilePermission
      "${user.home}${/}.highscore", "read,write";

  // It needs to get granted its own permission,
  // so it can call checkPermission
  // to see if its caller has permission.
  // Only grant it the permission
  // if the permission itself was signed by
  // "chris"

  permission
    com.scoredev.scores.HighScorePermission 
      "*", signedBy "chris";
};

密钥库条目

密钥库是密钥和证书的存储库,用于查找策略文件中指定的签名者的公钥(在本例中为"terry""chris")。

keytool实用程序用于创建和管理密钥库。

对于本课程,假设 Kim 想玩ExampleGame。如果 Kim 的密钥库命名为kim.keystore,那么 Kim 的策略文件需要在开头加上以下行:

keystore "kim.keystore";

ExampleGame 条目

策略文件条目指定了特定代码源的一个或多个权限 - 来自特定位置(URL)的代码,或者由特定实体签名的代码,或两者兼有。

我们的策略文件需要为每个游戏添加一个条目,为该游戏的创建者签名的代码授予一个名为HighScorePermission的权限,其名称为游戏名称。该权限允许游戏调用HighScore方法来获取或更新该特定游戏用户的最高分值。

ExampleGame所需的条目是:

grant SignedBy "terry" {
    permission
        com.scoredev.scores.HighScorePermission 
            "ExampleGame", signedBy "chris";
};

要求ExampleGame"terry"签名使 Kim 知道该游戏是 Terry 开发的实际游戏。为了使其工作,Kim 必须已经将 Terry 的公钥证书存储到kim.keystore中,别名为"terry"

注意,HighScorePermission需要由实际实现该权限的"chris"签名,以确保ExampleGame被授予由"chris"实现的实际权限,而不是其他人。与之前一样,为了使其工作,Kim 必须已经将 Chris 的公钥证书存储到kim.keystore中,别名为"chris"

最高分条目

策略文件中的最后一个条目授予HighScore 类权限。更具体地说,它授予由"chris"签名的代码权限,他创建并签署了这个类。要求类由"chris"签名确保当ExampleGame 调用这个类来更新用户的高分时,ExampleGame 确切知道它正在使用由"chris"实现的原始类。

要更新调用它的任何游戏的用户高分值,HighScore 类需要三个权限:

1. 读取"user.home"属性值的权限。

HighScore 类将用户的高分值存储在用户主目录中的.highscore文件中。因此,这个类需要一个java.util.PropertyPermission,允许它读取"user.home"属性值,以确定用户主目录的确切位置:

permission java.util.PropertyPermission 
    "user.home", "read";

2. 读写高分文件本身的权限。

这个权限是为了让HighScoregetHighScoresetHighScore 方法可以访问用户的.highscore文件,分别获取或设置当前游戏的当前高分。

这是所需的权限:

permission java.io.FilePermission
    "${user.home}${/}.highscore", "read,write";

注意:${propName} 表示属性的值。因此,${user.home} 将被"user.home"属性的值替换。${/} 表示文件分隔符的平台无关方式。

3. 所有 HighScorePermissions(即任何名称的 HighScorePermissions)。

这个权限是为了确保HighScore 检查调用游戏是否被授予了一个名为游戏名称的HighScorePermission。也就是说,HighScore 类必须同样被授予权限,因为权限检查要求堆栈上的所有代码都具有指定的权限。

这是所需的权限:

permission com.scoredev.scores.HighScorePermission
    "*", signedBy "chris";

与以前一样,HighScorePermission 本身需要由实际实现权限的"chris"签名。

将所有内容整合在一起

原文:docs.oracle.com/javase/tutorial/security/userperm/together.html

在这里,我们模拟依次成为HighScore开发者(克里斯),ExampleGame开发者(特里),以及运行游戏的用户(金)。

您可以执行所有指定的步骤,然后(作为金的最后一步)运行ExampleGame

这些步骤没有解释。关于代码签名者(如克里斯和特里)和接收此类代码的人(如金)需要采取的进一步信息,请参阅签署代码并授予权限课程。

这里是步骤:

  • HighScore 开发者(克里斯)的步骤

  • ExampleGame 开发者(特里)的步骤

  • 运行 ExampleGame 的用户(金)的步骤

高分开发者(克里斯)的步骤

原文:docs.oracle.com/javase/tutorial/security/userperm/chris.html

克里斯在创建HighScoreHighScorePermission类之后将采取的步骤是:

编译这些类

javac HighScore*.java -d .

将类文件放入一个 JAR 文件中

jar cvf hs.jar com/scoredev/scores/HighScore*.class

创建用于签名的密钥库和密钥

keytool -genkey -keystore chris.keystore -alias signJars

指定密码和显著名称信息

签署 JAR 文件

jarsigner -keystore chris.keystore hs.jar signJars

导出公钥证书

keytool -export -keystore chris.keystore
    -alias signJars -file Chris.cer

提供游戏开发人员和用户所需的文件和信息

也就是说,提供它们

  • 签名的 JAR 文件hs.jar

  • 公钥证书文件Chris.cer

  • HighScoreHighScorePermission类在策略文件中必须被授予的权限信息,以便能够正常工作。对于这一点,克里斯可以提供所需的确切授权条目。

示例游戏开发者(Terry)的步骤

原文:docs.oracle.com/javase/tutorial/security/userperm/terry.html

Terry 创建一个调用 HighScoregetHighScoresetHighScore 方法来获取和设置用户高分的游戏(ExampleGame)后,Terry 需要采取的步骤是:

编译游戏类

javac ExampleGame.java -classpath hs.jar -d .

将其类文件放入一个 JAR 文件中

jar cvf terry.jar com/gamedev/games/ExampleGame.class

创建用于签名的密钥库和密钥

keytool -genkey -keystore terry.keystore -alias signTJars

为密码和区分名称信息指定任何你想要的内容。

签署 JAR 文件

jarsigner -keystore terry.keystore terry.jar signTJars

导出公钥证书

keytool -export -keystore terry.keystore
    -alias signTJars -file Terry.cer

为用户提供所需的文件和信息

也就是说,向他们提供

  • 签名的 JAR 文件 terry.jar,

  • 公钥证书文件 Terry.cer, 和

  • ExampleGame 类所需权限的信息。对于这一点,Terry 可能会提供所需的确切授权条目。

游戏用户还需要来自 Chris 的文件和信息。为了方便他们,Terry 可能会将这些信息转发给他们:

  • 签名的 JAR 文件 hs.jar,

  • 公钥证书文件 Chris.cer, 和

  • 有关 HighScoreHighScorePermission 类在策略文件中必须被授予的权限的信息,以便其正常工作。这可能是所需的确切授权条目。

运行 ExampleGame(Kim)的用户步骤

原文:docs.oracle.com/javase/tutorial/security/userperm/kim.html

用户(比如 Kim)需要执行的步骤包括:

将证书导入为受信任的证书

keytool -import -alias chris -file Chris.cer -keystore kim.keystore
keytool -import -alias terry -file Terry.cer -keystore kim.keystore

设置具有所需权限的策略文件

这里是完整的kim.policy策略文件,如 A Sample Policy File 中所述。

运行 ExampleGame

设置高分:

java -Djava.security.manager 
    -Djava.security.policy=kim.policy
    -classpath hs.jar;terry.jar
    com.gamedev.games.ExampleGame set 456

获取高分:

java -Djava.security.manager
    -Djava.security.policy=kim.policy
    -classpath hs.jar;terry.jar
    com.gamedev.games.ExampleGame get

注意:

  • 如果不指定-Djava.security.manager,应用程序将无限制地运行(策略文件和权限不会被检查)。

  • -Djava.security.policy=kim.policy指定了策略文件的位置。注意:还有其他指定策略文件的方法。例如,你可以在安全属性文件中添加一个条目,指定包含kim.policy,如在查看策略文件效果课程末尾讨论的那样。

  • -classpath hs.jar;terry.jar指定了包含所需类文件的 JAR 文件。对于 Windows,使用分号(";")分隔 JAR 文件;对于 UNIX,使用冒号(":")。

  • 策略文件kim.policy指定了密钥库kim.keystore。由于未提供密钥库的绝对 URL 位置,因此假定密钥库与策略文件位于同一目录中。

教程:扩展机制

原文:docs.oracle.com/javase/tutorial/ext/index.html

扩展机制提供了一种标准、可扩展的方式,使自定义 API 对在 Java 平台上运行的所有应用程序可用。Java 扩展也被称为可选包。本教程可能会交替使用这两个术语。

扩展是通过扩展机制增强 Java 平台的一组包和类。扩展机制使运行时环境能够找到并加载扩展类,而无需在类路径上命名扩展类。在这方面,扩展类类似于 Java 平台的核心类。这也是扩展得名的原因--它们实际上扩展了平台的核心 API。

由于此机制扩展了平台的核心 API,应谨慎使用。最常见的用途是用于由 Java 社区流程定义的标准化接口,尽管也可能适用于站点范围接口。

此图显示了应用程序、Java 平台和扩展之间的关系。

如图所示,扩展充当 Java 平台的“附加”模块。它们的类和公共 API 自动对在平台上运行的任何应用程序可用。

扩展机制还提供了一种从远程位置下载扩展类供 applets 使用的方法。

扩展被打包为 Java 存档(JAR)文件,本教程假定您熟悉 JAR 文件格式。如果您对 JAR 文件不熟悉,您可能需要在继续本教程的课程之前查阅一些 JAR 文件文档:

  • 本教程中的在 JAR 文件中打包程序课程。

  • JDK™文档中的JAR 指南

本教程有两个课程:

创建和使用扩展

这一部分向您展示了如何向您的 Java 平台添加扩展,并且 applets 如何通过下载远程扩展类从扩展机制中受益。

使扩展安全

本节描述了在您的平台上授予扩展的安全特权和权限。如果您正在编写自己的扩展类,您将了解如何使用 Java 平台的安全架构。

附加文档

您可以在 JDK 文档的Java 扩展机制部分找到有关扩展的更多信息。

教程:创建和使用扩展

原文:docs.oracle.com/javase/tutorial/ext/basics/index.html

任何一组包或类都可以轻松地扮演扩展的角色。将一组类转变为扩展的第一步是将它们打包在一个 JAR 文件中。完成这一步后,您可以通过两种方式将软件转变为扩展:

  • 通过将 JAR 文件放置在 Java 运行时环境目录结构的特定位置,这种情况下称为已安装扩展。

  • 通过以特定方式从另一个 JAR 文件的清单中引用 JAR 文件,这种情况下称为下载扩展。

本课将通过使用一个简单的“玩具”扩展作为示例来展示扩展机制的工作原理。

已安装扩展

在本节中,您将创建一个简单的已安装扩展,并看到扩展软件如何被运行时环境视为平台的一部分。

下载扩展

本节将向您展示如何修改 JAR 文件的清单,以便 JAR 打包的软件可以利用下载扩展。

理解扩展类加载

本节是一个简短的插曲,总结了 Java 平台的类加载委托模型,并展示了它与扩展中类加载的关系。

创建可扩展应用程序

本节讨论了用于扩展应用程序的机制,通过插件或模块,而无需修改其原始代码库。

下一课,使扩展安全 使用相同的扩展来展示 Java 平台如何控制授予扩展的安全权限。

已安装的扩展

原文:docs.oracle.com/javase/tutorial/ext/basics/install.html

已安装的扩展是 JRE™软件的lib/ext目录中的 JAR 文件。顾名思义,JRE 是 Java 开发工具包的运行时部分,包含平台的核心 API,但不包括编译器和调试器等开发工具。JRE 可以单独使用,也可以作为 Java 开发工具包的一部分使用。

JRE 是 JDK 软件的严格子集。JDK 软件目录树的子集如下所示:

JDK 软件目录树

JRE 由图中突出显示的目录组成。无论您的 JRE 是独立的还是作为 JDK 软件的一部分,JRE 目录中的lib/ext中的任何 JAR 文件都会被运行时环境自动视为扩展。

由于安装的扩展会扩展平台的核心 API,请谨慎使用。它们很少适用于仅由单个或少量应用程序使用的接口。

此外,由于安装的扩展定义的符号将在所有 Java 进程中可见,因此应注意确保所有可见符号遵循适当的“反向域名”和“类层次结构”约定。例如,com.mycompany.MyClass

从 Java 6 开始,扩展 JAR 文件也可以放置在与任何特定 JRE 无关的位置,以便扩展可以被安装在系统上安装的所有 JRE 共享。在 Java 6 之前,java.ext.dirs的值指的是单个目录,但是从 Java 6 开始,它是一个目录列表(类似于CLASSPATH),指定扩展被搜索的位置。路径的第一个元素始终是 JRE 的lib/ext目录。第二个元素是 JRE 之外的目录。这个其他位置允许扩展 JAR 文件只安装一次,并被安装在该系统上安装的几个 JRE 使用。位置因操作系统而异:

  • Solaris™操作系统: /usr/jdk/packages/lib/ext

  • Linux: /usr/java/packages/lib/ext

  • Microsoft Windows: %SystemRoot%\Sun\Java\lib\ext

请注意,放置在上述任一目录中的安装扩展会扩展该系统上每个JRE(Java 6 或更高版本)的平台。

一个简单的例子

让我们创建一个简单的已安装扩展。我们的扩展由一个类RectangleArea组成,用于计算矩形的面积:

public final class RectangleArea {
    public static int area(java.awt.Rectangle r) {
        return r.width * r.height;
    }
}

此类有一个名为area的方法,该方法接受一个java.awt.Rectangle的实例并返回矩形的面积。

假设你想要使用名为AreaApp的应用程序测试RectangleArea

import java.awt.*;

public class AreaApp {
    public static void main(String[] args) {
        int width = 10;
        int height = 5;

        Rectangle r = new Rectangle(width, height);
        System.out.println("The rectangle's area is " 
                           + RectangleArea.area(r));
    }
}

此应用程序实例化一个 10 x 5 的矩形,然后使用RectangleArea.area方法打印出矩形的面积。

在没有扩展机制的情况下运行 AreaApp

让我们首先回顾一下如何在不使用扩展机制的情况下运行AreaApp应用程序。我们假设RectangleArea类被捆绑在名为area.jar的 JAR 文件中。

当然,RectangleArea类不是 Java 平台的一部分,因此您需要将area.jar文件放在类路径上才能运行AreaApp而不会出现运行时异常。例如,如果area.jar在目录/home/user中,您可以使用以下命令:

java -classpath .:/home/user/area.jar AreaApp 

此命令中指定的类路径包含当前目录,其中包含AreaApp.class,以及包含RectangleArea包的 JAR 文件的路径。通过运行此命令,您将获得所需的输出:

The rectangle's area is 50

使用扩展机制运行 AreaApp

现在让我们看看如何通过将RectangleArea类作为扩展来运行AreaApp

要将RectangleArea类变成一个扩展,您需要将文件area.jar放在 JRE 的lib/ext目录中。这样做会自动将RectangleArea赋予已安装扩展的状态。

area.jar安装为扩展后,您可以运行AreaApp而无需指定类路径:

java AreaApp 

因为您正在使用area.jar作为已安装的扩展,运行时环境将能够找到并加载RectangleArea类,即使您没有在类路径上指定它。同样,任何用户在您的系统上运行的小程序或应用程序都可以找到并使用RectangleArea类。

如果系统上安装了多个 JRE(Java 6 或更高版本),并且希望RectangleArea类作为所有 JRE 的扩展可用,而不是将其安装在特定 JRE 的lib/ext目录中,请将其安装在系统范围的位置。例如,在运行 Linux 的系统上,将area.jar安装在目录/usr/java/packages/lib/ext中。然后AreaApp可以在安装在该系统上的不同 JRE 上运行,例如,如果不同的浏览器配置为使用不同的 JRE。

下载扩展

原文:docs.oracle.com/javase/tutorial/ext/basics/download.html

下载扩展是 JAR 文件中的一组类(和相关资源)。JAR 文件的清单可以包含引用一个或多个下载扩展的头部。这些扩展可以通过以下两种方式引用:

  • 通过Class-Path头部

  • 通过Extension-List头部

请注意,清单中最多只允许一个。通过Class-Path头部指示的下载扩展仅在下载它们的应用程序(如 Web 浏览器)的生命周期内下载。它们的优点是客户端上没有安装任何内容;缺点是每次需要时都会下载它们。通过Extension-List头部下载的下载扩展将安装到下载它们的 JRE 的/lib/ext目录中。它们的优点是第一次需要时下载,随后可以在不下载的情况下使用。但是,正如本教程后面所示,它们部署起来更加复杂。

由于使用Class-Path头部的下载扩展更简单,让我们先考虑它们。例如假设a.jarb.jar是同一目录中的两个 JAR 文件,并且a.jar的清单包含了这个头部:

Class-Path: b.jar

那么b.jar中的类将作为a.jar中的类的扩展类。a.jar中的类可以调用b.jar中的类,而无需将b.jar中的类命名在类路径中。a.jar本身可能是扩展,也可能不是。如果b.jar不在与a.jar相同的目录中,那么Class-Path头部的值应设置为b.jar的相对路径名。

扮演下载扩展角色的类没有任何特殊之处。它们之所以被视为扩展,仅仅是因为它们被某个其他 JAR 文件的清单引用。

为了更好地理解下载扩展的工作原理,让我们创建一个并投入使用。

一个示例

假设你想要创建一个小程序,其中使用了前一节中的RectangleArea类:

public final class RectangleArea {  
    public static int area(java.awt.Rectangle r) {
        return r.width * r.height;
    }
}

在前一节中,你将RectangleArea类放入 JRE 的lib/ext目录中,将其转换为已安装扩展。通过将其转换为已安装扩展,任何应用程序都可以使用RectangleArea类,就好像它是 Java 平台的一部分。

如果你想要在小程序中使用RectangleArea类,情况会有些不同。例如,假设你有一个名为AreaApplet的小程序,其中使用了RectangleArea类:

import java.applet.Applet;
import java.awt.*;

public class AreaApplet extends Applet {
    Rectangle r;

    public void init() {    
        int width = 10;
        int height = 5;

        r = new Rectangle(width, height);
    }

    public void paint(Graphics g) {
        g.drawString("The rectangle's area is " 
                      + RectangleArea.area(r), 10, 10);
    }
}

此小程序实例化一个 10 x 5 的矩形,然后使用RectangleArea.area方法显示矩形的面积。

然而,你不能假设每个下载并使用你的小程序的人都会在他们的系统上有RectangleArea类可用,作为已安装的扩展或其他方式。解决这个问题的一种方法是从服务器端提供RectangleArea类,并且你可以通过将其作为下载扩展来实现。

要了解如何做到这一点,让我们假设你已经将AreaApplet捆绑在名为AreaApplet.jar的 JAR 文件中,并且类RectangleArea捆绑在RectangleArea.jar中。为了使RectangleArea.jar被视为下载扩展,RectangleArea.jar必须在AreaApplet.jar的清单中的Class-Path头中列出。例如,AreaApplet.jar的清单可能如下所示:

Manifest-Version: 1.0
Class-Path: RectangleArea.jar

这个清单中Class-Path头的值是RectangleArea.jar,没有指定路径,表示RectangleArea.jar位于与小程序的 JAR 文件相同的目录中。

关于Class-Path头的更多信息

如果一个小程序或应用程序使用多个扩展,你可以在清单中列出多个 URL。例如,以下是一个有效的头部:

Class-Path: area.jar servlet.jar images/

Class-Path头中,列出的任何不以'/'结尾的 URL 都被假定为 JAR 文件。以'/'结尾的 URL 表示目录。在上面的例子中,images/可能是一个包含小程序或应用程序所需资源的目录。

请注意,清单文件中只允许一个Class-Path头,并且清单中的每一行不能超过 72 个字符。如果需要指定的类路径条目超过一行的空间,可以将它们延伸到后续的续行上。每个续行都以两个空格开头。例如:

Class-Path: area.jar servlet.jar monitor.jar datasource.jar
  provider.jar gui.jar

未来的版本可能会取消每个标题只能有一个实例的限制,以及将行限制为仅有 72 个字符。

下载扩展可以“串联”,意味着一个下载扩展的清单可以有一个引用第二个扩展的Class-Path头,第二个扩展可以引用第三个扩展,依此类推。

安装下载扩展

在上面的例子中,小程序下载的扩展仅在加载小程序的浏览器仍在运行时可用。然而,如果在小程序和扩展的清单中包含了额外的信息,小程序可以触发扩展的安装。

由于这种机制扩展了平台的核心 API,其使用应谨慎。它很少适用于仅由单个或少量应用程序使用的接口。所有可见的符号应遵循反向域名和类层次结构约定。

基本要求是小程序和它使用的扩展在它们的清单中提供版本信息,并且它们被签名。版本信息允许 Java 插件确保扩展代码具有小程序期望的版本。例如,AreaApplet可以在其清单中指定一个areatest扩展:

Manifest-Version: 1.0
Extension-List: areatest
areatest-Extension-Name: area
areatest-Specification-Version: 1.1
areatest-Implementation-Version: 1.1.2
areatest-Implementation-Vendor-Id: com.example
areatest-Implementation-URL: http://www.example.com/test/area.jar

area.jar中的清单将提供相应的信息:

Manifest-Version: 1.0
Extension-Name: area
Specification-Vendor: Example Tech, Inc
Specification-Version: 1.1
Implementation-Vendor-Id: com.example
Implementation-Vendor: Example Tech, Inc
Implementation-Version: 1.1.2

应用程序和扩展程序都必须由相同的签名者签名。对 jar 文件进行签名会直接修改它们,在清单文件中提供更多信息。签名有助于确保只有可信任的代码被安装。签署 jar 文件的简单方法是首先创建一个密钥库,然后使用该密钥库保存用于应用程序和扩展程序的证书。例如:

keytool -genkey -dname "cn=Fred" -alias test  -validity 180

您将被要求输入密钥库和密钥密码。生成密钥后,jar 文件可以被签名:

jarsigner AreaApplet.jar test
jarsigner area.jar test

您将被要求输入密钥库和密钥密码。有关keytooljarsigner和其他安全工具的更多信息,请参阅Java 2 平台安全工具概述

这里是AreaDemo.html,它加载应用程序并导致扩展程序代码被下载并安装:

<html>
<body>
  <applet code="AreaApplet.class" archive="AreaApplet.jar"/>
</body>
</html>

当页面首次加载时,用户会被告知该应用程序需要安装扩展程序。随后的对话框会通知用户有关已签名的应用程序。接受两者会将扩展程序安装在 JRE 的lib/ext文件夹中并运行应用程序。

重新启动 Web 浏览器并加载相同的网页后,只会显示有关应用程序签名者的对话框,因为area.jar已经安装。如果在不同的 Web 浏览器中打开AreaDemo.html(假设两个浏览器都使用相同的 JRE),情况也是如此。

理解扩展类加载

译文:docs.oracle.com/javase/tutorial/ext/basics/load.html

扩展框架利用了类加载委托机制。当运行时环境需要为应用程序加载新类时,它按照以下顺序在以下位置查找类:

  1. 引导类rt.jar中的运行时类,i18n.jar中的国际化类等。

  2. 已安装扩展:JRE 的lib/ext目录中的 JAR 文件中的类,以及系统范围内的特定于平台的扩展目录(例如在 Solaris™操作系统上的/usr/jdk/packages/lib/ext,但请注意,此目录仅适用于 Java™ 6 及更高版本)。

  3. 类路径:包括系统属性java.class.path指定路径上的类,包括 JAR 文件中的类。如果类路径上的 JAR 文件具有带有Class-Path属性的清单,则还将搜索Class-Path属性指定的 JAR 文件。默认情况下,java.class.path属性的值为.,即当前目录。您可以通过使用-classpath-cp命令行选项或设置CLASSPATH环境变量来更改该值。命令行选项会覆盖CLASSPATH环境变量的设置。

优先级列表告诉您,例如,只有在要加载的类在rt.jari18n.jar或已安装扩展中的类中未找到时,才会搜索类路径。

除非您的软件为特殊目的实例化自己的类加载器,否则您实际上不需要了解比记住这个优先级列表更多的内容。特别是,您应该注意可能存在的任何类名冲突。例如,如果您在类路径上列出一个类,如果运行时环境代替加载了安装的扩展中找到的同名另一个类,您将得到意外的结果。

Java 类加载机制

Java 平台使用委托模型来加载类。基本思想是每个类加载器都有一个“父”类加载器。在加载类时,类加载器首先将类的搜索委托给其父类加载器,然后再尝试找到类本身。

以下是类加载 API 的一些亮点:

  • java.lang.ClassLoader及其子类中的构造函数允许您在实例化新类加载器时指定一个父类加载器。如果您没有明确指定父类加载器,则虚拟机的系统类加载器将被分配为默认父类加载器。

  • 当调用ClassLoader中的loadClass方法加载类时,它按顺序执行以下任务:

    1. 如果类已经被加载,它会返回该类。

    2. 否则,它将搜索新类的任务委托给父类加载器。

    3. 如果父类加载器未找到类,loadClass调用findClass方法来查找和加载类。

  • 如果父类加载器未找到类,则ClassLoaderfindClass方法将在当前类加载器中搜索该类。当您在应用程序中实例化类加载器子类时,可能需要重写此方法。

  • java.net.URLClassLoader用作扩展和其他 JAR 文件的基本类加载器,覆盖了java.lang.ClassLoaderfindClass方法,以在一个或多个指定的 URL 中搜索类和资源。

要查看一个使用与 JAR 文件相关的 API 的示例应用程序,请参阅本教程中的使用与 JAR 相关的 API 课程。

类加载和java命令

Java 平台的类加载机制体现在java命令中。

  • java工具中,-classpath选项是设置java.class.path属性的简便方式。

  • -cp-classpath选项是等效的。

  • -jar选项用于运行打包在 JAR 文件中的应用程序。有关此选项的描述和示例,请参阅本教程中的运行 JAR 打包软件课程。

创建可扩展应用程序

原文:docs.oracle.com/javase/tutorial/ext/basics/spi.html

下面涵盖了以下主题:

  • 介绍

  • 字典服务示例

  • 运行 DictionaryServiceDemo 示例

  • 编译和运行 DictionaryServiceDemo 示例

  • 理解 DictionaryServiceDemo 示例

    1. 定义服务提供者接口

    2. 定义检索服务提供者实现的服务

      • 单例设计模式
    3. 实现服务提供者

    4. 注册服务提供者

    5. 创建使用服务和服务提供者的客户端

    6. 将服务提供者、服务和服务客户端打包在 JAR 文件中

      • 将服务提供者打包在 JAR 文件中

      • 将字典 SPI 和字典服务打包在 JAR 文件中

      • 将客户端打包在 JAR 文件中

    7. 运行客户端

  • ServiceLoader 类

  • ServiceLoader API 的限制

  • 摘要

介绍

可扩展的应用程序是一种可以在不修改其原始代码基础的情况下扩展的应用程序。您可以通过添加新的插件或模块来增强其功能。开发人员、软件供应商和客户可以通过将新的 Java 存档(JAR)文件添加到应用程序类路径或应用程序特定的扩展目录中来添加新功能或应用程序编程接口(API)。

本节描述了如何创建具有可扩展服务的应用程序,这使您或其他人可以提供不需要修改原始应用程序的服务实现。通过设计一个可扩展的应用程序,您提供了一种升级或增强产品特定部分而无需更改核心应用程序的方法。

可扩展应用程序的一个示例是允许最终用户添加新字典或拼写检查器的文字处理器。在这个示例中,文字处理器提供了一个字典或拼写功能,其他开发人员甚至客户可以通过提供自己的功能实现来扩展该功能。

以下是理解可扩展应用程序重要的术语和定义:

服务

一组编程接口和类,提供对某些特定应用功能或特性的访问。服务可以定义功能的接口和检索实现的方法。在文字处理器示例中,字典服务可以定义检索字典和单词定义的方法,但不实现底层功能集。相反,它依赖于服务提供者来实现该功能。

服务提供者接口(SPI)

服务定义的一组公共接口和抽象类。SPI 定义了应用程序可用的类和方法。

服务提供者

实现 SPI。具有可扩展服务的应用程序使您、供应商和客户能够添加服务提供者,而无需修改原始应用程序。

字典服务示例

考虑如何在文字处理器或编辑器中设计一个字典服务。一种方法是定义一个由类DictionaryService和服务提供者接口Dictionary表示的服务。DictionaryService提供一个单例DictionaryService对象。(有关更多信息,请参见单例设计模式部分。)此对象从Dictionary提供者那里检索单词的定义。字典服务客户端——您的应用代码——检索此服务的一个实例,服务将搜索、实例化和使用Dictionary服务提供者。

尽管文字处理器开发人员很可能会在原始产品中提供一个基本的通用字典,但客户可能需要一个包含法律或技术术语的专业字典。理想情况下,客户能够创建或购买新的字典并将其添加到现有应用程序中。

DictionaryServiceDemo示例向您展示如何实现Dictionary服务,创建添加额外字典的Dictionary服务提供者,并创建一个简单的Dictionary服务客户端来测试该服务。此示例打包在 zip 文件DictionaryServiceDemo.zip中,包括以下文件:

  • build.xml

  • DictionaryDemo

    • build.xml

    • build

    • dist

      • DictionaryDemo.jar
    • src

      • dictionary

        • DictionaryDemo.java
  • DictionaryServiceProvider

    • build.xml

    • build

    • dist

      • DictionaryServiceProvider.jar
    • src

      • dictionary

        • DictionaryService.java

        • spi

          • Dictionary.java
  • ExtendedDictionary

    • build.xml

    • build

    • dist

      • ExtendedDictionary.jar
    • src

      • dictionary

        • ExtendedDictionary.java
      • META-INF

        • services

          • dictionary.spi.Dictionary
  • GeneralDictionary

    • build.xml

    • build

    • dist

      • GeneralDictionary.jar
    • src

      • dictionary

        • GeneralDictionary.java
      • META-INF

        • services

          • dictionary.spi.Dictionary

注意build目录包含与src目录中的 Java 源文件相同级别的编译后的类文件。

运行DictionaryServiceDemo示例

因为 zip 文件DictionaryServiceDemo.zip包含编译后的类文件,您可以将此文件解压缩到计算机上,并按照以下步骤运行示例而无需编译:

  1. 下载并解压缩示例代码:将文件DictionaryServiceDemo.zip下载并解压缩到计算机上。这些步骤假定您已将此文件的内容解压缩到目录C:\DictionaryServiceDemo中。

  2. 将当前目录更改为C:\DictionaryServiceDemo\DictionaryDemo,并按照步骤运行客户端进行操作。

编译和运行DictionaryServiceDemo示例

DictionaryServiceDemo示例包含 Apache Ant 构建文件,全部命名为build.xml。以下步骤展示了如何使用 Apache Ant 编译、构建和运行DictionaryServiceDemo示例:

  1. 安装 Apache Ant:前往以下链接下载并安装 Apache Ant:

    [ant.apache.org/](http://ant.apache.org/)

    确保包含 Apache Ant 可执行文件的目录在您的PATH环境变量中,以便您可以从任何目录运行它。此外,请确保您的 JDK 的bin目录,其中包含javajavac可执行文件(对于 Microsoft Windows 为java.exejavac.exe),在您的PATH环境变量中。有关设置PATH环境变量的信息,请参阅 PATH and CLASSPATH。

  2. 下载并解压缩示例代码:将文件DictionaryServiceDemo.zip下载并解压缩到计算机上。这些步骤假定您已将此文件的内容解压缩到目录C:\DictionaryServiceDemo中。

  3. 编译代码:将当前目录更改为C:\DictionaryServiceDemo,并运行以下命令:

    ant compile-all
    

    此命令编译了DictionaryDemoDictionaryServiceProviderExtendedDictionaryGeneralDictionary目录中包含的src目录中的源代码,并将生成的class文件放入相应的build目录中。

  4. 将编译后的 Java 文件打包成 JAR 文件:确保当前目录为C:\DictionaryServiceDemo,然后运行以下命令:

    ant jar
    

    此命令创建以下 JAR 文件:

    • DictionaryDemo/dist/DictionaryDemo.jar

    • DictionaryServiceProvider/dist/DictionaryServiceProvider.jar

    • GeneralDictionary/dist/GeneralDictionary.jar

    • ExtendedDictionary/dist/ExtendedDictionary.jar

  5. 运行示例:确保包含java可执行文件的目录在您的PATH环境变量中。有关更多信息,请参阅 PATH 和 CLASSPATH。

    将当前目录更改为C:\DictionaryServiceDemo\DictionaryDemo,然后运行以下命令:

    ant run
    

    该示例打印以下内容:

    book: a set of written or printed pages, usually bound with a protective cover

    编辑者:一个编辑文档的人

    xml:一种经常用于 Web 服务等的文档标准

    REST:一种用于创建、读取、更新和删除数据的架构风格,试图使用 HTTP 协议的常见词汇;表述性状态转移

理解 DictionaryServiceDemo 示例

以下步骤向您展示如何重新创建文件DictionaryServiceDemo.zip的内容。这些步骤向您展示示例的工作原理以及如何运行它。

1. 定义服务提供者接口

DictionaryServiceDemo示例定义了一个 SPI,即Dictionary.java接口。它只包含一个方法:


package dictionary.spi;

public interface Dictionary {
    public String getDefinition(String word);
}

该示例将编译后的类文件存储在DictionaryServiceProvider/build目录中。

2. 定义检索服务提供者实现的服务

DictionaryService.java类加载并访问可用的Dictionary服务提供者,代表字典服务客户端:


package dictionary;

import dictionary.spi.Dictionary;
import java.util.Iterator;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;

public class DictionaryService {

    private static DictionaryService service;
    private ServiceLoader<Dictionary> loader;

    private DictionaryService() {
        loader = ServiceLoader.load(Dictionary.class);
    }

    public static synchronized DictionaryService getInstance() {
        if (service == null) {
            service = new DictionaryService();
        }
        return service;
    }

    public String getDefinition(String word) {
        String definition = null;

        try {
            Iterator<Dictionary> dictionaries = loader.iterator();
            while (definition == null && dictionaries.hasNext()) {
                Dictionary d = dictionaries.next();
                definition = d.getDefinition(word);
            }
        } catch (ServiceConfigurationError serviceError) {
            definition = null;
            serviceError.printStackTrace();

        }
        return definition;
    }
}

该示例将编译后的类文件存储在DictionaryServiceProvider/build目录中。

DictionaryService类实现了单例设计模式。这意味着DictionaryService类只会创建一个实例。有关更多信息,请参阅单例设计模式部分。

DictionaryService类是字典服务客户端使用任何已安装的Dictionary服务提供者的入口点。使用ServiceLoader.load方法来检索私有静态成员DictionaryService.service,单例服务入口点。然后应用程序可以调用getDefinition方法,该方法遍历可用的Dictionary提供者,直到找到目标词。如果没有Dictionary实例包含指定词的定义,则getDefinition方法返回 null。

字典服务使用ServiceLoader.load方法来查找目标类。SPI 由接口dictionary.spi.Dictionary定义,因此示例使用这个类作为 load 方法的参数。默认的 load 方法使用默认类加载器搜索应用程序类路径。

然而,这个方法的重载版本允许您指定自定义的类加载器。这使您能够进行更复杂的类搜索。一个特别热情的程序员可能会创建一个ClassLoader实例,可以在运行时添加包含提供者 JAR 的应用程序特定子目录中进行搜索。结果是一个应用程序不需要重新启动就可以访问新的提供者类。

当这个类的加载器存在后,您可以使用它的迭代器方法来访问和使用它找到的每个提供者。getDefinition方法使用Dictionary迭代器来遍历提供者,直到找到指定词的定义。迭代器方法缓存Dictionary实例,因此连续调用需要很少的额外处理时间。如果自上次调用以来已经投入使用新的提供者,则迭代器方法将它们添加到列表中。

DictionaryDemo.java 类使用这个服务。要使用该服务,应用程序获取一个DictionaryService实例并调用getDefinition方法。如果有定义可用,应用程序将打印出来。如果没有定义可用,应用程序将打印一条消息,说明没有可用的字典包含这个词。

单例设计模式

设计模式是软件设计中常见问题的一般解决方案。思路是将解决方案转化为代码,并且该代码可以应用在不同的情况下。单例模式描述了一种技术,确保只创建一个类的实例。本质上,该技术采取以下方法:不要让类外部的任何人创建对象的实例。

例如,DictionaryService类实现了单例模式如下:

  • DictionaryService构造函数声明为private,这样除了DictionaryService之外的所有其他类都无法创建它的实例。

  • DictionaryService成员变量service声明为static,这确保只存在一个DictionaryService实例。

  • 定义了getInstance方法,使其他类可以受控地访问DictionaryService成员变量service

3. 实现服务提供程序

要提供此服务,您必须创建一个Dictionary.java的实现。为了保持简单,创建一个定义了几个词的通用词典。您可以使用数据库、一组属性文件或任何其他技术来实现词典。展示提供程序模式的最简单方法是在单个文件中包含所有单词和定义。

以下代码展示了Dictionary SPI 的一个实现,GeneralDictionary.java类。请注意,它提供了一个无参数构造函数,并实现了 SPI 定义的getDefinition方法。


package dictionary;

import dictionary.spi.Dictionary;
import java.util.SortedMap;
import java.util.TreeMap;

public class GeneralDictionary implements Dictionary {

    private SortedMap<String, String> map;

    public GeneralDictionary() {
        map = new TreeMap<String, String>();
        map.put(
            "book",
            "a set of written or printed pages, usually bound with " +
                "a protective cover");
        map.put(
            "editor",
            "a person who edits");
    }

    @Override
    public String getDefinition(String word) {
        return map.get(word);
    }

}

示例将编译后的类文件存储在目录GeneralDictionary/build中。注意:在类GeneralDictionary之前,您必须编译类dictionary.DictionaryServicedictionary.spi.Dictionary

此示例的GeneralDictionary提供程序仅定义了两个词:bookeditor。显然,一个更可用的词典将提供一个更实质的通用词汇列表。

为了演示多个提供程序如何实现相同的 SPI,以下代码展示了另一个可能的提供程序。ExtendedDictionary.java服务提供程序是一个包含大多数软件开发人员熟悉的技术术语的扩展词典。


package dictionary;

import dictionary.spi.Dictionary;
import java.util.SortedMap;
import java.util.TreeMap;

public class ExtendedDictionary implements Dictionary {

        private SortedMap<String, String> map;

    public ExtendedDictionary() {
        map = new TreeMap<String, String>();
        map.put(
            "xml",
            "a document standard often used in web services, among other " +
                "things");
        map.put(
            "REST",
            "an architecture style for creating, reading, updating, " +
                "and deleting data that attempts to use the common " +
                "vocabulary of the HTTP protocol; Representational State " +
                "Transfer");
    }

    @Override
    public String getDefinition(String word) {
        return map.get(word);
    }

}

示例将编译后的类文件存储在目录ExtendedDictionary/build中。注意:在类ExtendedDictionary之前,您必须编译类dictionary.DictionaryServicedictionary.spi.Dictionary

很容易想象客户使用完整的Dictionary提供程序集来满足他们自己的特殊需求。服务加载器 API 使他们能够根据需要或偏好向其应用程序添加新的词典。由于底层的文字处理应用程序是可扩展的,因此客户无需编写额外的代码即可使用新的提供程序。

4. 注册服务提供程序

要注册您的服务提供者,需要创建一个提供者配置文件,该文件存储在服务提供者的 JAR 文件的 META-INF/services 目录中。配置文件的名称是服务提供者的完全限定类名,其中名称的每个组件由句点(.)分隔,嵌套类由美元符号($)分隔。

提供者配置文件包含您的服务提供者的完全限定类名,每个名称占一行。该文件必须使用 UTF-8 编码。此外,您可以通过在注释行前面加上井号(#)来在文件中包含注释。

例如,要注册服务提供者 GeneralDictionary,创建一个名为 dictionary.spi.Dictionary 的文本文件。该文件包含一行:

dictionary.GeneralDictionary

类似地,要注册服务提供者 ExtendedDictionary,创建一个名为 dictionary.spi.Dictionary 的文本文件。该文件包含一行:

dictionary.ExtendedDictionary

5. 创建使用服务和服务提供者的客户端

因为开发完整的文字处理器应用程序是一项重大工作,本教程提供了一个更简单的应用程序,该应用程序使用 DictionaryServiceDictionary SPI。DictionaryDemo 示例从类路径上的任何 Dictionary 提供者中搜索单词 bookeditorxmlREST,并检索它们的定义。

以下是 DictionaryDemo 示例。它从 DictionaryService 实例请求目标单词的定义,然后将请求传递给已知的 Dictionary 提供者。


package dictionary;

import dictionary.DictionaryService;

public class DictionaryDemo {

  public static void main(String[] args) {

    DictionaryService dictionary = DictionaryService.getInstance();
    System.out.println(DictionaryDemo.lookup(dictionary, "book"));
    System.out.println(DictionaryDemo.lookup(dictionary, "editor"));
    System.out.println(DictionaryDemo.lookup(dictionary, "xml"));
    System.out.println(DictionaryDemo.lookup(dictionary, "REST"));
  }

  public static String lookup(DictionaryService dictionary, String word) {
    String outputString = word + ": ";
    String definition = dictionary.getDefinition(word);
    if (definition == null) {
      return outputString + "Cannot find definition for this word.";
    } else {
      return outputString + definition;
    }
  }
}

该示例将编译后的类文件存储在目录 DictionaryDemo/build 中。注意:在运行 DictionaryDemo 类之前,必须先编译类 dictionary.DictionaryServicedictionary.spi.Dictionary

6. 将服务提供者、服务和服务客户端打包到 JAR 文件中

请参阅课程 在 JAR 文件中打包程序 了解如何创建 JAR 文件的信息。

在 JAR 文件中打包服务提供者

要打包 GeneralDictionary 服务提供者,创建一个名为 GeneralDictionary/dist/GeneralDictionary.jar 的 JAR 文件,其中包含此服务提供者的编译类文件和以下目录结构中的配置文件:

  • META-INF

    • 服务

      • dictionary.spi.Dictionary
  • dictionary

    • GeneralDictionary.class

类似地,要打包 ExtendedDictionary 服务提供者,创建一个名为 ExtendedDictionary/dist/ExtendedDictionary.jar 的 JAR 文件,其中包含此服务提供者的编译类文件和以下目录结构中的配置文件:

  • META-INF

    • 服务

      • dictionary.spi.Dictionary
  • dictionary

    • ExtendedDictionary.class

注意,提供者配置文件必须位于 JAR 文件中的META-INF/services目录中。

将 Dictionary SPI 和 Dictionary Service 打包成一个 JAR 文件

创建一个名为DictionaryServiceProvider/dist/DictionaryServiceProvider.jar的 JAR 文件,其中包含以下文件:

  • dictionary

    • DictionaryService.class

    • spi

      • Dictionary.class

将客户端打包成一个 JAR 文件

创建一个名为DictionaryDemo/dist/DictionaryDemo.jar的 JAR 文件,其中包含以下文件:

  • dictionary

    • DictionaryDemo.class

7. 运行客户端

以下命令运行带有GeneralDictionary服务提供者的DictionaryDemo示例:

Linux 和 Solaris:

java -Djava.ext.dirs=../DictionaryServiceProvider/dist:../GeneralDictionary/dist -cp dist/DictionaryDemo.jar dictionary.DictionaryDemo

Windows:

java -Djava.ext.dirs=..\DictionaryServiceProvider\dist;..\GeneralDictionary\dist -cp dist\DictionaryDemo.jar dictionary.DictionaryDemo

使用此命令时,假设以下情况:

  • 当前目录是DictionaryDemo

  • 存在以下 JAR 文件:

    • DictionaryDemo/dist/DictionaryDemo.jar: 包含DictionaryDemo

    • DictionaryServiceProvider/dist/DictionaryServiceProvider.jar: 包含Dictionary SPI 和DictionaryService

    • GeneralDictionary/dist/GeneralDictionary.jar: 包含GeneralDictionary服务提供者和配置文件

该命令打印以下内容:

book: a set of written or printed pages, usually bound with a protective cover
editor: a person who edits
xml: Cannot find definition for this word.
REST: Cannot find definition for this word.

假设你运行以下命令并且ExtendedDictionary/dist/ExtendedDictionary.jar存在:

Linux 和 Solaris:

java -Djava.ext.dirs=../DictionaryServiceProvider/dist:../ExtendedDictionary/dist -cp dist/DictionaryDemo.jar dictionary.DictionaryDemo

Windows:

java -Djava.ext.dirs=..\DictionaryServiceProvider\dist;..\ExtendedDictionary\dist -cp dist\DictionaryDemo.jar dictionary.DictionaryDemo

该命令打印以下内容:

book: Cannot find definition for this word.
editor: Cannot find definition for this word.
xml: a document standard often used in web services, among other things
REST: an architecture style for creating, reading, updating, and deleting data that attempts to use the common vocabulary of the HTTP protocol; Representational State Transfer

ServiceLoader 类

java.util.ServiceLoader类帮助你查找、加载和使用服务提供者。它在应用程序的类路径或运行时环境的扩展目录中搜索服务提供者。它加载它们并使你的应用程序能够使用提供者的 API。如果你将新的提供者添加到类路径或运行时扩展目录中,ServiceLoader类会找到它们。如果你的应用程序知道提供者接口,它可以找到并使用该接口的不同实现。你可以使用接口的第一个可加载实例或遍历所有可用接口。

ServiceLoader类是 final 的,这意味着你不能将其作为子类或覆盖其加载算法。例如,你不能改变其算法以从不同位置搜索服务。

ServiceLoader类的角度来看,所有服务都具有单一类型,通常是单一接口或抽象类。提供者本身包含一个或多个具体类,这些类扩展了服务类型,具有特定于其目的的实现。ServiceLoader类要求单个公开的提供者类型具有默认构造函数,不需要参数。这使得ServiceLoader类可以轻松实例化它找到的服务提供者。

提供者是按需定位和实例化的。服务加载器维护了已加载的提供者的缓存。加载器的iterator方法的每次调用都会返回一个迭代器,首先按实例化顺序产生缓存中的所有元素。然后,服务加载器会定位和实例化任何新的提供者,依次将每个提供者添加到缓存中。您可以使用reload方法清除提供者缓存。

要为特定类创建加载器,请将类本身提供给loadloadInstalled方法。您可以使用默认类加载器或提供自己的ClassLoader子类。

loadInstalled方法搜索已安装的运行时提供者的扩展目录。默认的扩展位置是您运行时环境的jre/lib/ext目录。您应该仅将扩展位置用于知名的、可信任的提供者,因为此位置将成为所有应用程序的类路径的一部分。在本文中,提供者不使用扩展目录,而是依赖于特定于应用程序的类路径。

ServiceLoader API 的局限性

ServiceLoader API 很有用,但也有局限性。例如,不可能从ServiceLoader类派生类,因此无法修改其行为。您可以使用自定义的ClassLoader子类来更改类的查找方式,但ServiceLoader本身无法扩展。此外,当前的ServiceLoader类无法告诉您的应用程序运行时何时有新的提供者可用。此外,您无法向加载器添加更改侦听器,以查找新提供者是否放置在特定于应用程序的扩展目录中。

公共的ServiceLoader API 在 Java SE 6 中可用。虽然加载器服务早在 JDK 1.3 时就存在,但 API 是私有的,只对内部 Java 运行时代码可用。

摘要

可扩展的应用程序提供了可以由服务提供者扩展的服务点。创建可扩展应用程序的最简单方法是使用ServiceLoader,该工具适用于 Java SE 6 及更高版本。使用这个类,您可以将提供者实现添加到应用程序类路径中,以提供新功能。ServiceLoader类是 final 的,因此您无法修改其功能。

posted @ 2024-04-12 15:10  绝不原创的飞龙  阅读(3)  评论(0编辑  收藏  举报