【问题标题】:Simple Java HTTPS server简单的 Java HTTPS 服务器
【发布时间】:2011-01-19 11:47:31
【问题描述】:

我需要为 Java 应用程序设置一个真正轻量级的 HTTPS 服务器。这是一个模拟器,在我们的开发实验室中用于模拟野外设备接受的 HTTPS 连接。因为它纯粹是一个轻量级的开发工具,根本不以任何方式用于生产,我很乐意绕过认证和尽可能多的协商。

我计划在 Java 6 SE 中使用 HttpsServer 类,但我正在努力让它工作。作为测试客户端,我在 cygwin 命令行 (wget https://[address]:[port]) 中使用 wget,但 wget 报告它“无法建立 SSL 连接”。

如果我使用-d 选项运行wget 进行调试,它会告诉我“SSL 握手失败”。

我花了 30 分钟在谷歌上搜索,一切似乎都指向了相当无用的 Java 6 文档,该文档描述了这些方法,但实际上并没有谈论如何让该死的东西说话或提供任何示例代码.

谁能把我推向正确的方向?

【问题讨论】:

    标签: java ssl https java-6


    【解决方案1】:

    我最终使用的是这个:

    try {
        // Set up the socket address
        InetSocketAddress address = new InetSocketAddress(InetAddress.getLocalHost(), config.getHttpsPort());
    
        // Initialise the HTTPS server
        HttpsServer httpsServer = HttpsServer.create(address, 0);
        SSLContext sslContext = SSLContext.getInstance("TLS");
    
        // Initialise the keystore
        char[] password = "simulator".toCharArray();
        KeyStore ks = KeyStore.getInstance("JKS");
        FileInputStream fis = new FileInputStream("lig.keystore");
        ks.load(fis, password);
    
        // Set up the key manager factory
        KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
        kmf.init(ks, password);
    
        // Set up the trust manager factory
        TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
        tmf.init(ks);
    
        // Set up the HTTPS context and parameters
        sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
        httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext) {
            public void configure(HttpsParameters params) {
                try {
                    // Initialise the SSL context
                    SSLContext c = SSLContext.getDefault();
                    SSLEngine engine = c.createSSLEngine();
                    params.setNeedClientAuth(false);
                    params.setCipherSuites(engine.getEnabledCipherSuites());
                    params.setProtocols(engine.getEnabledProtocols());
    
                    // Get the default parameters
                    SSLParameters defaultSSLParameters = c.getDefaultSSLParameters();
                    params.setSSLParameters(defaultSSLParameters);
                } catch (Exception ex) {
                    ILogger log = new LoggerFactory().getLogger();
                    log.exception(ex);
                    log.error("Failed to create HTTPS port");
                }
            }
        });
        LigServer server = new LigServer(httpsServer);
        joinableThreadList.add(server.getJoinableThread());
    } catch (Exception exception) {
        log.exception(exception);
        log.error("Failed to create HTTPS server on port " + config.getHttpsPort() + " of localhost");
    }
    

    要生成密钥库:

    $ keytool -genkeypair -keyalg RSA -alias self_signed -keypass simulator \
      -keystore lig.keystore -storepass simulator
    

    另见here。

    可能 storepass 和 keypass 可能不同,在这种情况下 ks.load 和 kmf.init 必须分别使用 storepass 和 keypass。

    【讨论】:

    • 嗨,还是个新手。只是想知道您是否使用了设置配置器时创建的 SSLContext?由于您覆盖配置方法并调用 getDefault()。不使用构造函数中传递的 SSLContext。
    • 请注意,在 JDK 11 和 12 中,HttpsServer 默认无法正确处理 TLSv1.3 连接并导致 an infinite loop。 JDK 错误在here 中讨论。解决方法是设置系统属性:-Djdk.tls.acknowledgeCloseNotify=true
    【解决方案2】:

    我为 HTTPS 服务器(不是基于套接字的)更新了您的答案。它可能有助于 CSRF 和 AJAX 调用。

    import java.io.*;
    import java.net.InetSocketAddress;
    import java.lang.*;
    import java.net.URL;
    import com.sun.net.httpserver.HttpsServer;
    import java.security.KeyStore;
    import javax.net.ssl.KeyManagerFactory;
    import javax.net.ssl.TrustManagerFactory;
    import com.sun.net.httpserver.*;
    import javax.net.ssl.SSLEngine;
    import javax.net.ssl.SSLParameters;
    
    import java.io.InputStreamReader;
    import java.io.Reader;
    import java.net.URLConnection;
    
    import javax.net.ssl.HostnameVerifier;
    import javax.net.ssl.HttpsURLConnection;
    import javax.net.ssl.SSLContext;
    import javax.net.ssl.SSLSession;
    import javax.net.ssl.TrustManager;
    import javax.net.ssl.X509TrustManager;
    import java.security.cert.X509Certificate;
    
    import java.net.InetAddress;
    import com.sun.net.httpserver.HttpExchange;
    import com.sun.net.httpserver.HttpHandler;
    import com.sun.net.httpserver.HttpServer;
    import com.sun.net.httpserver.HttpsExchange;
    
    public class SimpleHTTPSServer {
    
        public static class MyHandler implements HttpHandler {
            @Override
            public void handle(HttpExchange t) throws IOException {
                String response = "This is the response";
                HttpsExchange httpsExchange = (HttpsExchange) t;
                t.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
                t.sendResponseHeaders(200, response.getBytes().length);
                OutputStream os = t.getResponseBody();
                os.write(response.getBytes());
                os.close();
            }
        }
    
        /**
         * @param args
         */
        public static void main(String[] args) throws Exception {
    
            try {
                // setup the socket address
                InetSocketAddress address = new InetSocketAddress(8000);
    
                // initialise the HTTPS server
                HttpsServer httpsServer = HttpsServer.create(address, 0);
                SSLContext sslContext = SSLContext.getInstance("TLS");
    
                // initialise the keystore
                char[] password = "password".toCharArray();
                KeyStore ks = KeyStore.getInstance("JKS");
                FileInputStream fis = new FileInputStream("testkey.jks");
                ks.load(fis, password);
    
                // setup the key manager factory
                KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
                kmf.init(ks, password);
    
                // setup the trust manager factory
                TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
                tmf.init(ks);
    
                // setup the HTTPS context and parameters
                sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
                httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext) {
                    public void configure(HttpsParameters params) {
                        try {
                            // initialise the SSL context
                            SSLContext context = getSSLContext();
                            SSLEngine engine = context.createSSLEngine();
                            params.setNeedClientAuth(false);
                            params.setCipherSuites(engine.getEnabledCipherSuites());
                            params.setProtocols(engine.getEnabledProtocols());
    
                            // Set the SSL parameters
                            SSLParameters sslParameters = context.getSupportedSSLParameters();
                            params.setSSLParameters(sslParameters);
    
                        } catch (Exception ex) {
                            System.out.println("Failed to create HTTPS port");
                        }
                    }
                });
                httpsServer.createContext("/test", new MyHandler());
                httpsServer.setExecutor(null); // creates a default executor
                httpsServer.start();
    
            } catch (Exception exception) {
                System.out.println("Failed to create HTTPS server on port " + 8000 + " of localhost");
                exception.printStackTrace();
    
            }
        }
    
    }
    

    创建自签名证书:

    keytool -genkeypair -keyalg RSA -alias selfsigned -keystore testkey.jks -storepass password -validity 360 -keysize 2048
    

    【讨论】:

    • setExecutor(null) 制作单线程服务器。要拥有多线程服务器,您需要提供适当的执行程序。例如:httpsServer.setExecutor(new ThreadPoolExecutor(4, 8, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100)));
    • 嗨,还是个新手。只是想知道您是否使用了设置配置器时创建的 SSLContext?由于您覆盖配置方法并调用 getDefault()。不使用构造函数中传递的 SSLContext。
    • 严格来说,你应该用response.getBytes().length而不是response.length()来调用t.sendResponseHeaders。它适用于示例中的字符串,但会因"Hëllö Wörld" 而中断。
    • context.getSupportedSSLParameters(); 应该换成 c.getSupportedSSLParameters(); 吧?
    【解决方案3】:

    提醒其他人:上述解决方案中的com.sun.net.httpserver.HttpsServer 不是Java 标准的一部分。尽管在 Oracle/OpenJDK JVM 中它是 bundled,但它并未包含在所有 JVM 中,因此这不会在任何地方开箱即用。

    有几个轻量级 HTTP 服务器可以嵌入到支持 HTTPS 并在任何 JVM 上运行的应用程序中。

    其中一个是JLHTTP - The Java Lightweight HTTP Server,它是一个没有依赖关系的小型单文件服务器(或~50K/35K jar)。设置密钥库、SSLContext 等与上述类似,因为它也依赖于标准 JSSE 实现,或者您可以指定标准系统属性来配置 SSL。您可以查看FAQ 或代码及其文档了解详细信息。

    免责声明:我是 JLHTTP 的作者。您可以自己检查并确定它是否适合您的需求。我希望你觉得它有用:-)

    【讨论】:

      【解决方案4】:

      与ServerSocket

      您可以使用 HttpsServer 所构建的类来更加轻量级:ServerSocket。

      单线程

      以下程序是一个非常简单的单线程服务器,侦听端口 8443。消息使用 TLS 加密,使用 ./keystore.jks 中的密钥:

      public static void main(String... args) {
          var address = new InetSocketAddress("0.0.0.0", 8443);
      
          startSingleThreaded(address);
      }
      
      public static void startSingleThreaded(InetSocketAddress address) {
      
          System.out.println("Start single-threaded server at " + address);
      
          try (var serverSocket = getServerSocket(address)) {
      
              var encoding = StandardCharsets.UTF_8;
      
              // This infinite loop is not CPU-intensive since method "accept" blocks
              // until a client has made a connection to the socket
              while (true) {
                  try (var socket = serverSocket.accept();
                       // Use the socket to read the client's request
                       var reader = new BufferedReader(new InputStreamReader(
                               socket.getInputStream(), encoding.name()));
                       // Writing to the output stream and then closing it sends
                       // data to the client
                       var writer = new BufferedWriter(new OutputStreamWriter(
                               socket.getOutputStream(), encoding.name()))
                  ) {
                      getHeaderLines(reader).forEach(System.out::println);
      
                      writer.write(getResponse(encoding));
                      writer.flush();
      
                  } catch (IOException e) {
                      System.err.println("Exception while handling connection");
                      e.printStackTrace();
                  }
              }
          } catch (Exception e) {
              System.err.println("Could not create socket at " + address);
              e.printStackTrace();
          }
      }
      
      private static ServerSocket getServerSocket(InetSocketAddress address)
              throws Exception {
      
          // Backlog is the maximum number of pending connections on the socket,
          // 0 means that an implementation-specific default is used
          int backlog = 0;
      
          var keyStorePath = Path.of("./keystore.jks");
          char[] keyStorePassword = "pass_for_self_signed_cert".toCharArray();
      
          // Bind the socket to the given port and address
          var serverSocket = getSslContext(keyStorePath, keyStorePassword)
                  .getServerSocketFactory()
                  .createServerSocket(address.getPort(), backlog, address.getAddress());
      
          // We don't need the password anymore → Overwrite it
          Arrays.fill(keyStorePassword, '0');
      
          return serverSocket;
      }
      
      private static SSLContext getSslContext(Path keyStorePath, char[] keyStorePass)
              throws Exception {
      
          var keyStore = KeyStore.getInstance("JKS");
          keyStore.load(new FileInputStream(keyStorePath.toFile()), keyStorePass);
      
          var keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
          keyManagerFactory.init(keyStore, keyStorePass);
      
          var sslContext = SSLContext.getInstance("TLS");
          // Null means using default implementations for TrustManager and SecureRandom
          sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
          return sslContext;
      }
      
      private static String getResponse(Charset encoding) {
          var body = "The server says hi ?\r\n";
          var contentLength = body.getBytes(encoding).length;
      
          return "HTTP/1.1 200 OK\r\n" +
                  String.format("Content-Length: %d\r\n", contentLength) +
                  String.format("Content-Type: text/plain; charset=%s\r\n",
                          encoding.displayName()) +
                  // An empty line marks the end of the response's header
                  "\r\n" +
                  body;
      }
      
      private static List<String> getHeaderLines(BufferedReader reader)
              throws IOException {
          var lines = new ArrayList<String>();
          var line = reader.readLine();
          // An empty line marks the end of the request's header
          while (!line.isEmpty()) {
              lines.add(line);
              line = reader.readLine();
          }
          return lines;
      }
      

      Here's a project 使用这种基于套接字的方法。

      多线程

      要为服务器使用多个线程,您可以使用thread pool:

      public static void startMultiThreaded(InetSocketAddress address) {
      
          try (var serverSocket = getServerSocket(address)) {
      
              System.out.println("Started multi-threaded server at " + address);
      
              // A cached thread pool with a limited number of threads
              var threadPool = newCachedThreadPool(8);
      
              var encoding = StandardCharsets.UTF_8;
      
              // This infinite loop is not CPU-intensive since method "accept" blocks
              // until a client has made a connection to the socket
              while (true) {
                  try {
                      var socket = serverSocket.accept();
                      // Create a response to the request on a separate thread to
                      // handle multiple requests simultaneously
                      threadPool.submit(() -> {
      
                          try ( // Use the socket to read the client's request
                                var reader = new BufferedReader(new InputStreamReader(
                                        socket.getInputStream(), encoding.name()));
                                // Writing to the output stream and then closing it
                                // sends data to the client
                                var writer = new BufferedWriter(new OutputStreamWriter(
                                        socket.getOutputStream(), encoding.name()))
                          ) {
                              getHeaderLines(reader).forEach(System.out::println);
                              writer.write(getResponse(encoding));
                              writer.flush();
                              // We're done with the connection → Close the socket
                              socket.close();
      
                          } catch (Exception e) {
                              System.err.println("Exception while creating response");
                              e.printStackTrace();
                          }
                      });
                  } catch (IOException e) {
                      System.err.println("Exception while handling connection");
                      e.printStackTrace();
                  }
              }
          } catch (Exception e) {
              System.err.println("Could not create socket at " + address);
              e.printStackTrace();
          }
      }
      
      private static ExecutorService newCachedThreadPool(int maximumNumberOfThreads) {
          return new ThreadPoolExecutor(0, maximumNumberOfThreads,
                  60L, TimeUnit.SECONDS,
                  new SynchronousQueue<>());
      }
      

      创建证书

      使用keytool创建自签名证书(您可以从Let's Encrypt免费获得正确的证书):

      keytool -genkeypair -keyalg RSA -alias selfsigned -keystore keystore.jks \
              -storepass pass_for_self_signed_cert \
              -dname "CN=localhost, OU=Developers, O=Bull Bytes, L=Linz, C=AT"
      

      联系服务器

      启动服务器后,用curl连接它:

      curl -k https://localhost:8443
      

      这将从服务器获取消息:

      服务器打招呼?

      检查 curl 和您的服务器建立的协议和密码套件

      curl -kv https://localhost:8443
      

      使用 JDK 13 和 curl 7.66.0,这产生了

      使用 TLSv1.3 / TLS_AES_256_GCM_SHA384 的 SSL 连接


      有关该主题的更多信息,请参阅 Elliotte Rusty Harold 的 Java Network Programming。

      【讨论】:

      • 虽然这是 ServerSocket 和 TLS 基础知识的一个很好的例子,但它并不完全是 HTTP(S)...查看RFC 7230 和其中引用的相关 RFC 以了解有关其他内容的详细信息HTTP 服务器需要实现。
      • 非常感谢您的评论,阿米猜。你是对的,该代码旨在让人们开始。假设它只需要提供一个text/plain 响应,那么您是否有任何具体的想法?
      • 虽然返回一个固定的响应让事情变得更简单,但服务器仍然必须正确处理各种有效和无效的客户端请求 - 例如。其他 HTTP 方法(动词)、正确的错误处理和状态代码、标题折叠、请求行和标题语法验证、一些特定的标题处理……就在我的脑海中。简而言之,您无法控制客户端/请求,因此您必须能够根据规范响应它向您抛出的任何内容,这可能会或可能不会导致发送固定响应。
      • 低级实现!
      【解决方案5】:

      虽然这个问题真的很老了,但有人提到我这个话题并问是否可以简化。大多数答案都很好地展示了如何使用 sun 设置简单的 https 服务器,但我想提供一个替代方案,希望它更容易一些。

      对于此设置,我假设您已经拥有密钥库和信任库。

      其余端点:

      import com.sun.net.httpserver.HttpExchange;
      import com.sun.net.httpserver.HttpHandler;
      
      import java.io.IOException;
      import java.io.OutputStream;
      import java.nio.charset.StandardCharsets;
      
      public class HelloWorldController implements HttpHandler {
      
          @Override
          public void handle(HttpExchange exchange) throws IOException {
              try (OutputStream responseBody = exchange.getResponseBody()) {
      
                  exchange.getResponseHeaders().set("Content-Type", "text/plain");
      
                  String payload = "Hello";
                  exchange.sendResponseHeaders(200, payload.length());
                  responseBody.write(payload.getBytes(StandardCharsets.UTF_8));
              }
          }
      
      }
      

      服务器配置:

      import com.sun.net.httpserver.HttpsConfigurator;
      import com.sun.net.httpserver.HttpsParameters;
      import com.sun.net.httpserver.HttpsServer;
      import nl.altindag.server.controller.HelloWorldController;
      import nl.altindag.ssl.SSLFactory;
      
      import java.io.IOException;
      import java.net.InetSocketAddress;
      import java.util.concurrent.Executors;
      
      public class App {
      
          public static void main(String[] args) throws IOException {
              SSLFactory sslFactory = SSLFactory.builder()
                      .withIdentityMaterial("keystore.jks", "secret".toCharArray())
                      .withTrustMaterial("truststore.jks", "secret".toCharArray())
                      .build();
      
              InetSocketAddress socketAddress = new InetSocketAddress(8443);
              HttpsServer httpsServer = HttpsServer.create(socketAddress, 0);
      
              httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslFactory.getSslContext()) {
                  @Override
                  public void configure(HttpsParameters params) {
                      params.setSSLParameters(sslFactory.getSslParameters());
                  }
              });
      
              httpsServer.createContext("/api/hello", new HelloWorldController());
              httpsServer.setExecutor(Executors.newCachedThreadPool());
              httpsServer.start();
          }
      
      }
      

      我需要在这里添加一些免责声明...我使用来自Github - SSLContext-Kickstart 库的 SSLFactory 类来轻松构建 SSLContext。它由我维护。您不需要使用它,因为其他人提供了一种仅使用普通 java 构建它的方法。

      【讨论】:

      • 对于图书馆的用户来说,这被证明是一个非常干净的实现。谢谢!
      猜你喜欢
      • 1970-01-01
      • 2013-11-11
      • 1970-01-01
      • 2010-10-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-11-11
      • 1970-01-01
      相关资源
      最近更新 更多