本文翻译自Oracle官方文档,原文地址

Java™ Archive (JAR) 文件格式使你能够把多个文件打包进一个归档文件中,一个Jar文件通常包括多个类文件和任意数量的资源文件。

使用Jar文件格式,我们可以获得这些好处:

  • 安全:你可以对Jar文件内容进行数字签名,识别签名的用户可以有选择地授予你的软件一些权限,否则他不会这么做地(不相信你的软件)
  • 减少下载时间:如果你的Applet被打包成一个Jar文件,那么浏览器只需要一个http连接即可下载Applet中的各种类文件和资源文件
  • 压缩:Jar格式支持对文件进行压缩,节约存储空间
  • 扩展性:扩展框架提供了一种向 Java 核心平台添加功能的方法,JAR 文件格式定义了扩展的打包。通过使用 JAR 文件格式,你可以将你的JAR文件变成扩展包。扩展框架介绍
  • 包密封:存储在 JAR 文件中的包可以被密封,以便强制包版本保存一致性。在 JAR 文件中密封包意味着必须在同一个 JAR 文件中找到在该包中定义的所有类。
  • 包版本控制:JAR 文件可以保存有关其包含的文件的数据,例如供应商和版本信息
  • 可移植性:Jar文件处理机制时Java平台核心API的标准部分
1. 如何使用Jar包

JAR 文件采用 ZIP 文件格式打包,所以你可以对Jar文件执行数据压缩、存档、解压缩和存档解包等操作。JDK提供一个名字Java Archive Tool的工具,来帮助我们完成这些操作,由于Java Archive Tool工具通过 jar 命令来执行,因此又被叫做Jar工具

下面的表描述了jar的常见操作

操作 命令
创建Jar文件 jar cf jar-file input-file(s)
查看Jar文件内容 jar tf jar-file
解压Jar文件 jar xf jar-file
解压Jar包中的特定文件 jar xf jar-file archived-file(s)
运行Jar包 (manifest文件中必须存在 Main-class 头信息) java -jar app.jar

1.1 创建JAR文件

命令格式

jar cf jar-file input-file(s)

命令选项说明:

  • c表示要创建JAR文件
  • f表示输出到文件而不是标准输出
  • jar-file表示JAR文件名称
  • input-file(s)表示要添加到JAR包中的文件,多个文件用逗号分割;input-file(s)可以包含通配符;如果input-file(s)是目录,那么目录下文件都会被添加到Jar包中(递归处理目录)

cf的顺序没有要求,但是二者之间不能有空格。

执行该命令后,会在当前目录下生成一个压缩的JAR文件,该命令会生成一个默认的manifest文件(在JAR中的META-INF目录下)。

注意:JAR 文件中的元数据(例如条目名称、注释和清单内容)必须以 UTF8 编码

除了cf外,还有一些选项:

Option Description
v 打印JAR文件构建过程中的详细信息,比如添加到JAR包中的文件名称
0 (zero) 不要对文件进行压缩
M 不要生成默认的manifest文件
m 用于合并已有的manifest文件内容,格式:jar cmf jar-file existing-manifest input-file(s)
-C 改变命令执行时的目录

注意:生成的JAR文件中回包含创建时间,因此,当你重复创建JAR文件时,即使文件内容不变,但生成的JAR也不会完全相同;推荐在manifest中使用版本信息而不是创建时间来控制JAR文件的版本。

命令使用示例

假设我们有一个叫TicTacToe的java applet,它包含一个java类文件,一些音频文件和一些图片,结构如下图所示:

Packaging Programs in JAR Files_Java

audio和image目录下面存放的是一些音频文件和一些图片。

进入TicTacToe目录,执行下面这个命令,把所有文件打包至一个名为TicTacToe.jar的文件中。

jar cvf TicTacToe.jar TicTacToe.class audio images

audio和image是目录,jar工具会进行递归处理,把目录下的文件全部放入JAR文件中,生成的JAR文件位于当前目录。由于使用了v选项,因此会看到类似下面的输出

adding: TicTacToe.class (in=3825) (out=2222) (deflated 41%)
adding: audio/ (in=0) (out=0) (stored 0%)
adding: audio/beep.au (in=4032) (out=3572) (deflated 11%)
adding: audio/ding.au (in=2566) (out=2055) (deflated 19%)
adding: audio/return.au (in=6558) (out=4401) (deflated 32%)
adding: audio/yahoo1.au (in=7834) (out=6985) (deflated 10%)
adding: audio/yahoo2.au (in=7463) (out=4607) (deflated 38%)
adding: images/ (in=0) (out=0) (stored 0%)
adding: images/cross.gif (in=157) (out=160) (deflated -1%)
adding: images/not.gif (in=158) (out=161) (deflated -1%)

从上面的输出结果可以看出文件被压缩了,Jar工具默认会文件进行压缩,你可以使用0选项关闭此行为:

jar cvf0 TicTacToe.jar TicTacToe.class audio images

某些情况下,你可能想禁制文件的压缩,比如提高浏览器加载JAR文件的速度,未压缩的文件通常加载的更快,因为解压文件需要时间的。凡事有利也有弊,禁止压缩会会导致文件下载时间更长(因为文件更大了 )。

Jar工具接受通配符*作为参数,如果你压缩的当前目录下的所有文件,你可以使用这个命令创建JAR包:

jar cvf TicTacToe.jar *

Jar工具会自动生成一个manifest文件,路径为META-INF/MANNIREST.MF.

在上面的例子中,JAR包中的文件仍然保持原来的目录结构和名称。你可以使用C选项来改变文件在JAR包中的路径。

假如你想要 音频文件和图像文件放在JAR中的一级目录下,你可以使用下面这个命令来创建JAR包:

jar cf ImageAudio.jar -C images . -C audio .

-C images 指示Jar工具进入images目录下,而 -C images 后面的 . 指示Jar工具将images目录的文件全部放在当前文件夹下,-C audio . 的处理逻辑类似,生成的JAR包文件内容如下:

META-INF/MANIFEST.MF
cross.gif
not.gif
beep.au
ding.au
return.au
yahoo1.au
yahoo2.au

相比之下,如果你不使用-C选项,jar cf ImageAudio.jar images audio,生成的JAR包内容如下:

META-INF/MANIFEST.MF
images/cross.gif
images/not.gif
audio/beep.au
audio/ding.au
audio/return.au
audio/yahoo1.au
audio/yahoo2.au

1.2 查看JAR文件内容

查看JAR文件内容的命令格式:

jar tf jar-file

命令选项说明:

  • t表示要查看JAR文件内容
  • f表示在命令行上指定要查看的JAR文件
  • jar-file参数表示JAR文件的路径和名称

tf的顺序没有要求,但是二者之间不能有空格。

该命令会将JAR文件的目录内容输出到标准输出上。

你可以使用v选项,输出更多信息,包括文件大小,最后的修改时间。

命令使用示例

假设我们要查看之间创建的TicTacToe.jar文件的内容:

jar tf TicTacToe.jar

输出结果如下:

META-INF/MANIFEST.MF
TicTacToe.class
audio/
audio/beep.au
audio/ding.au
audio/return.au
audio/yahoo1.au
audio/yahoo2.au
images/
images/cross.gif
images/not.gif

其中不仅有类文件TicTacToe、audio、images等目录,还有自动生成的manifest文件META-INF/MANIFEST.MF

不管你使用的是哪种平台或操作系统,输出的文件路径都是用正斜杠/。JAR文件中的路径总是相对的,你不会看到类似C:的开头。

如果你使用v选项,jar tvf TicTacToe.jar,将看到更多详细的输出内容:

68 Thu Nov 01 20:00:40 PDT 2012 META-INF/MANIFEST.MF
   553 Mon Sep 24 21:57:48 PDT 2012 TicTacToe.class
  3708 Mon Sep 24 21:57:48 PDT 2012 TicTacToe.class
  9584 Mon Sep 24 21:57:48 PDT 2012 TicTacToe.java
     0 Mon Sep 24 21:57:48 PDT 2012 audio/
  4032 Mon Sep 24 21:57:48 PDT 2012 audio/beep.au
  2566 Mon Sep 24 21:57:48 PDT 2012 audio/ding.au
  6558 Mon Sep 24 21:57:48 PDT 2012 audio/return.au
  7834 Mon Sep 24 21:57:48 PDT 2012 audio/yahoo1.au
  7463 Mon Sep 24 21:57:48 PDT 2012 audio/yahoo2.au
   424 Mon Sep 24 21:57:48 PDT 2012 example1.html
     0 Mon Sep 24 21:57:48 PDT 2012 images/
   157 Mon Sep 24 21:57:48 PDT 2012 images/cross.gif
   158 Mon Sep 24 21:57:48 PDT 2012 images/not.gif

1.3 抽取JAR包中的文件

抽取文件命令如下:

jar xf jar-file [archived-file(s)]

说明:

  • x 选项表示要抽取JAR包中的文件
  • f 表示要抽取的JAR包文件是通过命令行参数指定的,而不是标准输入
  • jar-file 参数是指要抽取的JAR包文件名称
  • archived-file(s) 是一个可选的参数,表示要抽取的目标文件,多个文件用英文逗号分割;如果不指定该参数,则抽取JAR包中的所有文件

xf 的顺序没有要求,但是二者之间不能有空格。

Jar工具会把抽取的文件放到当前工作目录下,并保留他们在JAR包中的目录结构,原始的JAR包不会有任何改变。

注意:抽取JAR包中的文件时,会覆盖当前目录下的同名文件

命令使用示例

上面我们创建的TicTacToe JAR包的文件内容如下:

META-INF/MANIFEST.MF
TicTacToe.class
TicTacToe.class
TicTacToe.java
audio/
audio/beep.au
audio/ding.au
audio/return.au
audio/yahoo1.au
audio/yahoo2.au
example1.html
images/
images/cross.gif
images/not.gif

假设我们想抽取出TicTacToe类文件和images目录中的cross.gif文件,你可以使用下面这个命令:

jar xf TicTacToe.jar TicTacToe.class images/cross.gif

该命令做两件事:

  • 拷贝TicTacToe.class文件至当前目录
  • 创建images目录(如果它不存在),拷贝cross.git文件至images目录下

原始的TicTacToe JAR包不会有任何改变。

如果你想抽取JAR包中的全部文件,使用下面这个命令:

jar xf TicTacToe.jar

1.4 更新JAR文件

你可以使用u 选项来修改JAR包中的清单文件manifest,或者向JAR包中添加文件。

命令格式如下:

jar uf jar-file input-file(s)

说明:

  • u 表示你想要更新存在的JAR包
  • f 表示在命令行上指定要更新的 JAR 文件
  • jar-file 指要更新的JAR包
  • input-file(s) 指要添加到JAR包中的文件,多个文件用空格分割

注意:JAR包中的同名文件会被覆盖。

命令使用实例

我们的 TicTacToe.jar文件内容如下:

META-INF/MANIFEST.MF
TicTacToe.class
TicTacToe.class
TicTacToe.java
audio/
audio/beep.au
audio/ding.au
audio/return.au
audio/yahoo1.au
audio/yahoo2.au
example1.html
images/
images/cross.gif
images/not.gif

现在让我们来把一个新文件 images/new.gif 添加到JAR包中,命令如下:

jar uf TicTacToe.jar images/new.gif

现在 TicTacToe.jar文件内容变成了这样:

META-INF/MANIFEST.MF
TicTacToe.class
TicTacToe.class
TicTacToe.java
audio/
audio/beep.au
audio/ding.au
audio/return.au
audio/yahoo1.au
audio/yahoo2.au
example1.html
images/
images/cross.gif
images/not.gif
images/new.gif

可以看到,最后一行是我们刚才新加入的文件。

你也可以使用-C选项改变文件目录:

jar uf TicTacToe.jar -C images new.gif

现在的文件内容变成了下面这样:

META-INF/MANIFEST.MF
META-INF/MANIFEST.MF
TicTacToe.class
TicTacToe.class
TicTacToe.java
audio/
audio/beep.au
audio/ding.au
audio/return.au
audio/yahoo1.au
audio/yahoo2.au
example1.html
images/
images/cross.gif
images/not.gif
new.gif

注意:new.gif文件在最外层了,不是在images目录下。

1.5 运行JAR文件

我们可以使用JAVA启动器(java命令),执行打包后的JAR文件:

java -jar jar-file

你只能指定一个jar文件。

在执行运行命令前,你必须确保在JAR包中的manifest文件中指定了应用的运行入口,格式如下:

Main-Class: classname

classname就是我们的java应用运行入口类,其中包含一个main方法。关于manifest文件,后面会详细说明。

设置好Main-Class后,我们就可以运行JAR包了:

java -jar app.jar

如果要运行的jar包在其它目录下,你必须指定完整的路径,像这样:java -jar path/app.jar

1.6 命令详细说明文档

window平台:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/jar.html

Linux平台:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jar.html

2. Manifest文件使用说明

JAR文件支持各种功能,包含电子签名、版本控制、包密封等等。是什么让 JAR 文件具有这种多功能性?答案是 JAR 文件的清单。

manifest是一个特殊文件,包含有关打包在 JAR 文件中的文件信息。通过定制manifest中包含的“元”信息,您可以使 JAR 文件用于多种用途。

2.1 默认的manifest文件

通过jar命令创建的JAR文件时,它会自动生成一个manifest文件。JAR文件中只能有一个menifest文件,且路径为:META-INF/MANIFEST.MF

默认的manifest文件内容大概长这样:

Manifest-Version: 1.0
Created-By: 1.7.0_06 (Oracle Corporation)

manifest文件内容格式为header: value

manifest中也可以包含JAR文件中其它的文件信息,这取决于你如何使用JAR文件。

2.2 修改manifest文件

创建JAR文件时,可以通过m选项向manifest文件中添加自定义信息。

你可以通过修改向menifest文件,来开启一些特殊功能,比如说包密封。通常,修改manifest文件涉及添加具有特定用途的header数据,以允许 JAR 文件执行特定的所需功能。

修改manifest文件的第一步是,创建一个txt文件,其中包含你要添加的条目(header: value),然后使用m选项将文件内容添加到manifest中。

注意:txt文件必须以换行符或者回车符结束,否则将不能被正确的解析。

命令格式如下:

jar cfm jar-file manifest-addition input-file(s)

解释说明:

  • c表示要创建JAR文件
  • f指定文件名称
  • m表示要合并存在的文件内容至manifest文件中
  • jar-file表示要创建的JAR文件名称
  • manifest-addition表示要合并的文件
  • input-file(s)表示要添加到JAR文件中的文件

注意:manifest的内容必须以 UTF-8 编码。

2.3 设置应用(JAR文件)执行入口

如果你有一个打包成JAR文件的应用,那么你需要以某种方式指定JAR包中某个文件作为应用的入口。你可以在manifest文件中提供一个Main-Class作为header的条目,格式为:

Main-Class: classname

其中classname是你的入口点(位于JAR文件中)的全限定名。入口点是一个具有签名 public static void main(String[] args) 的方法的类。

设置完Main-Class后,就可以使用java命令执行改JAR文件了:

java -jar myapp.jar

使用示例1

假设我们有一个含有main方法的类文件MyClass,其位于包MyPackage下。

首先,创建一个名称Manifest.txt的文件,内容为:

Main-Class: MyPackage.MyClass

注意:文件必须以换行符或者回车符结束,否则将不能被正确的解析。

然后执行命令生成JAR文件:

jar cfm MyJar.jar Manifest.txt MyPackage/*.class

MyJar.jar文件中的清单文件内容为:

Manifest-Version: 1.0
Created-By: 1.7.0_06 (Oracle Corporation)
Main-Class: MyPackage.MyClass

最后,你可以执行下面的命令,允许该JAR包:

java -jar MyJar.jar

使用示例2

创建JAR文件时,我们可以使用e选项,覆盖Main-Class属性的内容,比如:

jar cfe app.jar MyApp MyApp.class

Main-Class 的值为MyApp,现在你可以直接执行app.jar:

java -jar app.jar

如果入口点类位于包下,则需要使用.(点)字符作为分隔符。例如, Main.class 位于名为 foo 的包中,则可以通过以下方式指定入口点:

jar cfe Main.jar foo.Main foo/Main.class

2.4 在JAR文件的ClassPath中添加类

有时你可能需要在JAR文件中引用其它JAR文件中的类,在manifest文件中存在一个名叫Class-Path的header,可以帮助我们实现这个需求,格式如下:

Class-Path: jar1-name jar2-name directory-name/jar3-name

通过Class-Path,你可以避免使用-classpath

注意:Class-Path 指向本地网络上的类或 JAR 文件,而不是 JAR 文件中的 JAR 文件或可通过 Internet 协议访问的类。要将 JAR 文件中 JAR 文件中的类加载到类路径中,您必须编写自定义代码来加载这些类。例如,如果 MyJar.jar 包含另一个名为 MyUtils.jar 的 JAR 文件,则不能使用 MyJar.jar 清单中的 Class-Path 标头将 MyUtils.jar 中的类加载到类路径中。

使用示例

假如我们想在MyJar.jar中加载MyUtils.jar中的类,注意:这两个JAR在相同目录下

首先,创建一个名叫Manifest.txt的文件,内容如下:

Class-Path: MyUtils.jar

注意:文件必须以换行符或者回车符结束,否则将不能被正确的解析。

然后,使用下面的命令创建JAR文件:

jar cfm MyJar.jar Manifest.txt MyPackage/*.class

这将创建带有清单的 JAR 文件,其中包含以下内容:

Manifest-Version: 1.0
Class-Path: MyUtils.jar
Created-By: 1.7.0_06 (Oracle Corporation)

当你运行 MyJar.jar 时,MyUtils.jar 中的类现在已加载到类路径中。

2.5 设置包版本信息

您可能需要在 JAR 文件的清单中包含包版本信息。您在清单中使用以下标头提供此信息:

Header Definition
Name The name of the specification.
Specification-Title The title of the specification.
Specification-Version The version of the specification.
Specification-Vendor The vendor of the specification.
Implementation-Title The title of the implementation.
Implementation-Version The build number of the implementation.
Implementation-Vendor The vendor of the implementation.

可以为每个包分配一组此类标题。版本header应直接出现在包的 Name header下方。此示例显示所有版本控制标头:

Name: java/util/
Specification-Title: Java Utility Classes
Specification-Version: 1.2
Specification-Vendor: Example Tech, Inc.
Implementation-Title: java.util
Implementation-Version: build57
Implementation-Vendor: Example Tech, Inc.

使用示例

我们希望在 MyJar.jar 的清单中包含上面示例中的标头。

我们首先创建一个名为 Manifest.txt 的文本文件,内容如下:

Name: java/util/
Specification-Title: Java Utility Classes
Specification-Version: 1.2
Specification-Vendor: Example Tech, Inc.
Implementation-Title: java.util 
Implementation-Version: build57
Implementation-Vendor: Example Tech, Inc.

注意:文件必须以换行符或者回车符结束,否则将不能被正确的解析。

然后我们通过输入以下命令创建一个名为 MyJar.jar 的 JAR 文件:

jar cfm MyJar.jar Manifest.txt MyPackage/*.class

这将创建带有清单的 JAR 文件,其中包含以下内容:

Manifest-Version: 1.0
Created-By: 1.7.0_06 (Oracle Corporation)
Name: java/util/
Specification-Title: Java Utility Classes
Specification-Version: 1.2
Specification-Vendor: Example Tech, Inc.
Implementation-Title: java.util 
Implementation-Version: build57
Implementation-Vendor: Example Tech, Inc.

2.6 在 JAR 文件中密封包

JAR 文件中的包可以选择密封,这意味着该包中定义的所有类必须存档在同一个 JAR 文件中。例如,您可能想要密封一个包,以确保软件中类之间的版本一致性。

您可以通过在清单文件中添加 Sealed 头信息来密封 JAR 文件中的包,一般形式为:

Name: myCompany/myPackage/
Sealed: true

其中myCompany/myPackage/ 是要密封的包的名称。注意:包名称必须以“/”结尾。

使用示例

我们要在 JAR 文件 MyJar.jar 中密封两个包 firstPackage 和 secondPackage。

我们首先创建一个名为 Manifest.txt 的文本文件,内容如下:

Name: myCompany/firstPackage/
Sealed: true

Name: myCompany/secondPackage/
Sealed: true

注意:文件必须以换行符或者回车符结束,否则将不能被正确的解析。

然后我们通过输入以下命令创建一个名为 MyJar.jar 的 JAR 文件:

jar cfm MyJar.jar Manifest.txt MyPackage/*.class

这将创建带有清单的 JAR 文件,其中包含以下内容:

Manifest-Version: 1.0
Created-By: 1.7.0_06 (Oracle Corporation)
Name: myCompany/firstPackage/
Sealed: true
Name: myCompany/secondPackage/
Sealed: true

密封 JAR 文件

如果要保证包中的所有类都来自相同的代码源,请使用 JAR 密封。密封 JAR 指定该 JAR 定义的所有包都是密封的,除非在每个包的基础上被覆盖。

要密封 JAR 文件,请使用值为 true 的 Sealed manifest header。例如:

Sealed: true

指定此存档中的所有包都是密封的,除非为清单条目中具有 Sealed 属性的特定包显式覆盖。

2.7 使用清单(manifest)属性增强安全性

在JAR文件中的清单文件中,添加以下属性,可以增加应用的安全性,其中只有Permissions属性是必须的:

  • Permissions 属性用于确保应用程序仅请求在用于调用应用程序的小程序标记或 JNLP 文件中指定的权限级别。使用此属性有助于防止有人重新部署使用您的证书签名的应用程序并以不同的权限级别运行它。此属性在主 JAR 文件的清单中是必需的。有关更多信息,请参阅 Java Platform, Standard Edition Deployment Guide 中的 Permissions Attribute

  • Codebase 属性用于确保 JAR 文件的代码库仅限于特定域。使用此属性可防止有人出于恶意目的在另一个网站上重新部署您的应用程序。有关更多信息,请参阅 Java 平台标准版部署指南中的Codebase

  • Application-Name 属性用于提供在已签名应用程序的安全提示中显示的标题。有关更多信息,请参阅 Java Platform, Standard Edition Deployment Guide 中的 Application-Name 属性

  • Application-Library-Allowable-Codebase 属性用于标识您的应用程序应该被找到的位置。当 JAR 文件位于与 JNLP 文件或 HTML 页面不同的位置时,使用此属性可减少安全提示中显示的位置数。有关更多信息,请参阅 Java Platform, Standard Edition Deployment Guide 中的 Application-Library-Allowable-Codebase 属性

  • Caller-Allowable-Codebase 属性用于标识 JavaScript 代码可以从中调用应用程序的域。使用此属性可防止未知的 JavaScript 代码访问您的应用程序。有关更多信息,请参阅 Java Platform, Standard Edition Deployment Guide 中的 Caller-Allowable-Codebase 属性

  • Entry-Point 属性用于标识允许用作 RIA 入口点的类。使用此属性可防止从 JAR 文件中的其他可用入口点运行未经授权的代码。有关更多信息,请参阅 Java 平台标准版部署指南中的 Entry-Point 属性

  • Trusted-Only 属性用于防止加载不受信任的组件。有关更多信息,请参阅 Java Platform, Standard Edition Deployment Guide 中的 Trusted-Only 属性

  • Trusted-Library 属性用于允许特权 Java 代码和沙箱 Java 代码之间的调用,而无需提示用户许可。有关更多信息,请参阅 Java 平台标准版部署指南中的 Trusted-Library 属性

3. 签名、验证JAR文件

您可以选择使用您的电子“签名”对 JAR 文件进行签名。验证您的签名的用户可以授予您的 JAR 捆绑软件安全权限,您也可以验证要使用的已签名 JAR 文件的签名。

3.1 什么是签名、验证

Java™ 平台使您能够对 JAR 文件进行数字签名。您对文件进行数字签名的原因与使用笔和墨水签署纸质文档的原因相同——让读者知道您编写了该文档,或者至少该文档已获得您的批准。

例如,当您签署一封信时,每个认出您签名的人都可以确认这封信是您写的。同样,当您对文件进行数字签名时,“识别”您的数字签名的任何人都知道该文件来自您。 “识别”电子签名的过程称为验证。

对 JAR 文件进行签名后,您还可以选择为签名加盖时间戳。与在纸质文档上放置日期类似,签名时间戳标识 JAR 文件的签名时间。时间戳可用于验证 JAR 文件证书在签署时是否有效。

对文件进行签名和验证的能力是 Java 平台安全架构的重要组成部分。安全性由在运行时生效的安全策略控制。您可以配置策略以向小程序和应用程序授予安全权限。例如,您可以授予小程序执行通常禁止的操作的权限,例如读取和写入本地文件或运行本地可执行程序。如果您下载了一些由受信任实体签名的代码,您可以使用该事实作为决定将哪些安全权限分配给代码的标准。

一旦您(或您的浏览器)确认小程序来自可信来源,您就可以让平台放宽安全限制,让小程序执行通常被禁止的操作。受信任的小程序可以具有由有效策略文件指定的自由。

Java 平台通过使用称为公钥和私钥的特殊数字来启用签名和验证。公钥和私钥是成对出现的,它们起到互补的作用。

私钥是您可以用来签署文件的电子“笔”。顾名思义,您的私钥只有您自己知道,因此其他人无法“伪造”您的签名。使用您的私钥签名的文件只能通过相应的公钥进行验证。

然而,仅公钥和私钥不足以真正验证签名。即使您已经验证签名文件包含匹配的密钥对,您仍然需要某种方式来确认公钥实际上来自真正的签名者。

因此,还需要一个元素来进行签名和验证工作。该附加元素是签名者包含在已签名 JAR 文件中的证书。证书是来自公认的证书颁发机构的数字签名声明,表明谁拥有特定的公钥。证书颁发机构是整个行业都信任的实体(通常是专门从事数字安全的公司),可以为密钥及其所有者签署和颁发证书。对于签名的 JAR 文件,证书表明谁拥有 JAR 文件中包含的公钥。

当您签署 JAR 文件时,您的公钥与相关证书一起放在存档中,以便任何想要验证您的签名的人都可以轻松使用它。

数字签名总结:

  • 签名者使用私钥对 JAR 文件进行签名。
  • 相应的公钥与其证书一起放置在 JAR 文件中,以便任何想要验证签名的人都可以使用它。

摘要和签名文件

当您签署 JAR 文件时,存档中的每个文件都会在存档的清单中获得一个摘要条目。可能长这样:

Name: test/classes/ClassOne.class
SHA1-Digest: TD1GZt8G11dXY2p4olSZPc5Rj64=

摘要值是文件内容在签名时的散列或编码表示。当且仅当文件本身发生变化时,文件的摘要才会发生变化。

对 JAR 文件进行签名后,会自动生成一个签名文件并将其放置在 JAR 文件的 META-INF 目录中,该目录与包含存档清单的目录相同。签名文件的文件名带有 .SF 扩展名。以下是签名文件内容的示例:

Signature-Version: 1.0
SHA1-Digest-Manifest: h1yS+K9T7DyHtZrtI+LxvgqaMYM=
Created-By: 1.7.0_06 (Oracle Corporation)

Name: test/classes/ClassOne.class
SHA1-Digest: fcav7ShIG6i86xPepmitOVo4vWY=

Name: test/classes/ClassTwo.class
SHA1-Digest: xrQem9snnPhLySDiZyclMlsFdtM=

Name: test/images/ImageOne.gif
SHA1-Digest: kdHbE7kL9ZHLgK7akHttYV4XIa0=

Name: test/images/ImageTwo.gif
SHA1-Digest: mF0D5zpk68R4oaxEqoS9Q7nhm60=

如您所见,签名文件包含存档文件的摘要条目,这些条目看起来类似于清单中的摘要值条目。然而,虽然清单中的摘要值是根据文件本身计算的,但签名文件中的摘要值是根据清单中的相应条目计算的。签名文件还包含整个清单的摘要值(请参阅上面示例中的 SHA1-Digest-Manifest 标头)。

在验证已签名的 JAR 文件时,会重新计算其每个文件的摘要,并与清单中记录的摘要进行比较,以确保 JAR 文件的内容自签名后未更改。作为额外的检查,清单文件本身的摘要值被重新计算并与签名文件中记录的值进行比较。

您可以在 JDK™ 文档的清单格式页面上阅读有关签名文件的其他信息。

签名块文件

除了签名文件之外,签名块文件会在 JAR 文件签名时自动放置在 META-INF 目录中。与清单文件或签名文件不同,签名块文件不是人类可读的。

签名块文件包含两个验证必不可少的元素:

  • 使用签名者的私钥生成的 JAR 文件的数字签名
  • 包含签名者公钥的证书,供任何想要验证已签名 JAR 文件的人使用

签名块文件名通常具有 .DSA 扩展名,表明它们是由默认数字签名算法创建的。如果使用与某些其他标准算法关联的密钥进行签名,则其他文件扩展名是可能的。

相关文档

有关密钥、证书和证书颁发机构的其他信息,请参阅

有关 Java 平台安全架构的更多信息,请参阅以下相关文档:

3.2 签名JAR 文件

您可以使用 JAR 签名和验证工具对 JAR 文件进行签名并为签名添加时间戳。您可以使用 jarsigner 命令调用 JAR 签名和验证工具,因此我们将其简称为“Jarsigner”。

要签署 JAR 文件,您必须首先拥有一个私钥。私钥及其相关的公钥证书存储在称为keystores的受密码保护的数据库中。keystores可以保存许多潜在签名者的密钥。keystores中的每个密钥都可以通过别名来标识,别名通常是拥有密钥的签名者的姓名。例如,属于 Rita Jones 的密钥可能具有别名“rita”。

签名 JAR 文件的命令的基本形式是

jarsigner jar-file alias

命令说明:

  • jar-file 是要签名的 JAR 文件的路径名。
  • alias 是标识用于签署 JAR 文件的私钥的别名,以及密钥的关联证书。

Jarsigner 工具将提示您输入keystores和别名的密码。

此命令的基本形式假定要使用的keystores位于主目录中名为 .keystore 的文件中。它将分别创建名为 x.SF 和 x.DSA 的签名和签名块文件,其中 x 是别名的前八个字母,全部转换为大写。此基本命令将使用签名的 JAR 文件覆盖原始 JAR 文件。

实际上,您可能希望使用一个或多个可用的命令选项。例如,鼓励对签名进行时间戳记,以便用于部署应用程序的任何工具都可以验证用于签署 JAR 文件的证书在签署文件时是否有效。如果不包含时间戳,则 Jarsigner 工具会发出警告。

在 jar-file之前可以添加一些选项,下表描述了可用的选项:

Option Description
-keystore url 如果您不想使用 .keystore 默认数据库,则指定要使用的keystores。
-sigfile file 指定 .SF 和 .DSA 文件的基本名称,而不是使用别名的前八个字母。文件只能由大写字母 (A-Z)、数字 (0-9)、连字符 (-) 和下划线 (_) 组成。
-signedjar file 如果您不希望原始未签名文件被签名文件覆盖,则指定要生成的已签名 JAR 文件的名称。
-tsa url 使用 URL 标识的时间戳机构 (TSA) 为签名生成时间戳。
-tsacert alias 使用由别名标识的 TSA 公钥证书为签名生成时间戳。
-altsigner class 指示使用替代签名机制对签名进行时间戳记。完全限定的类名标识所使用的类。
-altsignerpath classpathlist 提供由 altsigner 选项标识的类的路径以及该类所依赖的任何 JAR 文件。

使用示例

让我们看几个使用 Jarsigner 工具签署 JAR 文件的示例。在这些示例中,我们将假设以下内容:

  • 您的别名是“johndoe”
  • 您要使用的keystores位于当前工作目录中名为“mykeys”的文件中
  • 您要用于为签名添加时间戳的 TSA 位于 http://tsa.url.example.com

在这些假设下,您可以使用此命令对名为 app.jar 的 JAR 文件进行签名:

jarsigner -keystore mykeys -tsa http://tsa.url.example.com app.jar johndoe

系统将提示您输入keystores和别名的密码。由于此命令未使用 -sigfile 选项,因此它创建的 .SF 和 .DSA 文件将命名为 JOHNDOE.SF 和 JOHNDOE.DSA。由于该命令不使用 -signedjar 选项,因此生成的签名文件将覆盖 app.jar 的原始版本。

让我们看看如果您使用不同的选项组合会发生什么:

jarsigner -keystore mykeys -sigfile SIG -signedjar SignedApp.jar 
          -tsacert testalias app.jar johndoe

签名和签名块文件将分别命名为 SIG.SF 和 SIG.DSA,并且签名的 JAR 文件 SignedApp.jar 将放置在当前目录中。原始未签名的 JAR 文件将保持不变。此外,签名将带有 TSA 的公钥证书的时间戳,标识为 testalias。

JAR 签名和验证工具的详细说明:安全工具摘要

注意:当证书是自签名证书时,UNKNOWN 将显示为应用程序的发布者。更多信息请参考 Is it safe to run an application from a publisher that is listed as UNKNOWN?.

3.3 验证签名的 JAR 文件

通常,验证签名的 JAR 文件将由您的 Java™ 运行时环境负责。您的浏览器将验证它下载的签名小程序。使用解释器的 -jar 选项调用的签名应用程序将由运行时环境验证。

但是,您可以使用 jarsigner 工具自行验证已签名的 JAR 文件。例如,您可能想要这样做,以测试您准备的已签名 JAR 文件。

用于验证已签名 JAR 文件的基本命令是:

jarsigner -verify jar-file

此命令将验证 JAR 文件的签名并确保存档中的文件自签名后未更改。如果验证成功,您将看到以下消息:

jar verified.

如果您尝试验证未签名的 JAR 文件,则会产生以下消息:

jar is unsigned. (signatures missing or not parsable)

如果验证失败,则会显示相应的消息。例如,如果 JAR 文件的内容在 JAR 文件签名后发生了更改,那么在验证该文件时,会出现类似于以下内容的消息:

jarsigner: java.lang.SecurityException: invalid SHA1 
signature file digest for test/classes/Manifest.class

注意:如果签名JAR文件使用java.home/lib/Security/java.Security文件(其中java.home是安装JRE的目录)中JDK.JAR.disabledAlgorithms安全属性中指定的任何算法,则JDK将签名JAR文件视为未签名。

4. 使用JAR相关的API

Java平台提供了一些与JAR相关的API:

为了让您了解这些新 API 带来的可能性,本课程将引导您了解名为 JarRunner 的示例应用程序的内部工作原理。

An Example - The JarRunner Application

JarRunner 使您能够通过在命令行上指定 JAR 文件的 URL 来运行捆绑在 JAR 文件中的应用程序。例如,如果名为 TargetApp 的应用程序捆绑在位于 http://www.example.com/TargetApp.jar 的 JAR 文件中,您可以使用以下命令运行该应用程序:

java JarRunner http://www.example.com/TargetApp.jar

为了让 JarRunner 工作,它必须能够执行以下任务,所有这些任务都是通过使用新的 API 来完成的:

  • 访问远程 JAR 文件并与其建立通信链接
  • 检查 JAR 文件的清单以查看存档中的哪个类是主类
  • 加载 JAR 文件中的类

JarRunner 应用程序由两个类组成,JarRunner 和 JarClassLoader。 JarRunner 将大部分 JAR 处理任务委托给 JarClassLoader 类。 JarClassLoader 扩展了 java.net.URLClassLoader 类。在继续本课程之前,您可以浏览 JarRunner 和 JarClassLoader 类的源代码:

4.1 JarClassLoader 类

JarClassLoader 类扩展了 java.net.URLClassLoader。顾名思义,URLClassLoader 旨在用于加载通过搜索一组 URL 访问的类和资源。 URL 可以引用目录或 JAR 文件。

除了继承 URLClassLoader 之外,JarClassLoader 还利用了另外两个与 JAR 相关的新 API,java.util.jar 包和 java.net.JarURLConnection 类中的特性。在本节中,我们将详细介绍 JarClassLoader 的构造函数和两个方法。

JarClassLoader构造方法

构造函数将 java.net.URL 的实例作为参数。传递给此构造函数的 URL 将在 JarClassLoader 的其他地方使用,以查找要从中加载类的 JAR 文件。

public JarClassLoader(URL url) {
    super(new URL[] { url });
    this.url = url;
}

URL 对象被传递给超类 URLClassLoader 的构造函数,它接受一个 URL[] 数组,而不是单个 URL 实例,作为参数。

getMainClassName方法

使用 JAR 捆绑应用程序的 URL 构造 JarClassLoader 对象后,它需要一种方法来确定 JAR 文件中的哪个类是应用程序的入口点。这就是 getMainClassName 方法的工作:

public String getMainClassName() throws IOException {
    URL u = new URL("jar", "", url + "!/");
    JarURLConnection uc = (JarURLConnection)u.openConnection();
    Attributes attr = uc.getMainAttributes();
    return attr != null
                   ? attr.getValue(Attributes.Name.MAIN_CLASS)
                   : null;
}

您可能还记得在上一课中,JAR 捆绑应用程序的入口点由 JAR 文件清单的 Main-Class 标头指定。要了解 getMainClassName 如何访问 Main-Class 标头值,让我们详细查看该方法,特别注意它使用的新 JAR 处理功能:

public String getMainClassName() throws IOException {
        URL u = new URL("jar", "", url + "!/");
        JarURLConnection uc = (JarURLConnection)u.openConnection();
        Attributes attr = uc.getMainAttributes();
        return attr != null ? attr.getValue(Attributes.Name.MAIN_CLASS) : null;
    }

The JarURLConnection class and JAR URLs

getMainClassName 方法使用 java.net.JarURLConnection 类指定的 JAR URL 格式。 JAR 文件 URL 的语法如下例所示:

jar:http://www.example.com/jarfile.jar!/

终止符 !/ 分隔符表示 URL 指的是整个 JAR 文件。分隔符后面的任何内容都指的是特定的 JAR 文件内容,如下例所示:

jar:http://www.example.com/jarfile.jar!/mypackage/myclass.class

getMainClassName 方法的第一行是:

URL u = new URL("jar", "", url + "!/");

此语句构造一个表示 JAR URL 的新 URL 对象,将 !/ 分隔符附加到用于创建 JarClassLoader 实例的 URL。

The java.net.JarURLConnection class

此类表示应用程序和 JAR 文件之间的通信链接。它具有访问 JAR 文件清单的方法。 getMainClassName 的第二行是:

JarURLConnection uc = (JarURLConnection)u.openConnection();

在这个语句中,第一行中创建的 URL 实例打开了一个 URLConnection。然后将 URLConnection 实例转换为 JarURLConnection,以便它可以利用 JarURLConnection 的 JAR 处理功能。

获取清单(Manifest)属性: java.util.jar.Attributes

通过对 JAR 文件打开 JarURLConnection,您可以使用 JarURLConnection 的 getMainAttributes 方法访问 JAR 文件清单中的头信息。此方法返回 java.util.jar.Attributes 的一个实例,该类将 JAR 文件清单中的标头名称与其关联的字符串值进行映射。 getMainClassName 中的第三行创建了一个 Attributes 对象:

Attributes attr = uc.getMainAttributes();

要获取清单的 Main-Class 标头(header)的值,getMainClassName 的第四行调用 Attributes.getValue 方法:

return attr != null
               ? attr.getValue(Attributes.Name.MAIN_CLASS)
               : null;

该方法的参数 Attributes.Name.MAIN_CLASS 指定它是您想要的 Main-Class 标头的值。 (Attributes.Name 类还提供静态字段,例如 MANIFEST_VERSION、CLASS_PATH 和 SEALED,用于指定其他标准清单标头。)

The invokeClass Method

我们已经看到 JarURLClassLoader 如何识别 JAR 捆绑应用程序中的主类。最后一个要考虑的方法是 JarURLClassLoader.invokeClass,它允许调用主类来启动 JAR 捆绑的应用程序:

public void invokeClass(String name, String[] args)
    throws ClassNotFoundException,
           NoSuchMethodException,
           InvocationTargetException
{
    Class c = loadClass(name);
    Method m = c.getMethod("main", new Class[] { args.getClass() });
    m.setAccessible(true);
    int mods = m.getModifiers();
    if (m.getReturnType() != void.class || !Modifier.isStatic(mods) ||
        !Modifier.isPublic(mods)) {
        throw new NoSuchMethodException("main");
    }
    try {
        m.invoke(null, new Object[] { args });
    } catch (IllegalAccessException e) {
        // This should not happen, as we have disabled access checks
    }
}

invokeClass 方法接受两个参数:应用程序入口类的名称和要传递给入口类的 main 方法的字符串参数数组。首先,加载主类:

Class c = loadClass(name);

loadClass 方法继承自 java.lang.ClassLoader。

加载主类后,将使用 java.lang.reflect 包的反射 API 将参数传递给该类并启动它。你可以参考反射 API 的教程来回顾反射。

4.2 JarRunner 类

JarRunner 应用程序使用以下形式的命令启动:

java JarRunner url [arguments]

在上一节中,我们已经看到 JarClassLoader 如何能够从给定的 URL 识别和加载 JAR 捆绑应用程序的主类。因此,要完成 JarRunner 应用程序,我们需要能够从命令行获取 URL 和任何参数,并将它们传递给 JarClassLoader 的实例。这些任务属于 JarRunner 类,JarRunner 应用程序的入口点。

它首先从命令行上指定的 URL 创建一个 java.net.URL 对象:

public static void main(String[] args) {
    if (args.length < 1) {
        usage();
    }
    URL url = null;
    try {
        url = new URL(args[0]);
    } catch (MalformedURLException e) {
        fatal("Invalid URL: " + args[0]);
    }

如果 args.length < 1,则表示没有在命令行中指定 URL,因此会打印一条用法消息。如果第一个命令行参数是一个有效的 URL,则会创建一个新的 URL 对象来表示它。

接下来,JarRunner 创建一个 JarClassLoader 的新实例,将命令行中指定的 URL 传递给构造函数:

JarClassLoader cl = new JarClassLoader(url);

正如我们在上一节中看到的, JarClassLoader 提供了用于处理JAR的方法。

传递给 JarClassLoader 构造函数的 URL 是您要运行的 JAR 捆绑应用程序的 URL。 JarRunner 接下来调用类加载器的 getMainClassName 方法来识别应用程序的入口类:

String name = null;
try {
    name = cl.getMainClassName();
} catch (IOException e) {
    System.err.println("I/O error while loading JAR file:");
    e.printStackTrace();
    System.exit(1);
}
if (name == null) {
    fatal("Specified jar file does not contain a 'Main-Class'" +
          " manifest attribute");
}

关键语句是:name = cl.getMainClassName()。其他语句用于错误处理。

一旦 JarRunner 确定了应用程序的入口类,只剩下两个步骤:将任何参数传递给应用程序并启动应用程序。 JarRunner 使用以下代码执行以下步骤:

// Get arguments for the application
String[] newArgs = new String[args.length - 1];
System.arraycopy(args, 1, newArgs, 0, newArgs.length);
// Invoke application's main class
try {
    cl.invokeClass(name, newArgs);
} catch (ClassNotFoundException e) {
    fatal("Class not found: " + name);
} catch (NoSuchMethodException e) {
    fatal("Class does not define a 'main' method: " + name);
} catch (InvocationTargetException e) {
    e.getTargetException().printStackTrace();
    System.exit(1);
}

回想一下,第一个命令行参数是 JAR 捆绑应用程序的 URL。因此,要传递给该应用程序的任何参数都位于 args 数组中的第一个元素之后的。 JarRunner 获取这些元素,并创建一个名为 newArgs 的新数组以传递给应用程序。 JarRunner 然后将入口类名和新参数列表newArgs 传递给 JarClassLoader 的 invokeClass 方法。正如我们在上一节中看到的,invokeClass 将加载应用程序的入口类,向它传递任何参数,然后启动应用程序。