京山游侠

专注技术 拒绝扯淡
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Java 程序的打包、签名和验证

Posted on 2016-08-02 11:09  京山游侠  阅读(3242)  评论(4编辑  收藏  举报

参考资料##

该文中的内容来源于 Oracle 的官方文档。Oracle 在 Java 方面的文档是非常完善的。对 Java 8 感兴趣的朋友,可以直接找到这个总入口 Java SE 8 Documentation ,想阅读什么就点什么。本博客不定期从 Oracle 官网搬砖。这里介绍的工具是 jarjarsigner

前言##

在前面的 在Linux中安装Oracle JDK 8以及JVM的类加载机制 这一篇中我已经初步讨论过 Java 程序的组成:Java 程序中没有独立函数,只有类和类中的方法,即使是程序的入口点也不是独立函数。 Java 程序的源代码存在于名为 *.java 的源代码文件中,然后经过 javac 命令进行编译,最终可生成名为 *.class 的类文件。 Java 程序的启动器是 java 命令,它负责加载相应的类并执行其中的指令。

Java 程序的这种组织方式和我们常用的文件系统契合度非常好,一个类就是一个文件,类名就是文件名。(当然也有例外,比如内部类,这里不做讨论。)更进一步,Java 中还有 package 的概念,而且一个 package 名(类似于 abc.def.ghi.*.class )正好对应文件系统的路径(类似于 abc/def/ghi/*.class )。这种对应关系不是可有可无的,而是强制性的,我们在组织源代码和类的时候,必须遵守这个准则,否则程序将无法运行。文件多了,自然需要将其打包成一个整体,这就需要用到 jar 命令生成文件名为 *.jar 的 jar 包文件,该 jar 包文件就是一个很常见的压缩包文件,它其中的内容完全维持文件系统中的那种树状结构,随时可以解包查看其中的文件。将库或程序打包成 jar 文件进行发布已经是 Java 世界的标准做法,为了安全, jar 文件还可以被签名和验证。这正是 Java 世界的方便所在。

Java 程序中的 package 名和类文件的路径的对应关系##

这里写一个 HelloWorld 程序来做示范。本来一个 HelloWorld 程序是可以很简单的,在 Java 中只需要一个 System.out.println("Hello, World!"); 即可。但是为了让类多一点,我把它写得稍微复杂了点。我先写了一个 Speaker 类,然后在 HelloWorld 类的 main 方法中调用 speaker.sayHello(); 方法来和这个世界打招呼。同时,我的两个类都定义在一个 package 中,如下图源代码中的 package com.xkland.sample;

前面讲过, package 名必须和文件的路径一一对应,所以,我将源文件放在了 src 目录的 com/xkland/sample 目录中,其实这不是必须的,源文件可以随便放,只是这么放是一个好习惯。但是类文件所在的路径就必须和 package 名完全一致了,否则程序无法执行。如下图,我使用 javac src/com/xkland/sample/*.java -d dst 命令编译源文件,使用 -d dst 选项就是让 javac 把生成的类文件放到 dst 目录中,而 javacdst 目录中自动生成了和 package 名完全一致的目录树。

然后,我们执行程序的时候,必须在 dst 目录中运行 java com.xkland.sample.HelloWorld,在其它的目录中运行都不行,即使在 dst/com/xkland/sample 目录下也不行,哪怕这里是 HelloWorld.class 所在的位置。(其实想在 dst 目录以外的地方运行该程序也有办法,那就是把 dst 目录加入到 CLASSPATH 中,这里不做讨论。)这个例子虽简单,但充分展示了 Java 中的 package类文件在文件系统中的路径 之间必须遵守的约定。将类文件打包也必须遵守这样的约定。

使用 jar 命令将程序打包##

让类就这样分散在文件系统中毕竟不是最方便的,前面讲过可以把类文件打包,生成一个 jar 文件,这里来进行实战。(其实打包的文件中可以包含任意类型的文件,不仅只是 Java 的类文件,图片视频什么的都可以,文本文件自然不在话下,这些东西都是资源,这里也不做深入讨论。) jar 包还可以作为一个单独的程序运行,使用 java -jar filename.jar 命令即可。由于每一个 jar 包中包含了不止一个类文件,所以要作为单独的程序运行,在生成 jar 包的时候就必须指定程序的入口点,这个可以通过 jar 命令的 -e 参数指定。

使用 jar 命令打包的时候最重要的注意事项也是前面提到的 package 名和类文件的路径的对应。先看下图:

在该图中,我使用了 jar 命令的 -cfe 选项,其中的 c 是创建 jar 文件,f 是指定 jar 文件的文件名,e 是指定程序的入口点。前面提到过,一定要注意 package 名和类文件的路径的对应关系,所以在这个例子中,使用 jar 命令打包时,要么先进入 dst 目录,再运行 jar -cfe HelloWorld.jar com.xkland.sample.HelloWorld com,要么使用 jar 命令的 -C 指定 jar 包中的类文件的路径从哪个目录开始。我这里用的就是 jar -cfe HelloWorld.jar com.xkland.sample.HelloWorld -C dst com。这里的 com 目录会自动全部打包进 jar 文件,包括其中的所有子目录和文件,也就是说,对于目录的打包是递归的。而且,运行下面这样的命令效果应该是一样的: jar -cfe HelloWorld.jar com.xkland.sample.HelloWorld -C dst .,这里的 . 代表当前目录,也就是 dst 目录中的所有东西都会进行打包;

像上面这样打包后,使用 java -jar HelloWorld.jar 可以运行程序,使用 jar -tf HelloWorld.jar 可以查看 HelloWorld.jar 中的内容。可以看到,类文件的路径为 com/xkland/sample/HelloWorld.classcom/xkland/sample/Speaker.class,正好和 package 名完全对应。还可以看到 HelloWorld.jar 中有一个 META-INF/MANIFEST.MF 文件,这个文件是 jar 包文件的灵魂,所有的配置信息都在这里,比如程序的入口点是什么就是保存在这里,它是一个纯文本文件,可以直接读写,但是我们实际工作中基本不需要自己手动编写该文件,所以这里不做深入讨论。

如果使用 jar 打包的时候没有选择正确的开始目录,则 jar 包中类文件的路径就会不正确,程序就无法运行。如下图,打包时既没有进入 dst 目录,又没有使用 -C dst 选项,结果打包后程序就无法正确运行了。使用 jar -tf HelloWorld.jar 查看一下,发现所有类文件的路径都不对,因此程序无法运行。

关于 jar 的更多内容,可以直接查看 jar命令的手册,或者查看 Java教程中关于jar的章节

jar 包的签名和验证##

在我介绍 JDK中的证书生成和管理工具keytool 时,已经简单的讲过网络安全、证书、签名等方面的内容,这里只需要实战一下即可。现在已经有了一个 HelloWorld.jar 文件,尝试一下使用 jarsigner 命令对它进行签名。签名之前,先得有个证书,所以先使用 keytool -genkeypair -alias youxia 为自己创建一个,别名为 youxia。然后,使用这个证书对 HelloWorld.jar 进行签名,命令为 jarsigner HelloWorld.jar youxia。最后,可以使用 jarsigner -verify HelloWorld.jarHelloWorld.jar 进行验证。如下图:

不动手不知道,一动手才发现 So Easy!更多的细节可以戳 Signing JAR FilesVerifying Signed JAR Files

jar 文件被签名后,里面多了一些文件,把它解包看一下,如下图:

可以看到:首先是 MANIFEST.MF 文件中多了几行,它为 jar 包中的每一个文件都生成了一个数据摘要,这个摘要是从 jar 包中包含的文件本身计算出来的;其次,多了一个 YOUXIA.SF 文件,其中的内容也是 jar 包中每个文件对应的摘要,但是这个摘要是从 MANIFEST.MF 中的数据项计算出来的,它同时包含有针对整个 MANIFEST.MF 文件计算出的摘要;最后,就是一个无法直接阅读的文件 YOUXIA.DSA,从图中可以看出这个文件显示为乱码,其中的内容就是 youxia 的公钥以及使用 youxia 的私钥对该 jar 文件进行签名后的结果。具体信息请看 Understanding Signing and Verification

有了这里的直观的印象,我们就对签名和验证有了更深入的了解。签名和验证是建立在信息摘要算法和非对称加密解密算法的基础上的。数据摘要算法是不可逆的,它只能从数据生成摘要,不能从摘要解密出数据。非对称加密解密算法需要公钥私钥对,使用私钥加密的数据只能使用公钥解密,因此使用私钥对上一步生成的摘要进行加密,就相当于是签名了,因为只能通过相应的公钥进行解密。一般情况下公钥是通过证书发布出去的,而在上面的例子中,签名者的公钥直接放在了 YOUXIA.DSA 文件中,方便验证者使用。

总结##

jarjarsigner 这两个命令用起来没有什么难度,主要是理解其中的思想。使用 jar 时,一定要注意 package 名和类文件的路径之间的对应关系;使用 jarsigner 时,要理解公钥私钥、证书、摘要和数字签名,而且 JDK 中提供了非常好用的生成和管理公钥私钥及证书的工具 keytool。对于这些工具,我们只要亲自动手试一下,就可以加深我们对 Java 安全方面的理解。至于这些命令的细节都不需要多记,用的时候查官方文档即可。