设置策略文件以授予所需的权限。
原文:
docs.oracle.com/javase/tutorial/security/toolsign/rstep3.html
接下来,您将使用策略工具创建一个名为exampleraypolicy
的策略文件,并在其中授予来自已签名 JAR 文件的代码权限。
JAR 文件必须使用与上一步中导入到 Ray 的密钥库(exampleraystore
)中的公钥对应的私钥进行签名。包含公钥的证书在密钥库中被别名为susan
。我们将授予此类代码权限以读取C:\TestData\
目录中的任何文件。
步骤如下:
-
启动策略工具
-
指定密钥库
-
使用 SignedBy 别名添加策略条目
-
保存策略文件
启动策略工具
原文:
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\
目录中所有文件的读取访问权限。您需要
-
指定包含别名为 susan 的证书信息的密钥库
-
创建授予权限的策略条目
密钥库是在将证书导入为受信任的证书步骤中创建的名为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/*
要添加权限,请选择添加权限按钮。这将打开权限对话框。
执行以下操作。
-
从权限下拉列表中选择文件权限。完整的权限类型名称(
java.io.FilePermission
)现在显示在下拉列表右侧的文本框中。 -
在标记为目标名称的列表右侧的文本框中键入以下内容,以指定
C:\TestData\
目录中的所有文件:C:\TestData\*
-
通过从操作下拉列表中选择读取选项来指定读取权限。
现在权限对话框看起来像下面这样。
选择确定按钮。新的权限出现在策略条目对话框中的一行中,如下所示。
注意: 你在文件路径中键入的每个反斜杠都已替换为两个反斜杠,以方便您使用。策略文件中的字符串由一个标记器处理,允许使用 \ 作为转义字符(例如,\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.jar
和exampleraypolicy
文件的目录中键入以下内容:
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 文件中存储数据文件而不是类文件。
作为合同发送方,您需要执行以下步骤。
-
创建包含合同的 JAR 文件,使用
jar
工具。 -
生成密钥(如果尚未存在),使用
keytool
的-genkey
命令。可选步骤:为公钥证书生成证书签名请求(CSR),并导入认证机构的响应。为简单起见,由于您只是假装是 Stan Smith,因此省略了此步骤。有关更多信息,请参见为公钥证书生成证书签名请求(CSR)。
-
对 JAR 文件进行签名,使用
jarsigner
工具和第 2 步生成的私钥。 -
导出公钥证书,使用
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,执行以下步骤。
-
使用
keytool
的-import
命令将证书导入为受信任的证书。 -
使用
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
示例程序。
-
准备初始程序结构
创建一个名为
GenSig.java
的文本文件。输入初始程序结构(导入语句、类名、main
方法等)。 -
生成公钥和私钥
生成一对密钥(公钥和私钥)。私钥用于对数据进行签名。公钥将被
VerSig
程序用于验证签名。 -
对数据进行签名
获取一个
Signature
对象并初始化以进行签名。提供要签名的数据,并生成签名。 -
保存签名和公钥到文件中
将签名字节保存在一个文件中,将公钥字节保存在另一个文件中。
-
编译并运行程序
准备初始程序结构
原文:
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
包,其中包含输入要签名的文件数据所需的方法。 -
期望提供一个参数,指定要签名的数据文件。
-
后续步骤中编写的代码将放在
try
和catch
块之间。
生成公钥和私钥
原文:
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 Specification和Java 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()
,因为它获取已知强算法的实例。
生成密钥对
最后一步是生成密钥对,并将密钥存储在PrivateKey
和PublicKey
对象中。
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
对象用于签名或验证之前,必须进行初始化。签名的初始化方法需要一个私钥。使用前一步中放入名为 priv
的 PrivateKey
对象中的私钥。
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
示例程序以导入文件并验证签名的步骤如下。
-
准备初始程序结构
创建一个名为
VerSig.java
的文本文件。输入初始程序结构(导入语句、类名、main
方法等)。 -
输入并转换编码的公钥字节
从指定为第一个命令行参数的文件中导入编码的公钥字节,并将其转换为
PublicKey
。 -
输入签名字节
从指定为第二个命令行参数的文件中输入签名字节。
-
验证签名
获取一个
Signature
对象并用于验证签名的公钥进行初始化。提供要验证签名的数据(来自指定为第三个命令行参数的文件),并验证签名。 -
编译和运行程序
准备初始程序结构
原文:
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
类的内容。 -
期望有三个参数,分别指定公钥、签名和数据文件。
-
在本课程后续步骤中编写的代码将放在
try
和catch
块之间。
输入并转换编码的公钥字节
原文:
docs.oracle.com/javase/tutorial/security/apisign/vstep2.html
接下来,VerSig
需要从指定为第一个命令行参数的文件中导入编码的公钥字节,并将其转换为PublicKey
。需要一个PublicKey
,因为Signature
的initVerify
方法需要它来初始化用于验证的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
程序添加了代码
-
输入编码的密钥字节并将其转换为名为
pubKey
的PublicKey
-
将签名字节输入到名为
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
本课程中的GenSig
和VerSig
程序演示了使用 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
本课程演示了如何编写一个定义自己特殊权限的类。本课程的基本组件包括:
-
一个名为ExampleGame的示例游戏。
-
一个名为HighScore的类,被
ExampleGame
用来存储用户最新的高分。 -
一个名为HighScorePermission的类,用于保护对用户存储的高分值的访问。
-
用户的安全策略文件,授予
ExampleGame
更新他/她的高分的权限。
基本场景如下:
-
用户玩
ExampleGame
。 -
如果用户达到新的高分,
ExampleGame
使用HighScore
类来保存这个新值。 -
HighScore
类查看用户的安全策略,以检查ExampleGame
是否有权限更新用户的高分值。 -
如果
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
类必须:
-
调用
System.getSecurityManager()
以获取当前安装的安全管理器。 -
如果结果不为空(也就是说,存在一个安全管理器,而不是调用者是一个无限制的应用程序),那么
-
构造一个
HighScorePermission
对象,并 -
调用安全管理器的
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.Permission
或 java.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"
的密钥库条目中。 -
HighScore
和HighScorePermissions
类是由实现它们的人(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. 读写高分文件本身的权限。
这个权限是为了让HighScore
的getHighScore
和 setHighScore
方法可以访问用户的.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
克里斯在创建HighScore
和HighScorePermission
类之后将采取的步骤是:
编译这些类
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
, -
HighScore
和HighScorePermission
类在策略文件中必须被授予的权限信息,以便能够正常工作。对于这一点,克里斯可以提供所需的确切授权条目。
示例游戏开发者(Terry)的步骤
原文:
docs.oracle.com/javase/tutorial/security/userperm/terry.html
Terry 创建一个调用 HighScore
的 getHighScore
和 setHighScore
方法来获取和设置用户高分的游戏(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
, 和 -
有关
HighScore
和HighScorePermission
类在策略文件中必须被授予的权限的信息,以便其正常工作。这可能是所需的确切授权条目。
运行 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 位置,因此假定密钥库与策略文件位于同一目录中。
教程:扩展机制
扩展机制提供了一种标准、可扩展的方式,使自定义 API 对在 Java 平台上运行的所有应用程序可用。Java 扩展也被称为可选包。本教程可能会交替使用这两个术语。
扩展是通过扩展机制增强 Java 平台的一组包和类。扩展机制使运行时环境能够找到并加载扩展类,而无需在类路径上命名扩展类。在这方面,扩展类类似于 Java 平台的核心类。这也是扩展得名的原因--它们实际上扩展了平台的核心 API。
由于此机制扩展了平台的核心 API,应谨慎使用。最常见的用途是用于由 Java 社区流程定义的标准化接口,尽管也可能适用于站点范围接口。
如图所示,扩展充当 Java 平台的“附加”模块。它们的类和公共 API 自动对在平台上运行的任何应用程序可用。
扩展机制还提供了一种从远程位置下载扩展类供 applets 使用的方法。
扩展被打包为 Java 存档(JAR)文件,本教程假定您熟悉 JAR 文件格式。如果您对 JAR 文件不熟悉,您可能需要在继续本教程的课程之前查阅一些 JAR 文件文档:
-
本教程中的在 JAR 文件中打包程序课程。
-
JDK™文档中的JAR 指南。
本教程有两个课程:
创建和使用扩展
这一部分向您展示了如何向您的 Java 平台添加扩展,并且 applets 如何通过下载远程扩展类从扩展机制中受益。
使扩展安全
本节描述了在您的平台上授予扩展的安全特权和权限。如果您正在编写自己的扩展类,您将了解如何使用 Java 平台的安全架构。
附加文档
您可以在 JDK 文档的Java 扩展机制部分找到有关扩展的更多信息。
教程:创建和使用扩展
任何一组包或类都可以轻松地扮演扩展的角色。将一组类转变为扩展的第一步是将它们打包在一个 JAR 文件中。完成这一步后,您可以通过两种方式将软件转变为扩展:
-
通过将 JAR 文件放置在 Java 运行时环境目录结构的特定位置,这种情况下称为已安装扩展。
-
通过以特定方式从另一个 JAR 文件的清单中引用 JAR 文件,这种情况下称为下载扩展。
本课将通过使用一个简单的“玩具”扩展作为示例来展示扩展机制的工作原理。
已安装扩展
在本节中,您将创建一个简单的已安装扩展,并看到扩展软件如何被运行时环境视为平台的一部分。
下载扩展
本节将向您展示如何修改 JAR 文件的清单,以便 JAR 打包的软件可以利用下载扩展。
理解扩展类加载
本节是一个简短的插曲,总结了 Java 平台的类加载委托模型,并展示了它与扩展中类加载的关系。
创建可扩展应用程序
本节讨论了用于扩展应用程序的机制,通过插件或模块,而无需修改其原始代码库。
下一课,使扩展安全 使用相同的扩展来展示 Java 平台如何控制授予扩展的安全权限。
已安装的扩展
已安装的扩展是 JRE™软件的lib/ext
目录中的 JAR 文件。顾名思义,JRE 是 Java 开发工具包的运行时部分,包含平台的核心 API,但不包括编译器和调试器等开发工具。JRE 可以单独使用,也可以作为 Java 开发工具包的一部分使用。
JRE 是 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。
下载扩展
下载扩展是 JAR 文件中的一组类(和相关资源)。JAR 文件的清单可以包含引用一个或多个下载扩展的头部。这些扩展可以通过以下两种方式引用:
-
通过
Class-Path
头部 -
通过
Extension-List
头部
请注意,清单中最多只允许一个。通过Class-Path
头部指示的下载扩展仅在下载它们的应用程序(如 Web 浏览器)的生命周期内下载。它们的优点是客户端上没有安装任何内容;缺点是每次需要时都会下载它们。通过Extension-List
头部下载的下载扩展将安装到下载它们的 JRE 的/lib/ext
目录中。它们的优点是第一次需要时下载,随后可以在不下载的情况下使用。但是,正如本教程后面所示,它们部署起来更加复杂。
由于使用Class-Path
头部的下载扩展更简单,让我们先考虑它们。例如假设a.jar
和b.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
您将被要求输入密钥库和密钥密码。有关keytool
、jarsigner
和其他安全工具的更多信息,请参阅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),情况也是如此。
理解扩展类加载
扩展框架利用了类加载委托机制。当运行时环境需要为应用程序加载新类时,它按照以下顺序在以下位置查找类:
-
引导类:
rt.jar
中的运行时类,i18n.jar
中的国际化类等。 -
已安装扩展:JRE 的
lib/ext
目录中的 JAR 文件中的类,以及系统范围内的特定于平台的扩展目录(例如在 Solaris™操作系统上的/usr/jdk/packages/lib/ext
,但请注意,此目录仅适用于 Java™ 6 及更高版本)。 -
类路径:包括系统属性
java.class.path
指定路径上的类,包括 JAR 文件中的类。如果类路径上的 JAR 文件具有带有Class-Path
属性的清单,则还将搜索Class-Path
属性指定的 JAR 文件。默认情况下,java.class.path
属性的值为.
,即当前目录。您可以通过使用-classpath
或-cp
命令行选项或设置CLASSPATH
环境变量来更改该值。命令行选项会覆盖CLASSPATH
环境变量的设置。
优先级列表告诉您,例如,只有在要加载的类在rt.jar
、i18n.jar
或已安装扩展中的类中未找到时,才会搜索类路径。
除非您的软件为特殊目的实例化自己的类加载器,否则您实际上不需要了解比记住这个优先级列表更多的内容。特别是,您应该注意可能存在的任何类名冲突。例如,如果您在类路径上列出一个类,如果运行时环境代替加载了安装的扩展中找到的同名另一个类,您将得到意外的结果。
Java 类加载机制
Java 平台使用委托模型来加载类。基本思想是每个类加载器都有一个“父”类加载器。在加载类时,类加载器首先将类的搜索委托给其父类加载器,然后再尝试找到类本身。
以下是类加载 API 的一些亮点:
-
java.lang.ClassLoader
及其子类中的构造函数允许您在实例化新类加载器时指定一个父类加载器。如果您没有明确指定父类加载器,则虚拟机的系统类加载器将被分配为默认父类加载器。 -
当调用
ClassLoader
中的loadClass
方法加载类时,它按顺序执行以下任务:-
如果类已经被加载,它会返回该类。
-
否则,它将搜索新类的任务委托给父类加载器。
-
如果父类加载器未找到类,
loadClass
调用findClass
方法来查找和加载类。
-
-
如果父类加载器未找到类,则
ClassLoader
的findClass
方法将在当前类加载器中搜索该类。当您在应用程序中实例化类加载器子类时,可能需要重写此方法。 -
类
java.net.URLClassLoader
用作扩展和其他 JAR 文件的基本类加载器,覆盖了java.lang.ClassLoader
的findClass
方法,以在一个或多个指定的 URL 中搜索类和资源。
要查看一个使用与 JAR 文件相关的 API 的示例应用程序,请参阅本教程中的使用与 JAR 相关的 API 课程。
类加载和java
命令
Java 平台的类加载机制体现在java
命令中。
-
在
java
工具中,-classpath
选项是设置java.class.path
属性的简便方式。 -
-cp
和-classpath
选项是等效的。 -
-jar
选项用于运行打包在 JAR 文件中的应用程序。有关此选项的描述和示例,请参阅本教程中的运行 JAR 打包软件课程。
创建可扩展应用程序
下面涵盖了以下主题:
-
介绍
-
字典服务示例
-
运行 DictionaryServiceDemo 示例
-
编译和运行 DictionaryServiceDemo 示例
-
理解 DictionaryServiceDemo 示例
-
定义服务提供者接口
-
定义检索服务提供者实现的服务
- 单例设计模式
-
实现服务提供者
-
注册服务提供者
-
创建使用服务和服务提供者的客户端
-
将服务提供者、服务和服务客户端打包在 JAR 文件中
-
将服务提供者打包在 JAR 文件中
-
将字典 SPI 和字典服务打包在 JAR 文件中
-
将客户端打包在 JAR 文件中
-
-
运行客户端
-
-
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
包含编译后的类文件,您可以将此文件解压缩到计算机上,并按照以下步骤运行示例而无需编译:
-
下载并解压缩示例代码:将文件
DictionaryServiceDemo.zip
下载并解压缩到计算机上。这些步骤假定您已将此文件的内容解压缩到目录C:\DictionaryServiceDemo
中。 -
将当前目录更改为
C:\DictionaryServiceDemo\DictionaryDemo
,并按照步骤运行客户端进行操作。
编译和运行DictionaryServiceDemo
示例
DictionaryServiceDemo
示例包含 Apache Ant 构建文件,全部命名为build.xml
。以下步骤展示了如何使用 Apache Ant 编译、构建和运行DictionaryServiceDemo
示例:
-
安装 Apache Ant:前往以下链接下载并安装 Apache Ant:
[
ant.apache.org/](http://ant.apache.org/)
确保包含 Apache Ant 可执行文件的目录在您的
PATH
环境变量中,以便您可以从任何目录运行它。此外,请确保您的 JDK 的bin
目录,其中包含java
和javac
可执行文件(对于 Microsoft Windows 为java.exe
和javac.exe
),在您的PATH
环境变量中。有关设置PATH
环境变量的信息,请参阅 PATH and CLASSPATH。 -
下载并解压缩示例代码:将文件
DictionaryServiceDemo.zip
下载并解压缩到计算机上。这些步骤假定您已将此文件的内容解压缩到目录C:\DictionaryServiceDemo
中。 -
编译代码:将当前目录更改为
C:\DictionaryServiceDemo
,并运行以下命令:ant compile-all
此命令编译了
DictionaryDemo
、DictionaryServiceProvider
、ExtendedDictionary
和GeneralDictionary
目录中包含的src
目录中的源代码,并将生成的class
文件放入相应的build
目录中。 -
将编译后的 Java 文件打包成 JAR 文件:确保当前目录为
C:\DictionaryServiceDemo
,然后运行以下命令:ant jar
此命令创建以下 JAR 文件:
-
DictionaryDemo/dist/DictionaryDemo.jar
-
DictionaryServiceProvider/dist/DictionaryServiceProvider.jar
-
GeneralDictionary/dist/GeneralDictionary.jar
-
ExtendedDictionary/dist/ExtendedDictionary.jar
-
-
运行示例:确保包含
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.DictionaryService
和dictionary.spi.Dictionary
。
此示例的GeneralDictionary
提供程序仅定义了两个词:book和editor。显然,一个更可用的词典将提供一个更实质的通用词汇列表。
为了演示多个提供程序如何实现相同的 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.DictionaryService
和dictionary.spi.Dictionary
。
很容易想象客户使用完整的Dictionary
提供程序集来满足他们自己的特殊需求。服务加载器 API 使他们能够根据需要或偏好向其应用程序添加新的词典。由于底层的文字处理应用程序是可扩展的,因此客户无需编写额外的代码即可使用新的提供程序。
4. 注册服务提供程序
要注册您的服务提供者,需要创建一个提供者配置文件,该文件存储在服务提供者的 JAR 文件的 META-INF/services
目录中。配置文件的名称是服务提供者的完全限定类名,其中名称的每个组件由句点(.
)分隔,嵌套类由美元符号($
)分隔。
提供者配置文件包含您的服务提供者的完全限定类名,每个名称占一行。该文件必须使用 UTF-8 编码。此外,您可以通过在注释行前面加上井号(#
)来在文件中包含注释。
例如,要注册服务提供者 GeneralDictionary
,创建一个名为 dictionary.spi.Dictionary
的文本文件。该文件包含一行:
dictionary.GeneralDictionary
类似地,要注册服务提供者 ExtendedDictionary
,创建一个名为 dictionary.spi.Dictionary
的文本文件。该文件包含一行:
dictionary.ExtendedDictionary
5. 创建使用服务和服务提供者的客户端
因为开发完整的文字处理器应用程序是一项重大工作,本教程提供了一个更简单的应用程序,该应用程序使用 DictionaryService
和 Dictionary
SPI。DictionaryDemo
示例从类路径上的任何 Dictionary
提供者中搜索单词 book、editor、xml 和 REST,并检索它们的定义。
以下是 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.DictionaryService
和 dictionary.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
方法清除提供者缓存。
要为特定类创建加载器,请将类本身提供给load
或loadInstalled
方法。您可以使用默认类加载器或提供自己的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 的,因此您无法修改其功能。