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

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

错误处理

原文:docs.oracle.com/javase/tutorial/jaxp/limits/error.html

建议应用程序在设置新属性时捕获org.xml.sax.SAXNotRecognizedException异常,以便应用程序在不支持这些属性的旧版本上正常工作。例如,可下载的示例代码包含以下方法,isNewPropertySupported,用于检测示例是否在支持JDK_GENERAL_ENTITY_SIZE_LIMIT属性的 JDK 版本上运行:

public boolean isNewPropertySupported() {
    try {
        SAXParser parser = getSAXParser(false, false, false);
        parser.setProperty(JDK_GENERAL_ENTITY_SIZE_LIMIT, "10000");
    } catch (ParserConfigurationException ex) {
        fail(ex.getMessage());
    } catch (SAXException ex) {
        String err = ex.getMessage();
        if (err.indexOf("Property '" + JDK_GENERAL_ENTITY_SIZE_LIMIT +
                                       "' is not recognized.") > -1) {
            //expected before this patch
            debugPrint("New limit properties not supported. Samples not run.");
            return false;
        }
    }
    return true;
}

当输入文件包含导致超出限制异常的结构时,应用程序可以检查错误代码以确定失败的性质。以下错误代码适用于这些限制:

  • EntityExpansionLimit: JAXP00010001

  • ElementAttributeLimit: JAXP00010002

  • MaxEntitySizeLimit: JAXP00010003

  • TotalEntitySizeLimit: JAXP00010004

  • MaxXMLNameLimit: JAXP00010005

  • maxElementDepth: JAXP00010006

  • EntityReplacementLimit: JAXP00010007

错误代码的格式如下:

"JAXP" + components (two digits) + error category (two digits) + sequence number

因此,代码 JAXP00010001 代表了 JAXP 基本解析器安全限制EntityExpansionLimit

StAX

原文:docs.oracle.com/javase/tutorial/jaxp/limits/stax.html

StAX,JSR 173,不支持 FSP。然而,JDK 中的 StAX 实现支持新的限制属性及其对应的系统属性。这意味着,虽然没有 FSP 来开启和关闭限制,但描述的限制和系统属性的工作方式完全相同。

为了兼容性,StAX 特定属性总是在新的 JAXP 限制之前生效。例如,将SupportDTD属性设置为 false 会导致在输入文件包含Entity引用时抛出异常。因此,对于使用SupportDTD属性禁用 DTD 的应用程序,新限制的添加不会产生影响。

Samples

原文:docs.oracle.com/javase/tutorial/jaxp/limits/sample.html

你可以下载包含两个 Java 应用程序文件LimitSamples.javaSampleBase.java以及一个包含示例 xml 文件的目录samplessamples.zip文件。

要运行示例,请解压samples.zip文件,编译,并在命令行上执行以下操作:

java LimitSamples

Trail: RMI

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

Java 远程方法调用(RMI)系统允许在一个 Java 虚拟机中运行的对象调用另一个 Java 虚拟机中运行的对象的方法。RMI 提供了 Java 编程语言编写的程序之间的远程通信。


注意: 如果你要连接到一个现有的 IDL 程序,应该使用 Java IDL 而不是 RMI。


本教程简要介绍了 RMI 系统,然后演示了一个完整的客户端/服务器示例,该示例利用了 RMI 的独特功能,在运行时加载和执行用户定义的任务。示例中的服务器实现了一个通用的计算引擎,客户端用它来计算 圆周率符号 的值。

trail icon **RMI 应用程序概述** 描述了 RMI 系统并列出了其优势。此外,本节介绍了典型的 RMI 应用程序,由服务器和客户端组成,并介绍了重要术语。

trail icon **编写 RMI 服务器** 演示了计算引擎服务器的代码。本节将教你如何设计和实现一个 RMI 服务器。

trail icon **创建客户端程序** 着眼于一个可能的计算引擎客户端,并用它来说明 RMI 客户端的重要特性。

trail icon **编译和运行示例** 展示了如何编译和运行计算引擎服务器及其客户端。

RMI 应用概述

原文:docs.oracle.com/javase/tutorial/rmi/overview.html

RMI 应用程序通常由两个独立的程序组成,一个服务器和一个客户端。典型的服务器程序创建一些远程对象,使这些对象的引用可访问,并等待客户端调用这些对象的方法。典型的客户端程序获取服务器上一个或多个远程对象的远程引用,然后调用这些对象的方法。RMI 提供了服务器和客户端进行通信和传递信息的机制。这样的应用有时被称为分布式对象应用

分布式对象应用需要执行以下操作:

  • 定位远程对象。 应用程序可以使用各种机制获取对远程对象的引用。例如,应用程序可以使用 RMI 的简单命名设施,即 RMI 注册表,注册其远程对象。另外,应用程序可以将远程对象引用作为其他远程调用的一部分传递和返回。

  • 与远程对象通信。 远程对象之间的通信细节由 RMI 处理。对于程序员来说,远程通信看起来类似于常规的 Java 方法调用。

  • 加载传递的对象的类定义。 因为 RMI 允许对象来回传递,它提供了加载对象的类定义以及传输对象数据的机制。

以下插图描述了一个使用 RMI 注册表获取远程对象引用的 RMI 分布式应用程序。服务器调用注册表将名称与远程对象关联(或绑定)。客户端在服务器的注册表中按名称查找远程对象,然后调用其方法。插图还显示了 RMI 系统使用现有的 Web 服务器在需要时从服务器到客户端和从客户端到服务器加载类定义的过程。

RMI 系统,使用现有的 Web 服务器,从服务器到客户端和从客户端到服务器进行通信

动态代码加载的优势

RMI 的一个核心和独特特性是,如果接收方的 Java 虚拟机中未定义类的定义,它可以下载对象类的定义。一个对象的所有类型和行为,以前仅在单个 Java 虚拟机中可用,可以传输到另一个可能是远程的 Java 虚拟机。RMI 通过其实际类传递对象,因此当它们被发送到另一个 Java 虚拟机时,对象的行为不会改变。这种能力使得可以将新类型和行为引入到远程 Java 虚拟机中,从而动态扩展应用程序的行为。本教程中的计算引擎示例使用了这种能力来向分布式程序引入新行为。

远程接口、对象和方法

与任何其他 Java 应用程序一样,使用 Java RMI 构建的分布式应用程序由接口和类组成。接口声明方法。类实现接口中声明的方法,并且可能还声明其他方法。在分布式应用程序中,一些实现可能驻留在一些 Java 虚拟机中,而另一些则不在。具有可以在 Java 虚拟机之间调用的方法的对象称为远程对象

通过实现远程接口,对象变成远程对象,具有以下特征:

  • 一个远程接口扩展接口java.rmi.Remote

  • 接口的每个方法在其throws子句中声明java.rmi.RemoteException,除了任何特定于应用程序的异常。

当对象从一个 Java 虚拟机传递到另一个 Java 虚拟机时,RMI 会将远程对象与非远程对象区别对待。RMI 不会在接收 Java 虚拟机中复制实现对象,而是传递一个远程对象的远程存根。存根充当远程对象的本地代表或代理,并且基本上是客户端的远程引用。客户端在本地存根上调用方法,本地存根负责在远程对象上执行方法调用。

一个远程对象的存根实现了远程对象实现的相同一组远程接口。这个属性使得一个存根可以被转换为远程对象实现的任何接口。然而,只有在远程接口中定义的方法才能从接收 Java 虚拟机中调用。

使用 RMI 创建分布式应用程序

使用 RMI 开发分布式应用程序涉及以下一般步骤:

  1. 设计和实现分布式应用程序的组件。

  2. 编译源代码。

  3. 使类可网络访问。

  4. 启动应用程序。

设计和实现应用程序组件

首先,确定您的应用程序架构,包括哪些组件是本地对象,哪些组件是可远程访问的。这一步包括:

  • 定义远程接口。 远程接口指定客户端可以远程调用的方法。客户端编程针对远程接口,而不是针对这些接口的实现类。这些接口的设计包括确定将用作这些方法的参数和返回值的对象类型。如果这些接口或类中的任何一个尚不存在,您也需要定义它们。

  • 实现远程对象。 远程对象必须实现一个或多个远程接口。远程对象类可能包括其他仅在本地可用的接口和方法的实现。如果要将任何本地类用作这些方法的参数或返回值,那么它们也必须被实现。

  • 实现客户端。 使用远程对象的客户端可以在定义远程接口之后的任何时间实现,包括在部署远程对象之后。

编译源代码

与任何 Java 程序一样,您使用javac编译器来编译源文件。源文件包含远程接口的声明、它们的实现、任何其他服务器类以及客户端类。


注意: 在 Java 平台标准版 5.0 之前的版本中,需要通过使用rmic编译器来构建存根类,但现在不再需要这一步骤。


使类能够在网络中访问

在这一步中,您需要使某些类定义能够在网络中访问,例如远程接口及其关联类型的定义,以及需要下载到客户端或服务器的类的定义。通常通过 Web 服务器使类定义能够在网络中访问。

启动应用程序

启动应用程序包括运行 RMI 远程对象注册表、服务器和客户端。

本节的其余部分将介绍创建计算引擎所使用的步骤。

构建通用计算引擎

本教程专注于一个简单但强大的分布式应用程序,称为计算引擎。计算引擎是服务器上的一个远程对象,它接收来自客户端的任务,运行这些任务,并返回任何结果。这些任务在运行服务器的机器上执行。这种类型的分布式应用程序可以让多台客户端机器利用特别强大或具有专门硬件的机器。

计算引擎的新颖之处在于它运行的任务不需要在编写或启动计算引擎时定义。可以随时创建新类型的任务,然后将其交给计算引擎运行。任务的唯一要求是其类实现特定接口。RMI 系统可以将执行任务所需的代码下载到计算引擎中。然后,计算引擎在运行它的机器上利用资源运行任务。

执行任意任务的能力是由 Java 平台的动态特性实现的,通过 RMI 扩展到网络。RMI 动态加载任务代码到计算引擎的 Java 虚拟机中,并在没有实现任务的类的先验知识的情况下运行任务。这种具有动态下载代码能力的应用程序通常被称为基于行为的应用程序。这些应用程序通常需要完整的代理启用基础设施。在 Java 平台上,这些应用程序是分布式计算的基本机制之一。

编写一个 RMI 服务器

原文:docs.oracle.com/javase/tutorial/rmi/server.html

计算引擎服务器接受来自客户端的任务,运行这些任务,并返回任何结果。服务器代码由一个接口和一个类组成。接口定义了可以从客户端调用的方法。实质上,接口定义了客户端对远程对象的视图。类提供了实现。

设计一个远程接口

这一部分解释了Compute接口,它提供了客户端和服务器之间的连接。您还将了解支持此通信的 RMI API。

实现一个远程接口

这一部分探讨了实现Compute接口的类,从而实现了一个远程对象。这个类还提供了组成服务器程序的其余代码,包括一个创建远程对象实例的main方法,将其注册到 RMI 注册表,并设置安全管理器。

设计远程接口

原文:docs.oracle.com/javase/tutorial/rmi/designing.html

计算引擎的核心是一种协议,使得任务可以提交到计算引擎,计算引擎可以运行这些任务,并将这些任务的结果返回给客户端。这个协议在支持计算引擎的接口中表达。该协议的远程通信在下图中有所体现。

客户端和计算引擎之间的远程通信

每个接口包含一个方法。计算引擎的远程接口Compute允许任务提交到引擎。客户端接口Task定义了计算引擎如何执行提交的任务。

compute.Compute接口定义了远程访问部分,即计算引擎本身。这里是Compute接口的源代码:

package compute;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Compute extends Remote {
    <T> T executeTask(Task<T> t) throws RemoteException;
}

通过扩展接口java.rmi.RemoteCompute接口将自身标识为一个可以从另一个 Java 虚拟机中调用其方法的接口。实现这个接口的任何对象都可以是一个远程对象。

作为远程接口的成员,executeTask方法是一个远程方法。因此,该方法必须被定义为能够抛出java.rmi.RemoteException的方法。这个异常是由 RMI 系统从远程方法调用中抛出的,用于指示通信失败或协议错误。RemoteException是一个受检异常,因此任何调用远程方法的代码都需要通过捕获它或在其throws子句中声明来处理这个异常。

计算引擎所需的第二个接口是Task接口,它是Compute接口中executeTask方法的参数类型。compute.Task接口定义了计算引擎和需要执行的工作之间的接口,提供了开始工作的方式。这里是Task接口的源代码:

package compute;

public interface Task<T> {
    T execute();
}

Task接口定义了一个方法execute,该方法没有参数,也不会抛出异常。因为该接口没有扩展Remote,所以在该接口中的方法不需要在throws子句中列出java.rmi.RemoteException

Task接口有一个类型参数T,代表任务计算的结果类型。该接口的execute方法返回计算的结果,因此其返回类型是T

Compute接口的executeTask方法反过来返回传递给它的Task实例的执行结果。因此,executeTask方法有自己的类型参数T,将其自己的返回类型与传递的Task实例的结果类型关联起来。

RMI 使用 Java 对象序列化机制在 Java 虚拟机之间按值传输对象。要使对象被视为可序列化,其类必须实现java.io.Serializable标记接口。因此,实现Task接口的类必须也实现Serializable,任务结果所使用的对象的类也必须实现。

只要它们是Task类型的实现,不同类型的任务可以由Compute对象运行。实现这个接口的类可以包含任务计算所需的任何数据以及计算所需的任何其他方法。

这就是 RMI 如何使这个简单的计算引擎成为可能。因为 RMI 可以假定Task对象是用 Java 编程语言编写的,之前未知于计算引擎的Task对象的实现会根据需要通过 RMI 下载到计算引擎的 Java 虚拟机中。这种能力使得计算引擎的客户端能够定义新的任务类型,而无需将代码明确安装在该机器上。

ComputeEngine类实现的计算引擎实现了Compute接口,通过调用其executeTask方法,使不同的任务可以提交给它。这些任务使用任务的execute方法的实现来运行,并将结果返回给远程客户端。

实现远程接口。

原文:docs.oracle.com/javase/tutorial/rmi/implementing.html

本节讨论实现计算引擎类的任务。一般来说,实现远程接口的类至少应该执行以下操作:

  • 声明正在实现的远程接口。

  • 为每个远程对象定义构造函数。

  • 为远程接口中的每个远程方法提供实现。

RMI 服务器程序需要创建初始远程对象并将其导出到 RMI 运行时,使其可用于接收传入的远程调用。此设置过程可以封装在远程对象实现类本身的方法中,也可以完全包含在另一个类中。设置过程应执行以下操作:

  • 创建并安装安全管理器

  • 创建并导出一个或多个远程对象。

  • 至少在 RMI 注册表(或其他命名服务,如通过 Java 命名和目录接口访问的服务)中注册一个远程对象,用于引导目的。

计算引擎的完整实现如下。engine.ComputeEngine类实现了远程接口Compute,并包括用于设置计算引擎的main方法。以下是ComputeEngine类的源代码:

package engine;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import compute.Compute;
import compute.Task;

public class ComputeEngine implements Compute {

    public ComputeEngine() {
        super();
    }

    public <T> T executeTask(Task<T> t) {
        return t.execute();
    }

    public static void main(String[] args) {
        if (System.getSecurityManager() == null) {
            System.setSecurityManager(new SecurityManager());
        }
        try {
            String name = "Compute";
            Compute engine = new ComputeEngine();
            Compute stub =
                (Compute) UnicastRemoteObject.exportObject(engine, 0);
            Registry registry = LocateRegistry.getRegistry();
            registry.rebind(name, stub);
            System.out.println("ComputeEngine bound");
        } catch (Exception e) {
            System.err.println("ComputeEngine exception:");
            e.printStackTrace();
        }
    }
}

以下各节讨论计算引擎实现的每个组件。

声明正在实现的远程接口。

计算引擎的实现类声明如下:

public class ComputeEngine implements Compute

此声明说明该类实现了Compute远程接口,因此可以用作远程对象。

ComputeEngine类定义了一个远程对象实现类,该类实现了一个单独的远程接口,没有其他接口。ComputeEngine类还包含两个仅可在本地调用的可执行程序元素。其中一个元素是用于ComputeEngine实例的构造函数。另一个元素是用于创建ComputeEngine实例并使其对客户端可用的main方法。

为远程对象定义构造函数。

ComputeEngine类有一个不带参数的构造函数。构造函数的代码如下:

public ComputeEngine() {
    super();
}

此构造函数只调用了超类构造函数,即Object类的无参数构造函数。尽管即使在ComputeEngine构造函数中省略了超类构造函数,超类构造函数也会被调用,但为了清晰起见,还是包含在内。

为每个远程方法提供实现。

远程对象的类为远程接口中指定的每个远程方法提供实现。Compute接口包含一个单独的远程方法,executeTask,其实现如下:

public <T> T executeTask(Task<T> t) {
    return t.execute();
}

此方法实现了ComputeEngine远程对象与其客户端之间的协议。每个客户端都向ComputeEngine提供一个具有Task接口的特定实现的Task对象的execute方法。ComputeEngine执行每个客户端的任务,并将任务的execute方法的结果直接返回给客户端。

在 RMI 中传递对象

远程方法的参数或返回值几乎可以是任何类型,包括本地对象、远程对象和基本数据类型。更准确地说,只要实体是基本数据类型、远程对象或可序列化对象的实例,就可以将任何类型的实体传递给远程方法或从远程方法传递出来,这意味着它实现了接口java.io.Serializable

一些对象类型不符合这些标准,因此无法传递给远程方法或从远程方法返回。其中大多数对象,如线程或文件描述符,封装的信息只在单个地址空间内有意义。许多核心类,包括java.langjava.util包中的类,实现了Serializable接口。

关于如何传递参数和返回值的规则如下:

  • 远程对象本质上是通过引用传递的。远程对象引用是一个存根,是一个客户端代理,实现了远程对象实现的完整远程接口集。

  • 本地对象通过对象序列化进行复制传递。默认情况下,除了标记为statictransient的字段外,所有字段都会被复制。可以按类覆盖默认序列化行为。

通过引用传递远程对象意味着通过远程方法调用对对象状态所做的任何更改都会反映在原始远程对象中。当传递远程对象时,只有接收者可用的是远程接口。在实现类中定义的方法或类实现的非远程接口中定义的任何方法对接收者不可用。

例如,如果您要传递对ComputeEngine类实例的引用,则接收方只能访问计算引擎的executeTask方法。该接收方将看不到ComputeEngine构造函数、其main方法或其对java.lang.Object的任何方法的实现。

在远程方法调用的参数和返回值中,不是远程对象的对象是按值传递的。因此,在接收方的 Java 虚拟机中创建对象的副本。接收方对对象状态的任何更改仅反映在接收方的副本中,而不是发送方的原始实例中。发送方对对象状态的任何更改仅反映在发送方的原始实例中,而不是接收方的副本中。

实现服务器的main方法

ComputeEngine 实现中最复杂的方法是 main 方法。main 方法用于启动 ComputeEngine,因此需要进行必要的初始化和管理工作,以准备服务器接受来自客户端的调用。这个方法不是一个远程方法,这意味着它不能从不同的 Java 虚拟机中调用。由于 main 方法声明为 static,该方法根本不与对象关联,而是与类 ComputeEngine 关联。

创建和安装安全管理器

main 方法的第一个任务是创建和安装安全管理器,以保护来自 Java 虚拟机内运行的不受信任的下载代码对系统资源的访问。安全管理器确定下载的代码是否可以访问本地文件系统或执行任何其他特权操作。

如果一个 RMI 程序没有安装安全管理器,RMI 将不会为作为参数接收的对象或远程方法调用的返回值下载类(除了从本地类路径)。这个限制确保下载代码执行的操作受安全策略约束。

这是创建和安装安全管理器的代码:

if (System.getSecurityManager() == null) {
    System.setSecurityManager(new SecurityManager());
}

使远程对象对客户端可用

接下来,main 方法创建了一个 ComputeEngine 实例,并使用以下语句将其导出到 RMI 运行时:

Compute engine = new ComputeEngine();
Compute stub =
    (Compute) UnicastRemoteObject.exportObject(engine, 0);

静态的 UnicastRemoteObject.exportObject 方法导出提供的远程对象,以便它可以接收来自远程客户端的远程方法调用。第二个参数是一个 int,指定用于监听对象的传入远程调用请求的 TCP 端口。通常使用值零,指定使用匿名端口。实际端口将由 RMI 或底层操作系统在运行时选择。但也可以使用非零值指定用于监听的特定端口。一旦 exportObject 调用成功返回,ComputeEngine 远程对象就准备好处理传入的远程调用。

exportObject 方法返回导出的远程对象的存根。请注意,变量 stub 的类型必须是 Compute,而不是 ComputeEngine,因为远程对象的存根只实现导出的远程对象实现的远程接口。

exportObject 方法声明可以抛出 RemoteException,这是一个已检查的异常类型。main 方法使用其 try/catch 块处理此异常。如果不以这种方式处理异常,则必须在 main 方法的 throws 子句中声明 RemoteException。如果请求的端口已绑定到其他用途,尝试导出远程对象可能会抛出 RemoteException,例如,如果必要的通信资源不可用。

在客户端可以调用远程对象的方法之前,必须首先获取对远程对象的引用。获取引用可以通过与程序中获取任何其他对象引用的方式相同完成,例如通过将引用作为方法的返回值的一部分或作为包含这样一个引用的数据结构的一部分。

系统提供了一种特定类型的远程对象,即 RMI 注册表,用于查找其他远程对象的引用。RMI 注册表是一个简单的远程对象命名服务,使客户端能够通过名称获取对远程对象的引用。注册表通常仅用于定位 RMI 客户端需要使用的第一个远程对象。然后,该第一个远程对象可能提供支持以查找其他对象。

java.rmi.registry.Registry远程接口是在注册表中绑定(或注册)和查找远程对象的 API。java.rmi.registry.LocateRegistry类提供了用于在特定网络地址(主机和端口)合成远程引用到注册表的静态方法。这些方法创建包含指定网络地址的远程引用对象,而不执行任何远程通信。LocateRegistry还提供了用于在当前 Java 虚拟机中创建新注册表的静态方法,尽管此示例未使用这些方法。一旦远程对象在本地主机上的 RMI 注册表中注册,任何主机上的客户端都可以按名称查找远程对象,获取其引用,然后调用对象上的远程方法。注册表可以被所有运行在主机上的服务器共享,或者单个服务器进程可以创建和使用自己的注册表。

ComputeEngine类使用以下语句为对象创建名称:

String name = "Compute";

代码然后将名称添加到运行在服务器上的 RMI 注册表中。此步骤稍后通过以下语句完成:

Registry registry = LocateRegistry.getRegistry();
registry.rebind(name, stub);

rebind调用会对本地主机上的 RMI 注册表进行远程调用。与任何远程调用一样,此调用可能导致抛出RemoteException,该异常由main方法末尾的catch块处理。

注意Registry.rebind调用的以下内容:

  • LocateRegistry.getRegistry的无参数重载在本地主机上和默认注册表端口 1099 上合成对注册表的引用。如果注册表在除 1099 之外的端口上创建,则必须使用具有int参数的重载。

  • 当对注册表进行远程调用时,传递的是远程对象的存根而不是远程对象本身的副本。远程实现对象,例如ComputeEngine的实例,永远不会离开它们被创建的 Java 虚拟机。因此,当客户端在服务器的远程对象注册表中执行查找时,会返回存根的副本。在这种情况下,远程对象实际上是通过(远程)引用而不是通过值传递的。

  • 出于安全原因,应用程序只能在运行在同一主机上的注册表上bindunbindrebind远程对象引用。这种限制防止远程客户端删除或覆盖服务器注册表中的任何条目。然而,可以从任何主机(本地或远程)请求lookup

一旦服务器向本地 RMI 注册表注册,它会打印一条消息,指示它已准备好开始处理调用。然后,main方法完成。不需要有一个线程等待来保持服务器处于活动状态。只要在另一个 Java 虚拟机中有对ComputeEngine对象的引用,无论是本地还是远程,ComputeEngine对象都不会被关闭或垃圾回收。因为程序在注册表中绑定了对ComputeEngine的引用,所以它可以从远程客户端,即注册表本身,访问。RMI 系统保持ComputeEngine的进程运行。ComputeEngine可用于接受调用,并且在其绑定从注册表中移除没有远程客户端持有对ComputeEngine对象的远程引用时才会被回收。

ComputeEngine.main方法中的最后一段代码处理可能出现的任何异常。代码中可能抛出的唯一已检查异常类型是RemoteException,可能是由UnicastRemoteObject.exportObject调用或注册表rebind调用引起的。在任何情况下,程序在打印错误消息后不能做更多事情,只能退出。在一些分布式应用中,可以从远程调用失败中恢复。例如,应用程序可以尝试重试操作或选择另一个服务器继续操作。

创建客户端程序

原文:docs.oracle.com/javase/tutorial/rmi/client.html

计算引擎是一个相对简单的程序:它运行交给它的任务。计算引擎的客户端更加复杂。客户端需要调用计算引擎,但也必须定义计算引擎执行的任务。

在我们的示例中,客户端由两个单独的类组成。第一个类ComputePi查找并调用Compute对象。第二个类Pi实现Task接口并定义计算引擎执行的工作。Pi类的工作是计算到某个小数位数的π符号的值。

非远程Task接口定义如下:

package compute;

public interface Task<T> {
    T execute();
}

调用Compute对象方法的代码必须获取对该对象的引用,创建一个Task对象,然后请求执行该任务。稍后将显示任务类Pi的定义。使用单个参数构造Pi对象,该参数是所需结果的精度。任务执行的结果是表示计算到指定精度的π符号java.math.BigDecimal

这里是client.ComputePi的源代码,主要客户端类:

package client;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.math.BigDecimal;
import compute.Compute;

public class ComputePi {
    public static void main(String args[]) {
        if (System.getSecurityManager() == null) {
            System.setSecurityManager(new SecurityManager());
        }
        try {
            String name = "Compute";
            Registry registry = LocateRegistry.getRegistry(args[0]);
            Compute comp = (Compute) registry.lookup(name);
            Pi task = new Pi(Integer.parseInt(args[1]));
            BigDecimal pi = comp.executeTask(task);
            System.out.println(pi);
        } catch (Exception e) {
            System.err.println("ComputePi exception:");
            e.printStackTrace();
        }
    }    
}

ComputeEngine服务器一样,客户端首先安装安全管理器。这一步是必要的,因为接收服务器远程对象存根的过程可能需要从服务器下载类定义。为了让 RMI 下载类,必须启用安全管理器。

安装安全管理器后,客户端构造一个名称用于查找Compute远程对象,使用与ComputeEngine绑定其远程对象相同的名称。此外,客户端使用LocateRegistry.getRegistry API 合成服务器主机上注册表的远程引用。第一个命令行参数args[0]的值是Compute对象运行的远程主机的名称。然后客户端在注册表上调用lookup方法,通过名称在服务器主机的注册表中查找远程对象。使用的LocateRegistry.getRegistry重载版本具有单个String参数,返回命名主机和默认注册表端口 1099 的注册表引用。如果注册表在除 1099 之外的端口上创建,则必须使用具有int参数的重载。

接下来,客户端创建一个新的Pi对象,将第二个命令行参数args[1]解析为整数传递给Pi构造函数。这个参数指示计算中要使用的小数位数。最后,客户端调用Compute远程对象的executeTask方法。传入executeTask调用的对象返回一个BigDecimal类型的对象,程序将其存储在变量result中。最后,程序打印结果。下图描述了ComputePi客户端、rmiregistryComputeEngine之间消息流的过程。

计算引擎、注册表和客户端之间消息流的示意图。

Pi类实现了Task接口,并计算了π符号的值到指定的小数位数。对于这个示例,实际算法并不重要。重要的是算法是计算密集型的,这意味着你希望它在一个能力强大的服务器上执行。

这是client.Pi的源代码,该类实现了Task接口:

package client;

import compute.Task;
import java.io.Serializable;
import java.math.BigDecimal;

public class Pi implements Task<BigDecimal>, Serializable {

    private static final long serialVersionUID = 227L;

    /** constants used in pi computation */
    private static final BigDecimal FOUR =
        BigDecimal.valueOf(4);

    /** rounding mode to use during pi computation */
    private static final int roundingMode = 
        BigDecimal.ROUND_HALF_EVEN;

    /** digits of precision after the decimal point */
    private final int digits;

    /**
     * Construct a task to calculate pi to the specified
     * precision.
     */
    public Pi(int digits) {
        this.digits = digits;
    }

    /**
     * Calculate pi.
     */
    public BigDecimal execute() {
        return computePi(digits);
    }

    /**
     * Compute the value of pi to the specified number of 
     * digits after the decimal point.  The value is 
     * computed using Machin's formula:
     *
     *          pi/4 = 4*arctan(1/5) - arctan(1/239)
     *
     * and a power series expansion of arctan(x) to 
     * sufficient precision.
     */
    public static BigDecimal computePi(int digits) {
        int scale = digits + 5;
        BigDecimal arctan1_5 = arctan(5, scale);
        BigDecimal arctan1_239 = arctan(239, scale);
        BigDecimal pi = arctan1_5.multiply(FOUR).subtract(
                                  arctan1_239).multiply(FOUR);
        return pi.setScale(digits, 
                           BigDecimal.ROUND_HALF_UP);
    }
    /**
     * Compute the value, in radians, of the arctangent of 
     * the inverse of the supplied integer to the specified
     * number of digits after the decimal point.  The value
     * is computed using the power series expansion for the
     * arc tangent:
     *
     * arctan(x) = x - (x³)/3 + (x⁵)/5 - (x⁷)/7 + 
     *     (x⁹)/9 ...
     */   
    public static BigDecimal arctan(int inverseX, 
                                    int scale) 
    {
        BigDecimal result, numer, term;
        BigDecimal invX = BigDecimal.valueOf(inverseX);
        BigDecimal invX2 = 
            BigDecimal.valueOf(inverseX * inverseX);

        numer = BigDecimal.ONE.divide(invX,
                                      scale, roundingMode);

        result = numer;
        int i = 1;
        do {
            numer = 
                numer.divide(invX2, scale, roundingMode);
            int denom = 2 * i + 1;
            term = 
                numer.divide(BigDecimal.valueOf(denom),
                             scale, roundingMode);
            if ((i % 2) != 0) {
                result = result.subtract(term);
            } else {
                result = result.add(term);
            }
            i++;
        } while (term.compareTo(BigDecimal.ZERO) != 0);
        return result;
    }
}

注意,所有可序列化的类,无论它们是否直接或间接实现了Serializable接口,都必须声明一个名为serialVersionUIDprivate static final字段,以确保在不同版本之间的序列化兼容性。如果该类之前没有发布过版本,则该字段的值可以是任何long值,类似于Pi使用的227L,只要该值在未来版本中一致使用即可。如果该类的先前版本已发布但没有显式声明serialVersionUID,但与该版本的序列化兼容性很重要,则必须使用先前版本的默认隐式计算值作为新版本显式声明的值。可以运行serialver工具来确定先前版本的默认计算值。

这个示例最有趣的特点是Compute实现对象在Pi对象作为参数传递给executeTask方法之前从不需要Pi类的定义。在那时,RMI 会将该类的代码加载到Compute对象的 Java 虚拟机中,调用execute方法,并执行任务的代码。结果,对于Pi任务来说是一个BigDecimal对象,会被传回给调用客户端,在那里用于打印计算结果。

供给的Task对象计算Pi值这一事实对于ComputeEngine对象来说并不重要。你也可以实现一个任务,比如通过使用概率算法生成一个随机素数。这个任务也会需要大量计算,因此是传递给ComputeEngine的一个很好的选择,但它需要非常不同的代码。当Task对象传递给Compute对象时,这段代码也可以被下载。就像在需要时引入计算派符号的算法一样,生成随机素数的代码也会在需要时被引入。Compute对象只知道它接收到的每个对象都实现了execute方法。Compute对象不知道,也不需要知道实现的具体内容。

编译和运行示例

原文:docs.oracle.com/javase/tutorial/rmi/example.html

现在,计算引擎示例的代码已经编写完成,需要进行编译和运行。

编译示例程序

在这一部分,您将学习如何编译组成计算引擎示例的服务器和客户端程序。

运行示例程序

最后,你运行服务器和客户端程序,从而计算出π符号的值。

编译示例程序

原文:docs.oracle.com/javase/tutorial/rmi/compiling.html

在一个真实的场景中,例如部署了像计算引擎这样的服务,开发人员可能会创建一个包含 ComputeTask 接口的 Java 存档(JAR)文件,供服务器类实现和客户端程序使用。接下来,一个开发人员,也许是接口 JAR 文件的相同开发人员,会编写 Compute 接口的实现,并将该服务部署在客户端可用的机器上。客户端程序的开发人员可以使用 JAR 文件中包含的 ComputeTask 接口,独立开发一个任务和客户端程序,使用 Compute 服务。

在本节中,您将学习如何设置 JAR 文件、服务器类和客户端类。您将看到客户端的 Pi 类将在运行时下载到服务器上。此外,ComputeTask 接口将在运行时从服务器下载到注册表中。

该示例将接口、远程对象实现和客户端代码分为三个包:

  • computeComputeTask 接口

  • engineComputeEngine 实现类

  • clientComputePi 客户端代码和 Pi 任务实现

首先,您需要构建接口 JAR 文件,以提供给服务器和客户端开发人员。

构建接口类的 JAR 文件

首先,您需要编译 compute 包中的接口源文件,然后构建一个包含它们类文件的 JAR 文件。假设用户 waldo 已经编写了这些接口,并将源文件放在 Windows 的目录 c:\home\waldo\src\compute 或 Solaris OS 或 Linux 的目录 /home/waldo/src/compute 中。给定这些路径,您可以使用以下命令编译接口并创建 JAR 文件:

微软 Windows

cd c:\home\waldo\src
javac compute\Compute.java compute\Task.java
jar cvf compute.jar compute\*.class

Solaris OS 或 Linux

cd /home/waldo/src
javac compute/Compute.java compute/Task.java
jar cvf compute.jar compute/*.class


jar 命令由于 -v 选项显示如下输出:

added manifest
adding: compute/Compute.class(in = 307) (out= 201)(deflated 34%)
adding: compute/Task.class(in = 217) (out= 149)(deflated 31%)

现在,您可以将 compute.jar 文件分发给服务器和客户端应用程序的开发人员,以便他们可以利用这些接口。

使用 javac 编译器构建服务器端或客户端类后,如果其中任何类需要由其他 Java 虚拟机动态下载,您必须确保它们的类文件放在网络可访问的位置。在本例中,对于 Solaris OS 或 Linux,这个位置是 /home/*user*/public_html/classes,因为许多 Web 服务器允许通过构造为 http://host/~*user*/ 的 HTTP URL 访问用户的 public_html 目录。如果您的 Web 服务器不支持这种约定,您可以在 Web 服务器的层次结构中使用不同的位置,或者您可以使用文件 URL。在 Solaris OS 或 Linux 上,文件 URL 的形式为 file:/home/*user*/public_html/classes/,在 Windows 上的形式为 file:/c:/home/*user*/public_html/classes/。您也可以根据需要选择另一种类型的 URL。

类文件的网络可访问性使得 RMI 运行时在需要时可以下载代码。RMI 不会为代码下载定义自己的协议,而是使用 Java 平台支持的 URL 协议(例如 HTTP)来下载代码。请注意,使用完整的重量级 Web 服务器来提供这些类文件是不必要的。例如,可以在 找到一个简单的 HTTP 服务器,提供了在 RMI 通过 HTTP 下载类所需的功能。

另请参阅 远程方法调用主页

构建服务器类

engine 包仅包含一个服务器端实现类 ComputeEngine,这是远程接口 Compute 的实现。

假设开发 ComputeEngine 类的用户 ann 已经将 ComputeEngine.java 放在 Windows 的目录 c:\home\ann\src\engine 或 Solaris OS 或 Linux 的目录 /home/ann/src/engine 中。她正在部署供客户下载的类文件在她的 public_html 目录的子目录中,Windows 上为 c:\home\ann\public_html\classes 或 Solaris OS 或 Linux 上为 /home/ann/public_html/classes。这个位置可以通过一些 Web 服务器访问,如 http://*host*:*port*/~ann/classes/

ComputeEngine 类依赖于 ComputeTask 接口,这些接口包含在 compute.jar JAR 文件中。因此,在构建服务器类时,您需要将 compute.jar 文件放在类路径中。假设 compute.jar 文件位于 Windows 的目录 c:\home\ann\public_html\classes 或 Solaris OS 或 Linux 的目录 /home/ann/public_html/classes 中。有了这些路径,您可以使用以下命令构建服务器类:

Microsoft Windows:

cd c:\home\ann\src
javac -cp c:\home\ann\public_html\classes\compute.jar
    engine\ComputeEngine.java

Solaris OS 或 Linux:

cd /home/ann/src
javac -cp /home/ann/public_html/classes/compute.jar
    engine/ComputeEngine.java

ComputeEngine的存根类实现了Compute接口,该接口引用了Task接口。因此,这两个接口的类定义需要对其他 Java 虚拟机(如注册表的 Java 虚拟机)可访问,以便存根能够接收。客户端 Java 虚拟机将已经在其类路径中包含了这些接口,因此实际上不需要下载它们的定义。public_html目录下的compute.jar文件可以起到这个作用。

现在,计算引擎已经准备好部署。你现在可以这样做,或者等到构建客户端之后再部署。

构建客户端类

client包含两个类,ComputePi,主要客户端程序,和Pi,客户端对Task接口的实现。

假设开发客户端类的用户jones已经将ComputePi.javaPi.java放置在 Windows 系统的c:\home\jones\src\client目录下,或者 Solaris OS 或 Linux 系统的/home/jones/src/client目录下。他正在将计算引擎的类文件部署在他的public_html目录的子目录中,Windows 系统为c:\home\jones\public_html\classes,Solaris OS 或 Linux 系统为/home/jones/public_html/classes。这个位置可以通过一些 Web 服务器访问,如http://*host*:*port*/~jones/classes/

客户端类依赖于ComputeTask接口,这些接口包含在compute.jar JAR 文件中。因此,在构建客户端类时,你需要将compute.jar文件放在类路径中。假设compute.jar文件位于 Windows 系统的c:\home\jones\public_html\classes目录下,或者 Solaris OS 或 Linux 系统的/home/jones/public_html/classes目录下。有了这些路径,你可以使用以下命令来构建客户端类:

Microsoft Windows

cd c:\home\jones\src
javac -cp c:\home\jones\public_html\classes\compute.jar
    client\ComputePi.java client\Pi.java
mkdir c:\home\jones\public_html\classes\client
cp client\Pi.class
    c:\home\jones\public_html\classes\client

Solaris OS 或 Linux

cd /home/jones/src
javac -cp /home/jones/public_html/classes/compute.jar
    client/ComputePi.java client/Pi.java
mkdir /home/jones/public_html/classes/client
cp client/Pi.class
    /home/jones/public_html/classes/client

只有Pi类需要放置在public_html\classes\client目录中,因为只有Pi类需要在计算引擎的 Java 虚拟机中可用以供下载。现在,你可以先运行服务器,然后再运行客户端。

运行示例程序

原文:docs.oracle.com/javase/tutorial/rmi/running.html

关于安全性的说明

服务器和客户端程序都安装了安全管理器。当您运行任一程序时,您需要指定一个安全策略文件,以便代码被授予其运行所需的安全权限。这是一个用于服务器程序的示例策略文件。

grant codeBase "file:/home/ann/src/" {
    permission java.security.AllPermission;
};

这是一个用于客户端程序的示例策略文件。

grant codeBase "file:/home/jones/src/" {
    permission java.security.AllPermission;
};

对于这两个示例策略文件,所有权限都授予程序本地类路径中的类,因为本地应用程序代码是受信任的,但不授予从其他位置下载的代码任何权限。因此,计算引擎服务器限制其执行的任务(其代码未知是否受信任且可能具有敌意)执行需要安全权限的任何操作。示例客户端的Pi任务不需要任何权限来执行。

在此示例中,服务器程序的策略文件名为server.policy,客户端程序的策略文件名为client.policy

启动服务器

在启动计算引擎之前,您需要启动 RMI 注册表。RMI 注册表是一个简单的服务器端引导命名工具,使远程客户端能够获取对初始远程对象的引用。它可以通过rmiregistry命令启动。在执行rmiregistry之前,您必须确保您将运行rmiregistry的 shell 或窗口要么没有设置CLASSPATH环境变量,要么具有不包括您希望下载到远程对象的客户端的任何类路径的CLASSPATH环境变量。

要在服务器上启动注册表,请执行rmiregistry命令。此命令不会产生任何输出,并且通常在后台运行。在此示例中,注册表在主机mycomputer上启动。

Microsoft Windows(如果start不可用,请使用javaw):

start rmiregistry

Solaris 操作系统或 Linux

rmiregistry &

默认情况下,注册表在端口 1099 上运行。要在不同端口上启动注册表,请在命令行上指定端口号。不要忘记取消设置CLASSPATH环境变量。

Microsoft Windows

start rmiregistry 2001

Solaris 操作系统或 Linux

rmiregistry 2001 &

一旦注册表启动,你可以启动服务器。你需要确保compute.jar文件和远程对象实现类都在你的类路径中。当你启动计算引擎时,你需要使用java.rmi.server.codebase属性指定服务器类的网络访问位置。在这个例子中,要提供下载的服务器端类是ComputeTask接口,在用户annpublic_html\classes目录中的compute.jar文件中可用。计算引擎服务器在主机mycomputer上启动,与注册表启动的主机相同。

Microsoft Windows:

java -cp c:\home\ann\src;c:\home\ann\public_html\classes\compute.jar
     -Djava.rmi.server.codebase=file:/c:/home/ann/public_html/classes/compute.jar
     -Djava.rmi.server.hostname=mycomputer.example.com
     -Djava.security.policy=server.policy
        engine.ComputeEngine

Solaris OS 或 Linux:

java -cp /home/ann/src:/home/ann/public_html/classes/compute.jar
     -Djava.rmi.server.codebase=http://mycomputer/~ann/classes/compute.jar
     -Djava.rmi.server.hostname=mycomputer.example.com
     -Djava.security.policy=server.policy
        engine.ComputeEngine

上述java命令定义了以下系统属性:

  • java.rmi.server.codebase属性指定了一个代码库 URL,从这个服务器可以下载源自*的类的定义。如果代码库指定了一个目录层次结构(而不是一个 JAR 文件),你必须在代码库 URL 的末尾包含一个斜杠。

  • java.rmi.server.hostname属性指定了要放在此 Java 虚拟机中导出的远程对象存根中的主机名或地址。当客户端尝试通信远程方法调用时,客户端使用的值是主机名或地址。默认情况下,RMI 实现使用服务器的 IP 地址,如java.net.InetAddress.getLocalHost API 所示。然而,有时,这个地址对于所有客户端都不合适,一个完全合格的主机名会更有效。为了确保 RMI 使用一个对所有潜在客户端都可路由的主机名(或 IP 地址)作为服务器,设置java.rmi.server.hostname属性。

  • java.security.policy属性用于指定包含您打算授予的权限的策略文件。

启动客户端

一旦注册表和计算引擎运行起来,你可以启动客户端,指定以下内容:

  • 客户端提供其类(Pi类)的位置,使用java.rmi.server.codebase属性

  • java.security.policy属性用于指定包含您打算授予各种代码片段的权限的安全策略文件

  • 作为命令行参数,服务器的主机名(以便客户端知道在哪里找到Compute远程对象)和在π符号计算中使用的小数位数

在另一台主机上启动客户端(例如名为mysecondcomputer的主机)如下:


Microsoft Windows:

java -cp c:\home\jones\src;c:\home\jones\public_html\classes\compute.jar
     -Djava.rmi.server.codebase=file:/c:/home/jones/public_html/classes/
     -Djava.security.policy=client.policy
        client.ComputePi mycomputer.example.com 45

Solaris OS 或 Linux:

java -cp /home/jones/src:/home/jones/public_html/classes/compute.jar
     -Djava.rmi.server.codebase=http://mysecondcomputer/~jones/classes/
     -Djava.security.policy=client.policy
        client.ComputePi mycomputer.example.com 45

注意,类路径是在命令行上设置的,以便解释器可以找到客户端类和包含接口的 JAR 文件。还要注意,java.rmi.server.codebase属性的值,指定一个目录层次结构,以斜杠结尾。

在启动客户端后,将显示以下输出:

3.141592653589793238462643383279502884197169399

以下图示说明了在程序执行期间rmiregistryComputeEngine服务器和ComputePi客户端获取类的位置。

在程序执行期间注册表、计算引擎和客户端获取类

ComputeEngine服务器在注册表中绑定其远程对象引用时,注册表会下载存根类依赖的ComputeTask接口。这些类是从ComputeEngine服务器的 Web 服务器或文件系统下载的,具体取决于启动服务器时使用的代码库 URL 的类型。

因为ComputePi客户端在其类路径中同时具有ComputeTask接口的定义,它从其类路径加载它们的定义,而不是从服务器的代码库加载。

最后,当Pi对象在executeTask远程调用中传递给ComputeEngine对象时,Pi类被加载到ComputeEngine服务器的 Java 虚拟机中。Pi类是由服务器从客户端的 Web 服务器或文件系统加载的,具体取决于启动客户端时使用的代码库 URL 的类型。

教程:Java SE 中的安全功能

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

在本教程中,您将了解内置的 Java™安全功能如何保护您免受恶意程序的侵害。您将看到如何使用工具来控制对资源的访问,生成和检查数字签名,以及创建和管理用于签名生成和检查所需的密钥。您还将看到如何将加密服务(如数字签名生成和检查)整合到您的程序中。

Java 开发工具包(JDK™)提供的安全功能面向各种受众:

  • 运行程序的用户

    内置的安全功能可保护您免受恶意程序(包括病毒)的侵害,保护您文件的隐私和关于您的信息,并验证每个代码提供者的身份。当您需要时,您可以对应用程序和小程序进行安全控制。

  • 开发人员

    您可以使用 API 方法将安全功能整合到您的程序中,包括加密服务和安全检查。API 框架使您能够定义和整合自己的权限(控制对特定资源的访问)、加密服务实现、安全管理器实现和策略实现。此外,还提供了用于管理您信任的人的公钥/私钥对和公钥证书的类。

  • 系统管理员、开发人员和用户

    JDK 工具管理您的密钥库(密钥和证书的数据库);为 JAR 文件生成数字签名,并验证这些签名的真实性和已签名内容的完整性;以及创建和修改定义安装安全策略的策略文件。


注意: 对于想要创建小程序和 Java Web 启动应用程序的开发人员,请参阅 Java 小程序以获取安全信息。


教程课程

教程图标  创建策略文件 展示了如何通过策略文件控制资源访问。有关策略配置文件的最新信息,请参阅策略指南 页面。

教程图标  快速浏览控制应用程序 在前一课程的基础上展示了资源访问的控制,例如对于在安全管理器下运行的应用程序,除非在策略文件中明确允许,否则不允许读取或写入文件等资源访问。

Trail icon  用于安全代码和文件交换的 API 和工具使用定义了数字签名、证书和密钥库,并讨论了它们为何需要。它还回顾了适用于接下来三个课程的信息,这些课程通常需要使用工具或 API 生成签名、导出/导入证书等步骤。

Trail icon  签署代码并授予权限展示了所有与安全相关的工具的使用。它展示了开发人员签署和分发代码供他人运行的步骤。本课程还展示了运行代码的人(或系统管理员)如何在策略文件中添加条目以授予代码所需的资源访问权限。

Trail icon  文件交换展示了一个人使用工具签署重要文件,如合同,并导出与用于签署合同的私钥对应的公钥证书。然后本课程展示了另一个人如何导入证书并验证签名,该人收到了合同、签名和公钥证书。

Trail icon  生成和验证签名逐步引导您通过一个示例,使用 JDK 安全 API 编写 Java 程序生成密钥,使用私钥为数据生成数字签名,并将公钥和签名导出到文件。然后示例展示了编写第二个程序,该程序可能预期在另一个人的计算机上运行,导入公钥并验证签名的真实性。最后,示例讨论了基本程序使用的方法的潜在弱点,并演示了可能的替代方法和提供和导入密钥的方法,包括在证书中。

Trail icon  实现自己的权限演示了如何编写一个定义自己特殊权限的类。

更多信息

JDK 安全发布文档可以在安全指南页面找到。此索引页面列出了规范,提供了关于最新安全功能的详细信息,包括架构规范、使用指南、API 文档和工具文档。

教训:安全功能概述

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

安全功能帮助您保护程序和数据免受伤害,保持数据受到保护和私密,并在安全意识的运行时环境中部署新应用程序。

Java 还提供了几个工具,帮助您管理对系统资源的访问;创建、存储和维护加密的公共和私有密码(密钥对)和证书;以及在部署过程中创建和签名 jar 文件。


注意: 要了解 Java SE 7 中支持的安全功能的简要概述,请参阅安全指南目录中的安全概述白皮书。


课程:创建策略文件

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

这节课展示了如何创建一个控制资源访问的策略文件。

本课程的步骤是:

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

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

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

策略文件是一个 ASCII 文本文件,可以通过文本编辑器或本节中演示的图形化策略工具来编写。策略工具可以节省您的输入时间,消除您需要了解策略文件所需语法的需求,从而减少错误。

本课程使用策略工具创建名为 examplepolicy 的策略文件,在其中您将添加一个 策略条目,授予来自 examples 目录的代码写入权限。

按照以下步骤创建和修改您的新策略文件:

  1. 启动策略工具

  2. 授予所需权限

  3. 保存策略文件


UNIX 用户注意事项: 这些步骤说明了如何为 Windows 系统创建策略文件。如果您在 UNIX 系统上工作,步骤完全相同。当文本中说要将策略文件存储在 C:\Test 目录中时,您可以将其存储在其他目录中。本课程中的示例假定您将其存储在 ~/test 目录中。


启动 Policy Tool

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

要启动 Policy Tool,只需在命令行中输入以下内容:

policytool

这将打开 Policy Tool 窗口。

每次启动 Policy Tool 时,它会尝试从用户策略文件中填充此窗口中的策略信息。用户策略文件默认在您的主目录中命名为.java.policy。如果 Policy Tool 找不到用户策略文件,它会发出警告并显示一个空白的 Policy Tool 窗口(一个带有标题和按钮但没有数据的窗口),如下图所示。

一个空白的 Policy Tool 窗口

然后,您可以选择打开现有的策略文件或创建新的策略文件。

第一次运行 Policy Tool 时,您会看到空白的 Policy Tool 窗口,因为用户策略文件尚不存在。您可以立即继续创建新的策略文件,如下一步所述。

授予所需权限

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

要创建新条目,请在主策略工具窗口中单击 添加策略条目 按钮。这将显示如下图所示的策略条目对话框。

策略条目对话框

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

CodeBaseSignedBy 文本框指定您要授予权限的代码,这些权限将添加到文件中。

  • CodeBase 值表示代码源位置;您授予来自该位置的代码的权限。空的 CodeBase 条目表示“任何代码”--代码的来源并不重要。

  • SignedBy 值表示存储在密钥库中的证书的别名。该证书内的公钥用于验证代码上的数字签名。您授予由与别名指定的密钥库条目中的公钥对应的私钥签名的代码的权限。SignedBy 条目是可选的;省略它表示“任何签名者”--代码是否签名或由谁签名并不重要。

如果您同时拥有 CodeBaseSignedBy 条目,权限仅授予来自指定位置且由指定别名签名的代码。

您可以授予存储示例的位置(URL)的所有代码权限。

在策略条目对话框的 CodeBase 文本框中键入以下 URL:

https://docs.oracle.com/javase/tutorial/security/tour1/examples/

注意: 这是一个 URL。因此,它必须始终使用斜杠作为分隔符,而不是反斜杠。

SignedBy 文本框留空,因为您不需要代码签名。


注意: 要授予权限给任何代码(.class 文件),不仅仅是从先前指定的目录,而是从 security 目录及其子目录中,将以下 URL 键入 CodeBase 框中:

https://docs.oracle.com/javase/tutorial/security/


您已经指定了代码的来源(CodeBase),并且代码不需要签名(因为没有 SignedBy 值)。

您现在已经指定了此策略条目,因此在策略条目对话框中单击 完成 按钮。策略工具窗口现在包含代表策略条目的一行,显示 CodeBase 值。

策略工具窗口,显示新的策略条目

注意: 我们将在下一课中授予此新策略条目的权限。

保存策略文件

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

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

在控制应用程序快速入门课程中的示例假设您将策略文件存储在C:驱动器上的Test目录中。

导航到Test目录。键入文件名examplepolicy,然后点击保存

策略文件现在已保存,其名称和路径显示在标记为Policy File的文本框中。

显示策略文件的 PolicyTool 窗口

通过从文件菜单中选择退出来退出策略工具。

课程:快速浏览控制应用程序

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


先决条件课程: 创建策略文件


本课程展示如何使用安全管理器为应用程序授予或拒绝对系统资源的访问权限。本课程还展示了资源访问,如读取或写入文件,对于使用安全管理器运行的应用程序,除非在策略文件中明确允许,否则是不允许的。

本课程的步骤为:

  1. 观察应用程序自由

  2. 查看如何限制应用程序

  3. 设置策略文件以授予所需权限

  4. 查看策略文件的效果

观察应用程序自由

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

应用程序运行时,不会自动安装安全管理器。在下一步中,您将看到如何将相同的安全策略应用于在本地文件系统上找到的应用程序和下载的沙箱小程序。但首先,让我们证明默认情况下未安装安全管理器用于应用程序,因此应用程序可以完全访问资源。

在您的计算机上创建一个名为GetProps.java的文件,可以通过复制或下载GetProps.java源代码来实现。

本课程中的示例假定您将GetProps.java放在C:\Test目录中(如果您使用 Windows 系统)或在 UNIX 上的~/test目录中。

如您所见,如果检查源文件,此程序尝试获取(读取)属性值,其名称为"os.name""java.version""user.home""java.home"

现在编译并运行GetProps.java。您应该看到类似以下的输出:

C:\TEST>java GetProps
    About to get os.name property value
      The name of your operating system is:
      Windows XP
    About to get java.version property value
      The version of the JVM you are running is:
      1.6.0
    About to get user.home property value
      Your user home directory is: C:\WINDOWS
    About to get java.home property value
      Your JRE installation directory is:
      C:\JDK7.0.0\JRE

这表明应用程序被允许访问所有属性值,如下图所示。

应用程序可以读取属性值

查看如何限制应用程序

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

正如您在上一步骤中看到的,当 Java 运行时运行一个应用程序时,它不会自动安装安全管理器。要将相同的安全策略应用于本地文件系统中找到的应用程序和下载的沙箱小程序,您可以使用新的-Djava.security.manager命令行参数调用解释器。

要使用默认安全管理器执行GetProps应用程序,请键入以下内容:

java -Djava.security.manager GetProps

以下是程序的输出:

C:\TEST>java -Djava.security.manager GetProps
    About to get os.name property value
      The name of your operating system is: SunOS
    About to get java.version property value
      The version of the JVM you are running is: 1.7.0
    About to get user.home property value
    Caught exception java.security.AccessControlException:
        access denied ("java.util.PropertyPermission"
        "user.home" "read")

过程如下图所示。

应用程序被阻止读取属性

安全敏感属性

Java 运行时默认加载默认策略文件,并授予所有代码访问一些常用属性(如"os.name""java.version")的权限。这些属性不是安全敏感的,因此授予这些权限通常不会构成安全风险。

GetProps尝试访问的其他属性"user.home""java.home"不在系统策略文件授予读取权限的属性之列。因此,一旦GetProps尝试访问这些属性中的第一个("user.home")时,安全管理器会阻止访问并报告AccessControlException。此异常表示当前生效的策略,其中包含一个或多个策略文件中的条目,不允许读取"user.home"属性的权限。


注意: 代码始终可以从与其所在目录相同的目录(或该目录的子目录)中读取文件;它不需要明确的权限来这样做。代码还可以获取其执行目录的路径名,而此路径名可能包含敏感信息。例如,如果代码是从主目录(或主目录的子目录)执行的,则路径名可能会显示当前用户的名称。


默认策略文件

默认策略文件,java.policy 默认位于:

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

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

请注意,java.home代表"java.home"属性的值,该属性是指定 JRE 安装目录的系统属性。因此,如果 JRE 安装在 Windows 上名为C:\jdk\jre,在 UNIX 上名为/jdk/jre的目录中,则系统策略文件位于:

  • WindowsC:\jdk\jre\lib\security\java.policy

  • UNIX/jdk/jre/lib/security/java.policy

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

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

这一步使用策略工具实用程序打开名为examplepolicy的策略文件,该文件是在创建策略文件课程中创建的。您将添加一个新的策略条目,允许来自存储GetProps.class的目录的代码读取"user.home""java.home"属性值,如下图所示。

示例策略文件授予和所需的权限

步骤如下。

  1. 打开策略文件

  2. 授予所需的权限

  3. 保存策略文件


UNIX 用户注意: 本说明演示了为 Windows 系统创建策略文件。如果您在 UNIX 系统上工作,则步骤完全相同,只有以下区别。

  • 您从您的主目录中的test目录中检索examplepolicy文件。

  • 对于授予所需权限的步骤中的CodeBase URL,您可以将file:${user.home}/test/替换为file:/C:/Test/。或者,您可以直接指定您的主目录,而不是引用"user.home"属性,如file:/home/jones/test/


打开策略文件

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

通过在命令行中键入以下内容启动策略工具:

policytool

这将打开策略工具窗口。要打开examplepolicy策略文件,请在文件菜单中使用打开命令。这将显示一个打开对话框,您可以使用它来浏览目录结构,直到找到包含策略文件的目录(即C:\Test\目录)。

选择该目录中的examplepolicy文件,然后选择打开按钮。

这将从examplepolicy策略文件中填充策略工具窗口中的信息,包括策略文件名和由创建策略文件课程创建的CodeBase部分的策略条目。

显示策略文件的策略工具窗口

授予所需权限

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

要授予GetProps应用程序读取"user.home""java.home"属性值的权限,必须创建一个授予这些权限的策略条目。在主策略工具窗口中选择添加策略条目按钮。这将弹出策略条目对话框,如下图所示。

策略条目对话框

CodeBase文本框中键入以下文件 URL,以指示您将授予来自指定目录中的代码的权限,该目录是存储GetProps.class的目录。

file:/C:/Test/

(注意,这是一个 URL,因此必须始终使用斜杠,而不是反斜杠。)

SignedBy文本框留空,因为您不需要代码签名。

要添加读取"user.home"属性值的权限,请选择添加权限按钮。这将弹出权限对话框。

权限对话框

执行以下操作。

  1. 从权限下拉列表中选择属性权限。完整的权限类型名称(java.util.PropertyPermission)现在出现在下拉列表右侧的文本框中。

  2. 在标有目标名称列表右侧的文本框中键入以下内容,以指定"user.home"属性:

    user.home
    
    
  3. 通过从操作下拉列表中选择读取选项来指定读取此属性的权限。

现在权限对话框如下所示。

填写文本字段的权限对话框

选择确定按钮。新权限将出现在策略条目窗口中的一行中。

策略条目窗口中的新权限

要添加读取"java.home"属性值的权限,请再次选择添加权限按钮。在权限对话框中,执行以下操作:

  1. 从权限下拉列表中选择属性权限

  2. 在标有目标名称列表右侧的文本框中键入以下内容,以指定"java.home"属性:

    java.home
    
    
  3. 通过从操作下拉列表中选择读取选项来指定读取此属性的权限。

现在权限对话框如下所示。

将 java home 属性设置为读取的权限对话框

选择确定按钮。新权限和先前添加的权限将出现在策略条目窗口中的行中,如下图所示。

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

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

保存策略文件

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

要保存策略文件,只需在文件菜单中选择保存命令。

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

查看策略文件效果

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

现在您已经向examplepolicy策略文件添加了所需的策略条目,当您使用安全管理器执行GetProps应用程序时,您应该能够读取指定的属性,如下图所示。

GetProps 应用程序现在可以读取指定的属性

每当您运行一个小程序,或者一个带有安全管理器的应用程序时,默认加载和使用的策略文件是位于以下目录之一的"安全属性文件"中指定的文件。

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

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

注意: java.home环境变量指定了 JRE 安装的目录。

策略文件的位置是指定为属性值的形式为:

policy.url.*n*

其中变量n表示一个数字。请在以下形式的行中指定每个属性值:

policy.url.*n*=*URL*

其中URL是 URL 规范。例如,默认的策略文件,有时分别称为系统和用户策略文件,在安全属性文件中定义为

policy.url.1=file:${java.home}/lib/security/java.policy
policy.url.2=file:${user.home}/.java.policy


注意: 在安全属性文件中使用${propName}符号是指定属性值的一种方式。因此${java.home}将在运行时被实际的"java.home"属性值替换,该属性值指示了 JRE 安装的目录,${user.home}将被"user.home"属性的值替换,例如,C:\Windows


有两种可能的方式可以使examplepolicy文件被视为整体策略的一部分,除了在安全属性文件中指定的策略文件。您可以通过将附加策略文件指定为传递给运行时系统的属性,如方法 1 中所述,或者在安全属性文件中添加指定附加策略文件的行,如方法 2 中所讨论的那样。

方法 1

您可以使用-Djava.security.policy解释器命令行参数来指定一个应该在安全属性文件中指定的策略文件之外使用的策略文件。

确保您在包含GetProps.classexamplepolicy的目录中。然后,您可以运行GetProps应用程序,并通过在一行上键入以下命令将examplepolicy策略文件传递给解释器:

java -Djava.security.manager -Djava.security.policy=examplepolicy GetProps

注意: 请记住,为了使用安全管理器运行应用程序,需要-Djava.security.manager,如查看如何限制应用程序步骤中所示。

该程序报告了"user.home""java.home"属性的值。

如果应用程序仍然报告错误,则策略文件中存在问题。使用策略工具检查您在设置策略文件以授予所需权限步骤中创建的策略条目。

方法 2

您可以在安全属性文件中指定多个 URL,所有指定的策略文件都将被加载。因此,让java解释器考虑您的examplepolicy文件的策略条目的一种方法是在安全属性文件中添加指定该策略文件的条目。


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


要修改安全属性文件,请在适合编辑 ASCII 文本文件的编辑器中打开它。然后在包含policy.url.2的行之后添加以下行:如果您在 Windows 系统上,请添加

policy.url.3=file:/C:/Test/examplepolicy

如果您在 UNIX 系统上,请添加

policy.url.3=file:${user.home}/test/examplepolicy

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

policy.url.3=file:/home/jones/test/examplepolicy

运行应用程序

现在,您应该能够成功运行以下内容。

java -Djava.security.manager GetProps

与方法 1 一样,如果仍然出现安全异常,则策略文件中存在问题。使用策略工具检查您在设置策略文件以授予所需权限步骤中创建的策略条目。然后修复任何拼写错误或其他错误。



重要提示: 除非您正在运行本教程课程,否则无需包含examplepolicy文件。要排除此文件,请打开安全属性文件并删除刚刚添加的行。

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


课程:用于安全代码和文件交换的 API 和工具使用

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

本课程解释了为什么需要数字签名、证书和密钥库。该课程还比较了使用这些工具与 JDK 安全 API 生成签名的情况。这些工具的使用在接下来的两节课中进行演示,签署代码并授予权限 和 文件交换。API 的使用在 生成和验证签名 课程中进行演示。

本课程包含以下部分

  • 代码和文档安全

  • 工具和 API 注释

  • 使用 JDK 安全 API 签署文档

  • 使用工具签署代码或文档

代码和文档安全

如果您通过电子方式向某人发送重要文件(或文件)、或要运行的小程序或应用程序,接收方需要一种方法来验证文件或代码是否来自您,并且在传输过程中未被修改(例如,被恶意用户拦截)。数字签名、证书和密钥库都有助于确保您发送的文件的安全性。

数字签名

使用数字签名的基本思想如下。

  1. 您使用您可以通过keytool或安全 API 方法生成的私钥对文档或代码进行“签名”。也就是说,您使用jarsigner工具或安全 API 方法为文档或代码生成数字签名。

  2. 您将签名的文档发送给接收方。

  3. 您还向接收方提供您的公钥。这个公钥对应于您最初用于生成签名的私钥。

  4. 接收方使用您的公钥来验证您的文件是否来自您,并且在到达之前未被修改。

接收方需要确保您的公钥本身是真实的,然后才能使用它来验证您的签名是否真实。因此,您通常会提供一个包含您的公钥以及可以为您的密钥真实性作证的证书颁发机构的密钥的证书。有关详细信息,请参见下一节。

有关签名和验证术语和概念的更多信息,以及有关好处的进一步解释,请参见“JAR 文件打包程序”课程的 签署 JAR 文件 部分。

证书

证书包含:

  • 一个公钥。

  • 证书的“显著名称”信息是指证书的实体(个人、公司等)的信息。这个实体被称为证书主体所有者。显著名称信息包括以下属性(或其子集):实体的名称、组织单位、组织、城市或地点、州或省和国家代码。

  • 数字签名。证书由一个实体,颁发者,签名,以证明封闭的公钥是另一个实体,所有者的实际公钥。

  • 签名者(颁发者)的显著名称信息。

收件人检查证书是否有效的一种方法是使用颁发者(签名者)的公钥验证其数字签名。该密钥本身可以存储在另一个证书中,该证书的签名也可以通过使用下一个证书的颁发者的公钥进行验证,而该密钥可能存储在另一个证书中,依此类推。当你到达一个你已经信任的公钥并用它来验证相应证书上的签名时,你可以停止检查。

如果收件人无法建立信任链,那么他/她可以使用 keytool-import-printcert 命令计算证书的指纹。指纹是一个相对较短的数字,可以唯一且可靠地识别证书。(从技术上讲,指纹是证书信息的哈希值,使用消息摘要函数。)然后,收件人可以打电话给证书所有者,并将收到的证书的指纹值与发送的证书进行比较。如果指纹相同,则证书相同。

因此,你可以确保证书在传输过程中没有被修改。在处理证书时的另一个潜在不确定性是发送者的身份。有时证书是自签名的,即使用与证书中的公钥对应的私钥签名;颁发者与主体相同。

自签名证书对于开发和测试应用程序很有用。然而,在部署给用户之前,从一个受信任的第三方(称为认证机构(CA))那里获取证书是必要的。为此,你向 CA 发送一个自签名证书签名请求(CSR)。CA 验证 CSR 上的签名和你的身份,可能通过检查你的驾驶执照或其他信息。然后,CA 通过使用自己的(CA 的)私钥签发证书并为你作为公钥所有者背书。任何信任颁发 CA 的公钥的人现在都可以验证证书上的签名。在许多情况下,颁发 CA 本身可能有一个来自 CA 层次结构更高层的证书,导致证书链

你信任的实体的证书通常被导入到你的密钥库中作为“受信任的证书”。每个这样的证书中的公钥可以用来验证使用相应私钥生成的签名。这样的验证可以通过以下方式完成:

  • jarsigner 工具(如果文档/代码和签名出现在一个 JAR 文件中),

  • API 方法,或

  • 运行时系统,在尝试访问资源时,如果策略文件指定允许代码尝试访问的资源访问,且其签名是真实的,则允许访问。代码的类文件和签名必须在一个 JAR 文件中。

如果您要向他人发送签名的代码或文档,则需要向他们提供包含与用于签署代码/文档的私钥对应的公钥的证书。keytool-export 命令或 API 方法可以将您的证书从密钥库导出到文件中,然后可以将该文件发送给需要的任何人。接收证书的人可以将其导入到密钥库中作为受信任的证书,例如使用 API 方法或 keytool-import 命令。

如果您使用 jarsigner 工具为 JAR 文件生成签名,则该工具会从您的密钥库中检索您的证书及其支持的证书链。然后,该工具将它们与签名一起存储在 JAR 文件中。

密钥库

私钥及其相关的公钥证书存储在受密码保护的数据库中,称为密钥库。密钥库可以包含两种类型的条目:上述讨论的受信任证书条目,以及密钥/证书条目,每个条目包含一个私钥和相应的公钥证书。密钥库中的每个条目都由一个别名标识。

密钥库所有者可以在密钥库中拥有多个密钥,通过不同的别名访问。别名通常以密钥库所有者在其中使用相关密钥的特定角色命名。别名也可以标识密钥的用途。例如,别名 signPersonalEmail 可能用于标识一个用于签署个人电子邮件的密钥库条目,而别名 signJarFiles 可能用于标识一个用于签署 JAR 文件的条目。

keytool 工具可用于

  • 创建私钥及其相关的公钥证书

  • 发出证书请求,然后将其发送给适当的认证机构

  • 导入从您联系的认证机构获得的证书回复

  • 导入属于其他方的公钥证书作为受信任的证书

  • 管理您的密钥库

API 方法也可用于访问和修改密钥库。

工具和 API 注意事项

请注意以下与数字签名相关的工具和 API 使用。

  • 您可以使用 JDK 安全 API、工具或组合来生成密钥和签名,并导入证书。您可以使用这些 API 或工具功能与他人安全地交换文档。

  • 要使用工具进行文档交换,文档必须放在 JAR(Java ARchive)文件中,可以通过jar工具创建。JAR 文件是将多个文件封装在一个位置的好方法。当文件被“签名”时,生成的数字签名字节需要存储在某个地方。当 JAR 文件被签名时,签名可以放在 JAR 文件本身中。这就是当您使用jarsigner工具对 JAR 文件进行签名时发生的情况。

  • 如果您正在创建将要签署的小程序代码,它需要放在 JAR 文件中。如果您正在创建可能受到安全管理器限制的应用程序代码,同样需要放在 JAR 文件中。您需要 JAR 文件的原因是,当策略文件指定由特定实体签名的代码允许一个或多个操作,例如特定文件读取或写入时,预期代码来自已签名的 JAR 文件。(术语“已签名代码”是指“出现在已签名 JAR 文件中的类文件中的代码”的简称。)

  • 为了使运行时系统检查代码签名,将运行代码的人/组织首先需要将验证用于签署代码的私钥对应的公钥的证书导入其密钥库中。

  • 为了使jarsigner工具验证 JAR 文件签名的真实性,首先需要将接收到的 JAR 文件的人/组织导入其密钥库中,以验证与用于签署代码的私钥对应的公钥的证书。

  • 目前还没有用于证书创建的 API。

使用 JDK 安全 API 签署文档

生成和验证签名展示了如何使用 JDK 安全 API 签署文档。该课程展示了由拥有原始文档的人执行的一个程序会做什么

  • 生成密钥,

  • 使用私钥为数据生成数字签名,然后

  • 导出公钥和签名到文件。

然后展示了另一个程序的示例,由数据、签名和公钥的接收者执行。展示了该程序如何

  • 导入公钥

  • 验证签名的真实性。

本课程还展示了导入和提供密钥的替代方法,包括证书。

使用工具签署代码或文档

签署代码并授予权限课程展示了如何使用 Java 安全工具将您的代码放入 JAR 文件中,对其进行签名,并导出您的公钥。然后展示了您的接收方如何使用相同的 Java 工具导入您的公钥证书,然后向策略文件添加条目,以授予您的代码访问受接收方控制的系统资源所需的权限。

文件交换课程教你如何使用 Java 安全工具签署文档,然后使用keytool导出公钥证书,对应于使用keytool签署该文档的私钥。然后它展示了如何接收者可以通过安装您的公钥证书并使用jarsigner工具验证您的签名。

这两个课程有很多共同之处。在两种情况下,发送代码或文档的前两个步骤是:

  • 使用jar工具创建包含文档或类文件的 JAR 文件。

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

接下来的两个步骤是可选的:

  • 使用keytool-certreq命令;然后将生成的证书签名请求发送给认证机构(CA),如 VeriSign。

  • 使用keytool-import命令导入 CA 的响应。

接下来的两个步骤是必需的:

  • 使用之前生成的私钥,使用jarsigner工具对 JAR 文件进行签名。

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

在两种情况下,签署的 JAR 文件和证书的接收者应该使用keytool-import命令将证书导入为受信任的证书。keytool将尝试从要导入的证书到密钥库中已受信任的证书构建信任链。如果失败,keytool将显示证书指纹并提示您进行验证。

如果发送的是代码,则接收者还需要修改策略文件以允许由导入证书中的公钥对应的私钥签名的代码访问所需资源。可以使用策略工具来执行此操作。

如果发送的是一个或多个文档,则接收者需要使用jarsigner工具验证 JAR 文件签名的真实性。

本课程讨论了两个可选步骤。其他步骤在接下来的两个课程中涵盖,签署代码并授予权限和文件交换。

为公钥证书生成证书签名请求(CSR)

当使用keytool生成公私钥对时,它会创建一个包含私钥和公钥的自签名证书的密钥库条目。(即,证书使用相应的私钥进行签名。)这在开发和测试应用程序时是足够的。

然而,如果证书由认证机构(CA)签名,其他人更有可能信任该证书。要获得由 CA 签名的证书,首先需要生成证书签名请求(CSR),通过类似以下命令:

keytool -certreq -alias *alias* -file *csrFile* 

这里alias用于访问包含私钥和公钥证书的密钥库条目,csrFile指定此命令创建的 CSR 使用的名称。

然后,你将此文件提交给 CA,如 VeriSign, Inc.。CA 对你(请求者/"主体")进行验证,然后签署并返回一个验证你的公钥的证书。通过签署证书,CA 保证你是公钥的所有者。

在某些情况下,CA 会返回一系列证书,每个证书都用于验证链中前一个证书签发者的公钥。

导入来自 CA 的响应

在向认证机构(CA)提交证书签名请求(CSR)后,你需要通过导入 CA 返回给你的证书(或证书链)来用证书链替换密钥库中的原始自签名证书。

但首先,你需要在你的密钥库中(或下面描述的cacerts密钥库文件中)有一个"受信任证书"条目,用于验证CA的公钥。有了这样的条目,可以验证 CA 对证书的签名。也就是说,可以验证 CA 对证书的签名,或者验证 CA 发送给你作为 CSR 响应中的最终证书链上的最终证书的签名。

从 CA 导入证书作为"受信任证书"

在导入来自 CA 的证书回复之前,你需要在你的密钥库或cacerts文件中拥有一个或多个"受信任证书"。

  • 如果证书回复是一个证书链,你只需要链中的顶层证书 -- "根" CA 证书,用于验证该 CA 的公钥。

  • 如果证书回复是单个证书,你需要签发它的 CA 的证书。如果该证书不是自签名的,你需要其签发者的证书,依此类推,直到自签名的"根" CA 证书。

cacerts文件代表一个系统范围的带有 CA 证书的密钥库。该文件位于 JRE 安全属性目录*java.home*/lib/security中,其中java.home是 JRE 安装目录。


重要提示:验证你的cacerts文件

由于你信任cacerts文件中的 CA 作为签署和颁发证书给其他实体的实体,你必须仔细管理cacerts文件。cacerts文件应只包含你信任的 CA 的证书。你有责任验证cacerts文件中捆绑的受信任根 CA 证书,并做出自己的信任决定。要从cacerts文件中删除一个不受信任的 CA 证书,使用keytool命令的删除选项。你可以在 JRE 安装目录中找到cacerts文件。如果没有权限编辑此文件,请联系系统管理员。


cacerts文件包含许多受信任的 CA 证书。如果您将您的 CSR 发送给了这些受信任的供应商之一(比如 VeriSign),您就不需要将供应商的根证书作为受信任证书导入到您的密钥库中;您可以继续到下一个部分,查看如何从 CA 导入证书回复。

从 CA 获得的证书通常是自签名的或者由另一个 CA 签名的,此时您还需要一个证书来验证该 CA 的公钥。假设 ABC 公司是一个 CA,并且您获得了一个名为ABCCA.cer的文件,据称是来自 ABC 公司的自签名证书,用于验证该 CA 的公钥。

在将证书导入为“受信任”证书之前,请务必确保证书是有效的!首先查看它(使用keytool-printcert命令或者keytool-import命令,不带-noprompt选项),并确保显示的证书指纹与预期的一致。您可以联系发送证书的人,并将您看到的指纹与他们展示的或者安全的公钥存储库展示的指纹进行比较。只有当指纹相等时,才能保证证书在传输过程中没有被替换为其他人(例如,攻击者)的证书。如果发生这样的攻击,并且您在导入证书之前没有检查证书,那么您将信任攻击者签署的任何内容。

如果您相信该证书是有效的,您可以通过类似以下命令将其添加到您的密钥库中:

keytool -import -alias *alias* -file ABCCA.cer -keystore *storefile* 

此命令在密钥库中创建一个名为storefile指定的“受信任证书”条目。该条目包含来自文件ABCCA.cer的数据,并分配指定的别名。

从 CA 导入证书回复

一旦您已经导入了所需的受信任证书,就像前一节中描述的那样,或者它们已经存在于您的密钥库中或者在cacerts文件中,您可以导入证书回复,从而用证书链替换您的自签名证书。这个链将是 CA 对您的请求的回复中返回的(如果 CA 的回复是一个链)或者通过使用证书回复和已经在密钥库中或者cacerts密钥库文件中可用的受信任证书构建的(如果 CA 的回复是一个单一证书)。

举例来说,假设您将您的证书签名请求发送给了 VeriSign。然后您可以通过以下方式导入回复,假设返回的证书在certReplyFile指定的文件中:

keytool -import -trustcacerts
    -keystore *storefile*
    -alias *alias* 
    -file *certReplyFile* 

在一行上输入此命令。

证书回复通过使用密钥库中的受信任证书进行验证,并可选择使用cacerts密钥库文件中配置的证书进行验证(如果指定了-trustcacerts选项)。每个链中的证书都会被验证,使用链中下一级别的证书。您只需要信任链中顶级的“根”CA 证书。如果您尚未信任顶级证书,keytool将显示该证书的指纹,并询问您是否要信任它。

指定(通过别名)条目的新证书链将替换与该条目关联的旧证书(或链)。只有在提供有效的keypass,用于保护条目私钥的密码时,才能替换旧链。如果未提供密码,并且私钥密码与密钥库密码不同,则会提示用户输入。

关于生成证书签名请求(CSR)和导入证书回复的更详细信息,请参阅keytool文档:

课程:签署代码并授予权限

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

本课程展示如何使用keytooljarsigner策略工具jar将文件放入 JAR(Java ARchive)文件,以便后续由jarsigner工具进行签名。

本课程分为两部分。首先,您将创建和部署一个应用程序。其次,您将作为签名应用程序的接收者。

以下是创建和部署应用程序的步骤:

注意: 为方便起见,您假装是一个名为 Susan Jones 的用户/开发者。您需要在生成密钥时定义 Susan Jones。

  • 将包含应用程序的 Java 类文件放入 JAR 文件中

  • 对 JAR 文件进行签名

  • 导出与用于签署 JAR 文件的私钥对应的公钥证书

以下是向应用程序授予权限的步骤

注意: 为方便起见,您假装是一个名为 Ray 的用户。

  • 您可以看到,当在安全管理器下运行时,签名应用程序通常无法读取文件。

  • 使用keytool将证书导入 Ray 的密钥库中,别名为susan

  • 使用策略工具在 Ray 的策略文件中创建一个条目,以允许由susan签名的代码读取指定文件。

  • 最后,您将看到在安全管理器下运行的应用程序现在可以读取文件,因为已经被授予了相应的权限。

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


重要: 您需要在存储示例应用程序的目录中执行本课程中的任务,但应将应用程序所需的数据文件存储在不同的目录中。本教程中的所有示例都假定您正在C:\Test目录中工作,并且数据文件位于C:\TestData目录中。

如果您在 UNIX 系统上工作,请用您自己的目录名称替换。


以下是步骤:

  • 代码签署者的步骤

  • 代码接收者的步骤

Code Signer 的步骤

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

Code Signer 执行以下步骤:

  1. 下载并尝试示例应用程序。

  2. 创建包含类文件的 JAR 文件,使用jar工具。

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


    可选步骤 为公钥证书生成证书签名请求(CSR),并导入认证机构(CA)的响应。为简单起见(并且因为你只是假装是苏珊·琼斯),这一步被省略了。有关更多信息,请参阅为公钥证书生成证书签名请求(CSR)。


  4. 对 JAR 文件进行签名,使用jarsigner工具和私钥。

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

下载并尝试示例应用程序

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

本课程使用我们提供给您的一个简单应用程序。

  1. 通过复制或下载Count.java源代码,在本地计算机上创建一个名为Count.java的文件。本课程中的示例假定您将count放在C:\Test目录中。

  2. count应用程序需要访问包含其将处理的数据的文本文件。下载一个示例数据文件,或使用任何其他方便的文本文件作为数据。


    重要:将数据文件放入一个不同于包含已下载count类文件的目录中。我们建议使用C:\TestData\data

    在本课程的后面部分,您将看到在安全管理器下运行的应用程序除非有明确的权限,否则无法读取文件。但是,应用程序始终可以从相同目录(或子目录)中读取文件。它不需要明确的权限。


  3. 编译然后运行Count应用程序以查看其功能。

    当您运行count应用程序时,您需要指定(作为参数)要读取的文件的路径名。

    java Count C:\TestData\data

这里是一个示例运行:

    C:\Test>java Count C:\TestData\data
    Counted 65 chars.

创建一个包含类文件的 JAR 文件

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

接下来,在命令窗口中输入以下内容,创建一个包含Count.class文件的 JAR 文件:

jar cvf Count.jar Count.class

这将创建一个名为Count.jar的 JAR 文件,并将Count.class文件放入其中。

生成密钥

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

如果代码签名者尚未拥有适合签署代码的私钥,必须首先生成该私钥,以及一个相应的公钥,供代码接收方的运行时系统用于验证签名。

由于这节课假设你还没有这样的密钥,你将创建一个名为examplestore的密钥库,并创建一个带有新生成的公钥/私钥对的条目(其中公钥在证书中)。

在命令窗口中输入以下命令以创建一个名为examplestore的密钥库并生成密钥:

keytool -genkey -alias signFiles -keystore examplestore

您将被提示输入密钥和密钥库的密码。

keytool命令的子部分

让我们看看keytool的每个子部分的含义。

  • 生成密钥的命令是-genkey

  • -alias signFiles子部分指示将来用于引用包含将生成的密钥的密钥库条目的别名。

  • -keystore examplestore子部分指示您正在创建或已经使用的密钥库的名称(和可选路径)。

  • 你被提示输入的storepass值指定了密钥库密码。

  • 你被提示输入的keypass值指定了即将生成的私钥的密码。您始终需要此密码才能访问包含该密钥的密钥库条目。该条目不必有自己的密码。当提示您输入密钥密码时,您可以选择让它与密钥库密码相同。

注意:出于安全原因,您不应在命令行上设置密钥或密钥库密码,因为这样更容易被拦截。

专有名称信息

如果您使用上述keystore命令,将提示您输入您的专有名称信息。以下是提示信息;粗体表示您应该输入的内容。

What is your first and last name?
  [Unknown]:  Susan Jones 
What is the name of your organizational unit?
  [Unknown]:  Purchasing 
What is the name of your organization?
  [Unknown]:  ExampleCompany 
What is the name of your City or Locality?
  [Unknown]:  Cupertino 
What is the name of your State or Province?
  [Unknown]:  CA 
What is the two-letter country code for this unit?
  [Unknown]:  US 
Is <CN=Susan Jones, OU=Purchasing, O=ExampleCompany,
    L=Cupertino, ST=CA, C=US> correct?
  [no]:  y 

命令结果

keytool命令在执行命令的同一目录中创建名为examplestore的密钥库(如果尚不存在)。该命令为具有 Susan Jones 作为通用名称和采购部门的实体生成公钥/私钥对。

该命令创建一个包含公钥和专有名称信息的自签名证书。(您提供的专有名称将用作证书中的“主题”字段。)该证书将在 90 天内有效,如果您不指定-validity选项,则为默认有效期。该证书与密钥库条目中的私钥关联,该条目由别名signFiles引用。

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

注意: 如果接受选项的默认值或希望提示输入各种值,命令可能会更短。每当执行keytool命令时,对于未指定具有默认值的选项,将使用默认值,并提示您输入任何必需的值。对于genkey命令,具有默认值的选项包括别名(默认为mykey)、有效期(90 天)和密钥库(位于您的主目录中名为.keystore的文件)。必需的值包括dnamestorepasskeypass

签署 JAR 文件

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

现在,您可以准备签署 JAR 文件。在命令窗口中键入以下内容,以使用密钥库条目中别名为signFiles的私钥签署 JAR 文件Count.jar,并将生成的签名 JAR 文件命名为sCount.jar

jarsigner -keystore examplestore -signedjar sCount.jar Count.jar signFiles 

您将被要求输入存储密码和私钥密码。


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


建议为签名加上时间戳,如果签名未加时间戳,则会显示警告。时间戳用于验证用于签署 JAR 文件的证书在签署时是否有效。有关使用jarsigner选项在签名中包含时间戳的信息,请参阅签署 JAR 文件。

导出公钥证书

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

现在您有一个已签名的 JAR 文件sCount.jar。当签名的 JAR 文件中的Count应用程序尝试读取文件并且策略文件授予该权限给这个已签名代码时,代码接收者(Ray)的运行时系统将需要验证签名。

为了使运行时系统能够验证签名,Ray 的密钥库需要具有与用于生成签名的私钥对应的公钥。您可以通过将证书复制到名为Example.cer的文件中,向 Ray 发送验证公钥的证书。通过以下方式从密钥库examplestore复制该证书:

keytool -export -keystore examplestore -alias signFiles -file Example.cer

你将被提示输入存储密码。

代码接收者的步骤

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

在这节课中,你将扮演接收者的角色,接收包含count.class文件的已签名的 jar 文件。它请求访问你系统资源的权限,这是它通常没有权限访问的。

这个过程需要你执行下面列出的步骤。

  1. 观察受限应用程序。在导入 Susan 的证书并创建策略文件之前,该应用程序将无法访问你的系统资源。

  2. 使用keytool -import命令将 Susan 的证书导入为受信任的证书,并将其别名设置为susan

  3. 设置策略文件以授予权限给由susan签名的count应用程序读取你系统上指定的文件。

  4. 测试你重新配置的count应用程序,以验证具有受信任证书和访问你新策略文件的权限后,count现在可以读取你的data文件。

观察受限应用程序

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

控制应用程序快速浏览课程的最后一部分展示了如何通过使用新的-Djava.security.manager命令行参数调用解释器来在安全管理器下运行应用程序。但是,如果要调用的应用程序位于 JAR 文件中怎么办?

解释器选项之一是-cp(用于类路径)选项,它允许您为应用程序类和资源指定搜索路径。因此,要在sCount.jar JAR 文件内执行Count应用程序,并将文件C:\TestData\data指定为其参数,您可以在包含sCount.jar的目录中键入以下命令:

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

要使用安全管理器执行应用程序,请添加-Djava.security.manager,如下所示:

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


重要: 运行此命令时,您的 Java 解释器将抛出下面显示的异常:

Exception in thread "main" java.security.AccessControlException:
access denied (java.io.FilePermission C:\TestData\data read)
    at java.security.AccessControlContext.checkPermission(Compiled Code)
    at java.security.AccessController.checkPermission(Compiled Code)
    at java.lang.SecurityManager.checkPermission(Compiled Code)
    at java.lang.SecurityManager.checkRead(Compiled Code)
    at java.io.FileInputStream.<init>(Compiled Code)
    at Count.main(Compiled Code)

在此示例中,AccessControlException报告说count应用程序没有权限读取文件C:\TestData\data。您的解释器引发此异常,因为除非在policy文件中包含的grant语句中明确允许,否则它不会允许任何在安全管理器下运行的应用程序读取文件或访问其他资源。


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

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

在授予已签署代码读取指定文件的权限之前,您需要将 Susan 的证书作为受信任的证书导入到您的密钥库中。

假设您已经从 Susan 那里收到了

  • 已签署的 JAR 文件sCount.jar,其中包含Count.class文件,以及

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

即使您创建了这些文件并且它们实际上还没有被传输到任何地方,您也可以模拟成不是创建者和发送者 Susan 的其他人。假装您现在是 Ray。作为 Ray,您将创建一个名为exampleraystore的密钥库,并将其用于将证书导入到具有别名susan的条目中。

每当您使用指定尚不存在的密钥库的keytool命令时,都会创建一个密钥库。因此,我们可以通过单个keytool命令创建exampleraystore并导入证书。在命令窗口中执行以下操作。

  1. 转到包含公钥证书文件Example.cer的目录。(实际上,您应该已经在那里,因为本课程假设您在整个过程中都保持在一个目录中。)

  2. 在一行上输入以下命令:

    keytool -import -alias susan
       -file Example.cer -keystore exampleraystore
    
    

由于密钥库尚不存在,将会创建它,并提示您输入密钥库密码;输入任何您想要的密码。

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

keytool -printcert -file Example.cer

如果她看到的指纹与keytool向您报告的指纹相同,则证书在传输过程中未被修改。在这种情况下,您可以让keytool继续将受信任的证书条目放入密钥库中。该条目包含来自文件Example.cer的公钥证书数据,并被分配别名susan

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