Простой HTTPS-сервер Java
Вопрос
Мне нужно настроить очень легкий HTTPS-сервер для приложения Java.Это симулятор, который используется в наших лабораториях разработки для моделирования HTTPS-соединений, принимаемых оборудованием в реальных условиях.Поскольку это всего лишь легкий инструмент разработки и вообще не используется в производстве, я вполне рад обойти сертификацию и как можно больше переговоров.
Я планирую использовать HttpsServer
class в Java 6 SE, но я изо всех сил пытаюсь заставить его работать.В качестве тестового клиента я использую wget
из командной строки cygwin (wget https://[address]:[port]
) но wget
сообщает, что «Не удалось установить SSL-соединение».
Если я побегу wget
с -d
вариант отладки сообщает мне: «Ошибка установления связи SSL».
Я потратил 30 минут на поиск этого в Google, и все, кажется, просто указывает на довольно бесполезную документацию Java 6, которая описывает методы, но на самом деле не говорит о том, как заставить эту чертову штуку говорить, или вообще не предоставляет какой-либо пример кода.
Может ли кто-нибудь подтолкнуть меня в правильном направлении?
Решение
В конечном итоге я использовал следующее:
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
Смотрите также здесь.
Потенциально storepass и keypass могут различаться, и в этом случае ks.load
и kmf.init
необходимо использовать storepass и keypass соответственно.
Другие советы
Я обновил ваш ответ для 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
Просто напоминание остальным: com.sun.net.httpserver.HttpsServer
в приведенных выше решениях не является частью стандарта Java.Хотя это в комплекте с JVM Oracle/OpenJDK он включен не во все JVM, поэтому он не везде будет работать «из коробки».
Существует несколько облегченных HTTP-серверов, которые вы можете встроить в свое приложение, поддерживающее HTTPS и работающее на любой JVM.
Один из них является JLHTTP — облегченный HTTP-сервер Java это крошечный однофайловый сервер (или jar ~ 50 КБ/35 КБ) без каких-либо зависимостей.Настройка хранилища ключей, SSLContext и т. д.аналогичен приведенному выше, поскольку он также опирается на стандартную реализацию JSSE, или вы можете указать стандартные системные свойства для настройки SSL.Вы можете увидеть Часто задаваемые вопросы или код и его документацию для получения подробной информации.
Отказ от ответственности:Я автор JLHTTP.Вы можете проверить это самостоятельно и определить, соответствует ли оно вашим потребностям.Надеюсь, вам это будет полезно :-)
С 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;
}
Многопоточный
Чтобы использовать более одного потока для сервера, вы можете использовать пул потоков:
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
для создания самозаверяющего сертификата (вы можете получить соответствующий сертификат на сайте Давайте зашифруем бесплатно):
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 -k https://localhost:8443
Это приведет к получению сообщения с сервера:
Сервер передает привет 👋
Проверьте, какой протокол и набор шифров были установлены Curl и вашим сервером с помощью
curl -kv https://localhost:8443
Используя JDK 11 и Curl 7.65.1, это создало
SSL-соединение с использованием TLSv1.3/TLS_AES_256_GCM_SHA384
Ссылаться на Сетевое программирование на Java Эллиотт Расти Гарольд, чтобы узнать больше по этой теме.