Java - SocketException:客户端发送请求时连接重置

Java - SocketException: Connection Reset when Client Send Request

Java Network Programming 4th Edition, Chapter 10 about Secure Socket, there is an example of build Secure Server. The code can be find here.

我正在尝试制作更简单的代码版本。这是我的代码:

 try {
        SSLContext context = SSLContext.getInstance(algorithm);
        KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
        KeyStore ks = KeyStore.getInstance("JKS");

        //char[] password = System.console().readPassword("Password: "); // open the .class with command line
        char[] password = {'t', 'h', 'i', 's', 'i', 's', 't', 'i', 'a', 'n'};

        ks.load(new FileInputStream("src/jnp4e.keys"), password);            
        kmf.init(ks, password);
        context.init(kmf.getKeyManagers(), null, null); // null = accept the default

        // wipe the password
        Arrays.fill(password, '0');

        SSLServerSocketFactory factory
                = context.getServerSocketFactory();
        SSLServerSocket server
                = (SSLServerSocket) factory.createServerSocket(PORT);

        // add anonymous (non-authenticated) cipher suites
        String[] supported = server.getSupportedCipherSuites();
        String[] anonCipherSuitesSupported = new String[supported.length];
        int numAnonCipherSuitesSupported = 0;
        for (int i = 0; i < supported.length; i++) {
            if (supported[i].indexOf("_anon_") > 0) {
                anonCipherSuitesSupported[numAnonCipherSuitesSupported++]
                        = supported[i];
            }
        }
        String[] oldEnabled = server.getEnabledCipherSuites();
        String[] newEnabled = new String[oldEnabled.length
                + numAnonCipherSuitesSupported];
        System.arraycopy(oldEnabled, 0, newEnabled, 0, oldEnabled.length);
        System.arraycopy(anonCipherSuitesSupported, 0, newEnabled,
                oldEnabled.length, numAnonCipherSuitesSupported);
        server.setEnabledCipherSuites(newEnabled);

        System.out.println("OK..");

        // Now all the set up is complete and we can focus
        // on the actual communication.
        while (true) {
            // This socket will be secure,
            // but there's no indication of that in the code!
            try (Socket theConnection = server.accept()) {
                InputStream in = theConnection.getInputStream();
                Reader r = new InputStreamReader(in, "UTF-8");
                int c;
                while ((c = r.read()) != -1) {
                    System.out.write(c);
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    } catch (IOException | KeyManagementException | KeyStoreException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException ex) {
        ex.printStackTrace();
    }

唯一的区别是在我的代码中我创建了一个 Reader 以便服务器可以读取字符。 我用发送文本的简单客户端尝试了这个服务器。这是客户:

int port = 7000; // default https port
    String host = "localhost";
    SSLSocketFactory factory
            = (SSLSocketFactory) SSLSocketFactory.getDefault();


    SSLSocket socket = null;
    try {
        socket = (SSLSocket) factory.createSocket(host, port);

        // enable all the suites
        String[] supported = socket.getSupportedCipherSuites();
        socket.setEnabledCipherSuites(supported);

        Writer out = new OutputStreamWriter(socket.getOutputStream(), "UTF-8");
        out.write("Hello");

    }catch(Exception e){

    }

我运行 先是服务器,然后是客户端。但是当服务器接受来自客户端的输入时,它抛出这个异常:

java.net.SocketException: Connection reset
at java.net.SocketInputStream.read(SocketInputStream.java:189)
at java.net.SocketInputStream.read(SocketInputStream.java:121)
...

更新客户端 根据戴夫的回答,我添加了 2 行代码 flush()close()

...
out.write("Hello");
out.flush();
socket.close();
...

但另一个例外来了:

javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_unknown

套接字流上的 OutputStreamWriter 显然有缓冲,而您的客户端 没有 .flush().close() 因此您的数据实际上并未发送。

如果您的 Java 程序(或更确切地说是 JVM) 退出时没有对套接字流执行 .close()(包括关闭传递给Stream)处理取决于平台;在 Windows 上,它发送一个 RST,导致您在对等方看到的 "Connection reset" 异常。 Unix在TCP级别正常关闭连接,这对于SSL/TLS来说其实并不完全正常,但是"close enough"(可以说)Java将其视为EOF。

编辑 后续问题:

服务器获取 SSLHandshakeException "received alert bad_certificatecertificate_unknown" 理论上可能意味着一些事情,但几乎总是 意味着服务器的证书是使用(来自您加载的密钥库以及匹配的私钥)未由客户端信任的 CA(证书颁发机构)签名。

您为客户端显示的代码不会设置或更改其信任库;如果其他地方没有这样做的代码,或者像 java 命令行选项 -Dx=y 这样的外部设置来设置系统属性,客户端将使用 JSSE default truststore,如果文件 JRE/lib/security/jssecacerts 存在,则为文件 JRE/lib/security/cacerts(其中 JRE 是安装 JRE 的目录;如果使用 JDK,则 JRE 是JDK 目录)。如果您(以及您系统上的任何其他人)在安装 JRE 后未修改这些文件,则 jssecacerts 不存在并且 cacerts 包含一组 "well-known root" CA 由 Oracle 确定,如 Verisign 和 Equifax 等

因此,您需要:

  • 使用知名CA颁发的证书;如果您还没有这样的证书,则必须通过(至少)证明您对认证域名的控制权并根据 CA 可能支付的费用从 CA 获得它;如果您确实拥有或获得这样的证书,请将其安装在您的密钥库中,in 私钥条目,with 任何链证书(众所周知CA 几乎总是至少有一个链证书)。

  • 使用任何 其他 CA 颁发的证书,包括您创建的临时 CA,并且 包括 作为极限情况下 selfsigned 证书隐式是它自己的 CA,例如 keytool -genkeypair 自动生成的证书;并将该 CA 的 CA 证书(或该自签名证书)放入客户端使用的信任库。为此,有两种方法:

    • 将服务器的 CA 证书(或自签名证书)放入客户端使用的 JRE 的默认信任库文件中。这确实会影响共享该默认信任库的任何其他程序,这可能是使用该 JRE 的所有其他程序。如果你使用 jssecacerts 它只会影响 JSSE,如果你使用 cacerts 它也会影响签名代码的验证(如果有的话),而且如果你就地升级 JRE 它会被清除,就像通常那样Windows.

    • 自动
    • 创建(或重用)另一个信任库,将服务器的 CA 证书放在那里,并让客户端使用该非默认信任库。有几个选项:在外部设置默认信任库的系统属性,在您的程序中显式设置它们(在首次使用 JSSE 之前!),显式加载 "keystore" 文件(实际上包含证书)并在非默认 SSLSocketFactory 中使用它的 trustmanager,就像您的服务器代码对 keymanager 所做的那样,或者甚至用您喜欢的任何商店编写您自己的 trustmanager 并类似地使用它。

编辑#2 简单示例

详细介绍所有这些选项会太长,但是 一个 简单选项如下。

  1. 生成密钥对和(默认)自签名证书:

keytool -genkeypair -keystore ksfile -keyalg RSA

对于提示"first and last name"(实际上是证书中的CommonName 属性)输入服务器的名称,特别是客户端将用于连接到服务器的名称;在问题中这是 "localhost"。其他名称字段无关紧要;根据您的喜好填写或省略它们,除了 Country 如果使用必须是 2 个字母,如提示所述。您可以在命令行上添加 -dname "CN=common_name_value" 而不是回答提示。如果您有多个服务器名称,则此处省略了一些选项。 对于某些其他应用程序,您可能需要使用 -alias name 指定条目名称;对于这个问题,不需要。

  1. 获取证书副本:

keytool -exportcert -rfc -keystore ksfile [-alias name] -file certfile

在此示例中,客户端与服务器位于同一台计算机上。在另一种情况下,需要将该文件的内容从服务器复制到客户端;这通常通过复制文件最方便地完成。

  1. 将证书放入客户端信任库。如上所述,有很多选项,但您可能最常看到的建议是将它放在 JRE 默认文件 cacerts:
  2. 中,因为它通常启动最快

keytool -importcert -keystore JRE/lib/security/cacerts -file certfile

其中 JRE 是安装 JRE(Java 运行时环境)的目录。这取决于您的 OS 以及您如何安装 JRE(或 JDK,其中包括 JRE),例如是否使用包管理器。如果您安装了多个 JRE and/or JDK,这取决于您使用的是哪一个。

在 Unix 上,如果你在没有指定路径的情况下调用 javawhich java(或者在 bash 和其他 shell 中,type java)会告诉你完整的路径名即运行。但是请注意,这通常是对实际位置的符号 link,其形式应为 /somewhere/java[version]/bin/java;将 bin/java 部分更改为 lib/security/cacerts.

在 Windows 上,如果你安装一个普通的 "system-wide" Java,你 运行 的程序是 %windir%\system32\java.exewindir 通常是c:\windows 但 JRE 的实际代码和文件在 c:\program files\java\jreNc:\program files (x86)\java\jreN 中,具体取决于您的体系结构(32 位或 64 位),jreN 当前为 jre7jre8 适用,但将来可能会扩展。或 运行 Java 控制面板;在 Java 选项卡中,“查看”按钮显示已安装 JRE 的位置。