简体   繁体   English

简单的 Java HTTPS 服务器

[英]Simple Java HTTPS server

I need to set up a really lightweight HTTPS server for a Java application.我需要为 Java 应用程序设置一个真正轻量级的 HTTPS 服务器。 It's a simulator that's being used in our development labs to simulate the HTTPS connections accepted by a piece of equipment in the wild.这是一个模拟器,我们的开发实验室正在使用它来模拟一台设备在野外接受的 HTTPS 连接。 Because it's purely a lightweight development tool and isn't used in production in any way at all, I'm quite happy to bypass certifications and as much negotiation as I can.因为它纯粹是一个轻量级的开发工具,根本没有以任何方式用于生产,所以我很乐意绕过认证和尽可能多的谈判。

I'm planning on using the HttpsServer class in Java 6 SE but I'm struggling to get it working.我计划在 Java 6 SE 中使用HttpsServer类,但我正在努力让它工作。 As a test client, I'm using wget from the cygwin command line ( wget https://[address]:[port] ) but wget reports that it was "Unable to establish SSL connection".作为测试客户端,我用wget从cygwin命令行( wget https://[address]:[port] ),但wget报道,这是“无法建立SSL连接”。

If I run wget with the -d option for debugging it tells me "SSL handshake failed".如果我使用-d选项运行wget进行调试,它会告诉我“SSL 握手失败”。

I've spent 30 minutes googling this and everything seems to just point back to the fairly useless Java 6 documentation that describes the methods but doesn't actually talk about how to get the darn thing talking or provide any example code at all.我花了 30 分钟在谷歌上搜索这个,一切似乎只是指向描述方法的相当无用的 Java 6 文档,但实际上并没有谈论如何让该死的事情说话或提供任何示例代码。

Can anyone nudge me in the right direction?谁能把我推向正确的方向?

What I eventually used was this:我最终使用的是这个:

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");
}

To generate a keystore:要生成密钥库:

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

See also here .另请参见此处

Potentially storepass and keypass might be different, in which case the ks.load and kmf.init must use storepass and keypass, respectively.可能 storepass 和 keypass 可能不同,在这种情况下, ks.loadkmf.init必须分别使用 storepass 和 keypass。

I updated your answer for a HTTPS server (not socket-based).我更新了您对 HTTPS 服务器(不是基于套接字的)的回答。 It might help with CSRF and AJAX calls.它可能有助于 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();

        }
    }

}

To create a self-signed certificate:要创建自签名证书:

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

With ServerSocket使用服务器ServerSocket

You can use the class that HttpsServer is built around to be even more light-weight: ServerSocket .您可以使用HttpsServer构建的类来实现更轻量级: ServerSocket

Single-threaded单线程

The following program is a very simple, single-threaded server listening on port 8443. Messages are encrypted with TLS using the keys in ./keystore.jks :以下程序是一个非常简单的单线程服务器,侦听端口 8443。使用./keystore.jks的密钥使用 TLS 对消息进行加密:

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 using this socket-based approach.这是一个使用这种基于套接字的方法的项目

Multi-threaded多线程

To use more than one thread for the server, you can employ a 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<>());
}

Create a certificate创建证书

Use the keytool to create a self-signed certificate (you can get a proper certificate from Let's Encrypt for free):使用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"

Contact the server联系服务器

After starting the server, connect to it with curl :启动服务器后,使用curl连接到它:

curl -k https://localhost:8443

This will fetch a message from the server:这将从服务器获取一条消息:

The server says hi 👋服务员打招呼👋

Inspect which protocol and cipher suite were established by curl and your server with检查 curl 和您的服务器建立了哪些协议和密码套件

curl -kv https://localhost:8443

Using JDK 13 and curl 7.66.0, this produced使用 JDK 13 和 curl 7.66.0,这产生了

SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384使用 TLSv1.3 / TLS_AES_256_GCM_SHA384 的 SSL 连接


Refer to Java Network Programming by Elliotte Rusty Harold for more on the topic.有关该主题的更多信息,请参阅 Elliotte Rusty Harold 的Java Network Programming

Although this question is really old, someone mentioned me this topic and asked if it could be simplified.虽然这个问题真的很老了,但有人提到了我这个话题,问是否可以简化。 Most of the answers demonstrate very well how to setup a simple https server with sun, but I want to provide an alternative which is hopefully a bit easier.大多数答案都很好地展示了如何使用 sun 设置一个简单的 https 服务器,但我想提供一个替代方案,希望它更容易一些。

For this setup I am assuming you already have the keystore and truststore in place.对于此设置,我假设您已经拥有密钥库和信任库。

The rest endpoint:其余端点:

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));
        }
    }

}

Server configuration:服务器配置:

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();
    }

}

I need to add some disclaimer here... I use SSLFactory class from the Github - SSLContext-Kickstart library to easily construct a SSLContext.我需要在这里添加一些免责声明...我使用Github - SSLContext-Kickstart库中的 SSLFactory 类来轻松构建 SSLContext。 It is maintained by me.它由我维护。 You don't need to use it as others have provided a way to construct it with just plain java.您不需要使用它,因为其他人已经提供了一种仅使用普通 java 构建它的方法。

Just a reminder to others: com.sun.net.httpserver.HttpsServer in the solutions above is not part of the Java standard.提醒其他人:上述解决方案中的com.sun.net.httpserver.HttpsServer不是 Java 标准的一部分。 Although it is bundled with the Oracle/OpenJDK JVM, it is not included in all JVMs so this will not work out of the box everywhere.尽管它与 Oracle/OpenJDK JVM 捆绑在一起,但并未包含在所有 JVM 中,因此这不会在任何地方开箱即用。

There are several lightweight HTTP servers out there that you can embed in your application that support HTTPS and run on any JVM.有几个轻量级 HTTP 服务器可以嵌入到支持 HTTPS 并在任何 JVM 上运行的应用程序中。

One of them is JLHTTP - The Java Lightweight HTTP Server which is a tiny one-file server (or ~50K/35K jar) with no dependencies.其中之一是JLHTTP - Java 轻量级 HTTP 服务器,它是一个没有依赖项的小型单文件服务器(或 ~50K/35K jar)。 Setting up the keystore, SSLContext etc. is similar to the above, since it also relies on the standard JSSE implementation, or you can specify the standard system properties to configure SSL.设置密钥库、SSLContext 等与上面类似,因为它也依赖于标准 JSSE 实现,或者您可以指定标准系统属性来配置 SSL。 You can see the FAQ or the code and its documentation for details.您可以查看常见问题解答或代码及其文档以了解详细信息。

Disclaimer: I'm the author of JLHTTP.免责声明:我是 JLHTTP 的作者。 You can check it out for yourself and determine if it suits your needs.您可以自行检查并确定它是否适合您的需求。 I hope you find it useful :-)希望对你有帮助 :-)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM